Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 33s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Failing after 1m2s
Migration 049: 4 optionale TEXT-Spalten in media_asset_rights_declarations (person_consent_context, parental_consent_context, music_rights_context, third_party_rights_context) fuer Freitext zum Einwilligungskontext. Backend: - media_rights.py: write_rights_declaration speichert 4 Kontextfelder - media_assets.py: copyright_notice + 4 Kontextfelder in Bulk-Upload, RightsDeclarationBody, MediaAssetPatch, MediaBulkPatchBody - exercises.py: copyright_notice + 4 Kontextfelder in upload_exercise_media, wird in INSERT gespeichert Frontend (alle 3 Formulare): - RightsDeclarationDialog: Copyright-Eingabefeld (immer sichtbar) + Freitext-Textarea bei jeder Ja-Antwort (Personen, Minderjaehrige, Musik, Fremdinhalte) - ExerciseInlineFileMediaModal: gleiche Felder inline im Upload-Tab - ExerciseInlineEmbedModal: gleiche Felder inline - api.js: copyright_notice + 4 Kontextfelder in bulkUploadMediaAssets version: 0.8.77 module: media_rights 1.1.0, media_assets 1.14.0, exercises 2.21.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
289 lines
16 KiB
JavaScript
289 lines
16 KiB
JavaScript
/**
|
||
* Modal: Embed-URL als exercise_media anlegen und §11-Platzhalter einfügen.
|
||
*/
|
||
import React, { useEffect, useState } from 'react'
|
||
import api from '../utils/api'
|
||
import {
|
||
INLINE_MEDIA_SIZES,
|
||
DEFAULT_INLINE_MEDIA_SIZE,
|
||
sanitizeInlineMediaSize,
|
||
} from '../constants/inlineExerciseMedia'
|
||
import { sanitizeInlineMediaCaption } from '../utils/inlineMediaCaption'
|
||
|
||
const DECL_INIT = {
|
||
copyright_notice: '',
|
||
rights_holder_confirmed: false,
|
||
contains_identifiable_persons: null,
|
||
person_consent_confirmed: false,
|
||
person_consent_context: '',
|
||
contains_minors: null,
|
||
parental_consent_confirmed: false,
|
||
parental_consent_context: '',
|
||
contains_music: null,
|
||
music_rights_confirmed: false,
|
||
music_rights_context: '',
|
||
contains_third_party_content: null,
|
||
third_party_rights_confirmed: false,
|
||
third_party_rights_context: '',
|
||
}
|
||
|
||
function validateDecl(decl) {
|
||
if (!decl.rights_holder_confirmed)
|
||
return 'Bitte bestätigen, dass du die erforderlichen Rechte an diesem Medium besitzt.'
|
||
if (decl.contains_identifiable_persons === null)
|
||
return 'Bitte angeben, ob erkennbare Personen abgebildet sind.'
|
||
if (decl.contains_identifiable_persons && !decl.person_consent_confirmed)
|
||
return 'Bitte bestätigen, dass die Einwilligungen aller erkennbaren Personen vorliegen.'
|
||
if (decl.contains_minors === null)
|
||
return 'Bitte angeben, ob Minderjährige abgebildet sind.'
|
||
if (decl.contains_minors && !decl.parental_consent_confirmed)
|
||
return 'Bitte bestätigen, dass die Einwilligungen der Sorgeberechtigten vorliegen.'
|
||
if (decl.contains_music === null)
|
||
return 'Bitte angeben, ob das Medium Musik enthält.'
|
||
if (decl.contains_music && !decl.music_rights_confirmed)
|
||
return 'Bitte bestätigen, dass die erforderlichen Musikrechte vorliegen.'
|
||
if (decl.contains_third_party_content === null)
|
||
return 'Bitte angeben, ob fremde geschützte Inhalte enthalten sind.'
|
||
if (decl.contains_third_party_content && !decl.third_party_rights_confirmed)
|
||
return 'Bitte bestätigen, dass die Rechte an allen enthaltenen Fremdmaterialien vorliegen.'
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* @param {{
|
||
* open: boolean,
|
||
* onClose: () => void,
|
||
* exerciseId: number,
|
||
* onMediaListChanged: () => Promise<void>,
|
||
* onInserted: (exerciseMediaId: number, displaySize: string, caption?: string) => void,
|
||
* }} props
|
||
*/
|
||
export default function ExerciseInlineEmbedModal({
|
||
open,
|
||
onClose,
|
||
exerciseId,
|
||
onMediaListChanged,
|
||
onInserted,
|
||
}) {
|
||
const [url, setUrl] = useState('')
|
||
const [title, setTitle] = useState('')
|
||
const [displaySize, setDisplaySize] = useState(DEFAULT_INLINE_MEDIA_SIZE)
|
||
const [busy, setBusy] = useState(false)
|
||
const [decl, setDecl] = useState({ ...DECL_INIT })
|
||
const [declErr, setDeclErr] = useState('')
|
||
|
||
const setDeclField = (key, val) => setDecl((d) => ({ ...d, [key]: val }))
|
||
|
||
useEffect(() => {
|
||
if (!open) return
|
||
setUrl('')
|
||
setTitle('')
|
||
setDisplaySize(DEFAULT_INLINE_MEDIA_SIZE)
|
||
setDecl({ ...DECL_INIT })
|
||
setDeclErr('')
|
||
}, [open])
|
||
|
||
const submit = async () => {
|
||
const u = url.trim()
|
||
if (!u) {
|
||
alert('Bitte eine Embed-URL eingeben (https://…).')
|
||
return
|
||
}
|
||
const validErr = validateDecl(decl)
|
||
if (validErr) {
|
||
setDeclErr(validErr)
|
||
return
|
||
}
|
||
setDeclErr('')
|
||
const size = sanitizeInlineMediaSize(displaySize)
|
||
const fd = new FormData()
|
||
fd.append('embed_url', u)
|
||
fd.append('media_type', 'video')
|
||
fd.append('title', title.trim())
|
||
fd.append('description', '')
|
||
fd.append('context', 'ablauf')
|
||
fd.append('is_primary', 'false')
|
||
for (const [k, v] of Object.entries(decl)) {
|
||
fd.append(k, String(v))
|
||
}
|
||
setBusy(true)
|
||
try {
|
||
const row = await api.uploadExerciseMedia(exerciseId, fd)
|
||
const mid = row?.id
|
||
if (mid == null) {
|
||
throw new Error('Antwort ohne exercise_media-ID')
|
||
}
|
||
await onMediaListChanged()
|
||
const cap = sanitizeInlineMediaCaption(
|
||
title.trim() || u.replace(/^https?:\/\//i, '').slice(0, 96),
|
||
)
|
||
onInserted(Number(mid), size, cap)
|
||
onClose()
|
||
} catch (e) {
|
||
alert(e.message || String(e))
|
||
} finally {
|
||
setBusy(false)
|
||
}
|
||
}
|
||
|
||
if (!open) return null
|
||
|
||
return (
|
||
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && !busy && onClose()}>
|
||
<div
|
||
className="admin-modal-sheet"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="rte-inline-embed-title"
|
||
style={{ maxWidth: '480px', width: '100%', maxHeight: '90vh', display: 'flex', flexDirection: 'column' }}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="admin-modal-sheet__header">
|
||
<h3 id="rte-inline-embed-title" className="admin-modal-sheet__title">
|
||
Embed im Textfeld
|
||
</h3>
|
||
<button type="button" className="btn btn-secondary admin-modal-sheet__close" disabled={busy} onClick={onClose}>
|
||
Schließen
|
||
</button>
|
||
</div>
|
||
<div style={{ overflowY: 'auto', flex: 1, padding: '14px 16px' }}>
|
||
<label className="form-label">Embed-URL</label>
|
||
<input
|
||
type="url"
|
||
className="form-input"
|
||
placeholder="https://…"
|
||
value={url}
|
||
onChange={(e) => setUrl(e.target.value)}
|
||
disabled={busy}
|
||
/>
|
||
<label className="form-label" style={{ marginTop: '12px' }}>
|
||
Titel (optional)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
className="form-input"
|
||
value={title}
|
||
onChange={(e) => setTitle(e.target.value)}
|
||
disabled={busy}
|
||
/>
|
||
<label className="form-label" style={{ marginTop: '12px' }}>
|
||
Darstellung im Text
|
||
</label>
|
||
<select className="form-input" value={displaySize} onChange={(e) => setDisplaySize(e.target.value)} disabled={busy}>
|
||
{INLINE_MEDIA_SIZES.map((o) => (
|
||
<option key={o.value} value={o.value}>
|
||
{o.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
|
||
{/* P-06 Rechte-Erklärung */}
|
||
<div style={{ marginTop: '18px', paddingTop: '14px', borderTop: '1px solid var(--border)' }}>
|
||
<p style={{ fontSize: '0.82rem', fontWeight: 600, marginBottom: '10px', color: 'var(--text2)' }}>
|
||
Rechte-Erklärung <span style={{ fontWeight: 400, color: 'var(--text3)' }}>(VORLÄUFIG – p06-v1-conservative)</span>
|
||
</p>
|
||
|
||
<div style={{ marginBottom: '10px' }}>
|
||
<label htmlFor="emb-cr" style={{ display: 'block', fontSize: '0.82rem', fontWeight: 600, marginBottom: '3px' }}>Copyright-Angabe (optional)</label>
|
||
<input id="emb-cr" type="text" className="form-input" placeholder="z.B. © 2026 Lars Stommer, CC BY-NC"
|
||
value={decl.copyright_notice} onChange={(e) => setDeclField('copyright_notice', e.target.value)}
|
||
disabled={busy} style={{ fontSize: '0.85rem' }} />
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '10px' }}>
|
||
<input type="checkbox" id="emb-rhc" checked={decl.rights_holder_confirmed}
|
||
onChange={(e) => setDeclField('rights_holder_confirmed', e.target.checked)}
|
||
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="emb-rhc" style={{ fontSize: '0.85rem' }}>
|
||
Ich bestätige, dass ich die erforderlichen Rechte an diesem Medium besitze oder zur Veröffentlichung berechtigt bin. *
|
||
</label>
|
||
</div>
|
||
|
||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Erkennbare Personen abgebildet? *</legend>
|
||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cip" checked={decl.contains_identifiable_persons === true} onChange={() => setDeclField('contains_identifiable_persons', true)} disabled={busy} /> Ja</label>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cip" checked={decl.contains_identifiable_persons === false} onChange={() => setDeclField('contains_identifiable_persons', false)} disabled={busy} /> Nein</label>
|
||
</div>
|
||
{decl.contains_identifiable_persons === true && (
|
||
<div style={{ paddingLeft: 2 }}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||
<input type="checkbox" id="emb-pcc" checked={decl.person_consent_confirmed} onChange={(e) => setDeclField('person_consent_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="emb-pcc" style={{ fontSize: '0.85rem' }}>Einwilligungen aller erkennbaren Personen liegen vor. *</label>
|
||
</div>
|
||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Einwilligungskontext (optional)</label>
|
||
<textarea className="form-input" rows={2} placeholder="z.B. Schriftliche Einwilligung vom 2026-05-01" value={decl.person_consent_context} onChange={(e) => setDeclField('person_consent_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
|
||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Minderjährige abgebildet? *</legend>
|
||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cm" checked={decl.contains_minors === true} onChange={() => setDeclField('contains_minors', true)} disabled={busy} /> Ja</label>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cm" checked={decl.contains_minors === false} onChange={() => setDeclField('contains_minors', false)} disabled={busy} /> Nein</label>
|
||
</div>
|
||
{decl.contains_minors === true && (
|
||
<div style={{ paddingLeft: 2 }}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||
<input type="checkbox" id="emb-pcc2" checked={decl.parental_consent_confirmed} onChange={(e) => setDeclField('parental_consent_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="emb-pcc2" style={{ fontSize: '0.85rem' }}>Einwilligungen der Sorgeberechtigten liegen vor. *</label>
|
||
</div>
|
||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Einwilligungskontext (optional)</label>
|
||
<textarea className="form-input" rows={2} placeholder="z.B. Elterliche Einwilligung per E-Mail vom 2026-04-15" value={decl.parental_consent_context} onChange={(e) => setDeclField('parental_consent_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
|
||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
|
||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Musik enthalten? *</legend>
|
||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cmu" checked={decl.contains_music === true} onChange={() => setDeclField('contains_music', true)} disabled={busy} /> Ja</label>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-cmu" checked={decl.contains_music === false} onChange={() => setDeclField('contains_music', false)} disabled={busy} /> Nein</label>
|
||
</div>
|
||
{decl.contains_music === true && (
|
||
<div style={{ paddingLeft: 2 }}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||
<input type="checkbox" id="emb-mrc" checked={decl.music_rights_confirmed} onChange={(e) => setDeclField('music_rights_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="emb-mrc" style={{ fontSize: '0.85rem' }}>Musikrechte (GEMA / Lizenz) liegen vor. *</label>
|
||
</div>
|
||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Lizenz / GEMA-Kontext (optional)</label>
|
||
<textarea className="form-input" rows={2} placeholder="z.B. CC BY 4.0 oder GEMA-Freimeldung Nr. …" value={decl.music_rights_context} onChange={(e) => setDeclField('music_rights_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
|
||
<fieldset style={{ border: 'none', padding: 0, marginBottom: '4px' }}>
|
||
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Fremde geschützte Inhalte (Logos, Grafiken, Fotos Dritter)? *</legend>
|
||
<div style={{ display: 'flex', gap: '14px', marginBottom: '4px' }}>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-ctpc" checked={decl.contains_third_party_content === true} onChange={() => setDeclField('contains_third_party_content', true)} disabled={busy} /> Ja</label>
|
||
<label style={{ fontSize: '0.85rem' }}><input type="radio" name="emb-ctpc" checked={decl.contains_third_party_content === false} onChange={() => setDeclField('contains_third_party_content', false)} disabled={busy} /> Nein</label>
|
||
</div>
|
||
{decl.contains_third_party_content === true && (
|
||
<div style={{ paddingLeft: 2 }}>
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '4px' }}>
|
||
<input type="checkbox" id="emb-tprc" checked={decl.third_party_rights_confirmed} onChange={(e) => setDeclField('third_party_rights_confirmed', e.target.checked)} disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
|
||
<label htmlFor="emb-tprc" style={{ fontSize: '0.85rem' }}>Rechte an allen enthaltenen Fremdmaterialien liegen vor. *</label>
|
||
</div>
|
||
<label style={{ display: 'block', fontSize: '0.8rem', color: 'var(--text2)', marginBottom: '2px' }}>Rechtsgrundlage (optional)</label>
|
||
<textarea className="form-input" rows={2} placeholder="z.B. Verbandslogo mit Genehmigung vom 2026-03-10" value={decl.third_party_rights_context} onChange={(e) => setDeclField('third_party_rights_context', e.target.value)} disabled={busy} style={{ fontSize: '0.82rem', resize: 'vertical' }} />
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
|
||
{declErr && (
|
||
<p style={{ color: 'var(--danger)', fontSize: '0.85rem', marginTop: '8px' }}>{declErr}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', flexShrink: 0 }}>
|
||
<button type="button" className="btn btn-primary btn-full" disabled={busy} onClick={submit}>
|
||
{busy ? 'Speichern…' : 'Hinzufügen & in Text einfügen'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|