mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-04-24 08:14:14 -04:00
462 lines
19 KiB
Python
462 lines
19 KiB
Python
from crispy_forms.bootstrap import FormActions
|
|
from crispy_forms.helper import FormHelper
|
|
from crispy_forms.layout import HTML, Field, Layout, Submit
|
|
from django import forms
|
|
from django.conf import settings
|
|
|
|
from .methods import get_next_state, is_mediacms_editor
|
|
from .models import MEDIA_STATES, Category, Media, MediaPermission, Subtitle, Tag
|
|
from .widgets import CategoryModalWidget
|
|
|
|
|
|
class CustomField(Field):
|
|
template = 'cms/crispy_custom_field.html'
|
|
|
|
|
|
class MultipleSelect(forms.CheckboxSelectMultiple):
|
|
input_type = "checkbox"
|
|
|
|
|
|
class MediaMetadataForm(forms.ModelForm):
|
|
new_tags = forms.CharField(label="Tags", help_text="a comma separated list of tags.", required=False)
|
|
|
|
class Meta:
|
|
model = Media
|
|
fields = (
|
|
"friendly_token",
|
|
"title",
|
|
"new_tags",
|
|
"add_date",
|
|
"uploaded_poster",
|
|
"description",
|
|
"enable_comments",
|
|
"thumbnail_time",
|
|
)
|
|
|
|
widgets = {
|
|
"new_tags": MultipleSelect(),
|
|
"description": forms.Textarea(attrs={'rows': 4}),
|
|
"add_date": forms.DateTimeInput(attrs={'type': 'datetime-local', 'step': '1'}, format='%Y-%m-%dT%H:%M:%S'),
|
|
"thumbnail_time": forms.NumberInput(attrs={'min': 0, 'step': 0.1}),
|
|
}
|
|
labels = {
|
|
"friendly_token": "Slug",
|
|
"uploaded_poster": "Poster Image",
|
|
"thumbnail_time": "Thumbnail Time (seconds)",
|
|
}
|
|
help_texts = {
|
|
"title": "",
|
|
"friendly_token": "Media URL slug",
|
|
"thumbnail_time": "Select the time in seconds for the video thumbnail",
|
|
"uploaded_poster": "Maximum file size: 5MB",
|
|
}
|
|
|
|
def __init__(self, user, *args, **kwargs):
|
|
self.user = user
|
|
super(MediaMetadataForm, self).__init__(*args, **kwargs)
|
|
if not getattr(settings, 'ALLOW_CUSTOM_MEDIA_URLS', False):
|
|
self.fields.pop("friendly_token")
|
|
if self.instance.media_type != "video":
|
|
self.fields.pop("thumbnail_time")
|
|
if self.instance.media_type == "image":
|
|
self.fields.pop("uploaded_poster")
|
|
|
|
self.fields["new_tags"].initial = ", ".join([tag.title for tag in self.instance.tags.all()])
|
|
|
|
self.helper = FormHelper()
|
|
self.helper.form_tag = True
|
|
self.helper.form_class = 'post-form'
|
|
self.helper.form_method = 'post'
|
|
self.helper.form_enctype = "multipart/form-data"
|
|
self.helper.form_show_errors = False
|
|
|
|
layout_fields = [
|
|
CustomField('title'),
|
|
CustomField('new_tags'),
|
|
CustomField('add_date'),
|
|
CustomField('description'),
|
|
CustomField('enable_comments'),
|
|
]
|
|
if self.instance.media_type != "image":
|
|
layout_fields.append(CustomField('uploaded_poster'))
|
|
|
|
self.helper.layout = Layout(*layout_fields)
|
|
|
|
if self.instance.media_type == "video":
|
|
self.helper.layout.append(CustomField('thumbnail_time'))
|
|
if getattr(settings, 'ALLOW_CUSTOM_MEDIA_URLS', False):
|
|
self.helper.layout.insert(0, CustomField('friendly_token'))
|
|
|
|
self.helper.layout.append(FormActions(Submit('submit', 'Update Media', css_class='primaryAction')))
|
|
|
|
def clean_friendly_token(self):
|
|
token = self.cleaned_data.get("friendly_token", "").strip()
|
|
|
|
if token:
|
|
if not all(c.isalnum() or c in "-_" for c in token):
|
|
raise forms.ValidationError("Slug can only contain alphanumeric characters, underscores, or hyphens.")
|
|
|
|
if Media.objects.filter(friendly_token=token).exclude(pk=self.instance.pk).exists():
|
|
raise forms.ValidationError("This slug is already in use. Please choose a different one.")
|
|
return token
|
|
|
|
def clean_uploaded_poster(self):
|
|
image = self.cleaned_data.get("uploaded_poster", False)
|
|
if image:
|
|
if image.size > 5 * 1024 * 1024:
|
|
raise forms.ValidationError("Image file too large ( > 5mb )")
|
|
return image
|
|
|
|
def save(self, *args, **kwargs):
|
|
data = self.cleaned_data # noqa
|
|
|
|
media = super(MediaMetadataForm, self).save(*args, **kwargs)
|
|
return media
|
|
|
|
|
|
class MediaPublishForm(forms.ModelForm):
|
|
confirm_state = forms.BooleanField(required=False, initial=False, label="Acknowledge sharing status", help_text="")
|
|
shared = forms.BooleanField(required=False, initial=False, label="Shared")
|
|
|
|
class Meta:
|
|
model = Media
|
|
fields = ("category", "state", "featured", "reported_times", "is_reviewed", "allow_download")
|
|
|
|
widgets = {
|
|
"category": CategoryModalWidget(),
|
|
"state": forms.RadioSelect(),
|
|
}
|
|
|
|
def __init__(self, user, *args, **kwargs):
|
|
self.user = user
|
|
self.request = kwargs.pop('request', None)
|
|
super(MediaPublishForm, self).__init__(*args, **kwargs)
|
|
|
|
self.was_shared = self.instance.is_shared if self.instance.pk else False
|
|
|
|
if not is_mediacms_editor(user):
|
|
for field in ["featured", "reported_times", "is_reviewed"]:
|
|
self.fields[field].disabled = True
|
|
self.fields[field].widget.attrs['class'] = 'read-only-field'
|
|
self.fields[field].widget.attrs['title'] = "This field can only be modified by MediaCMS admins or editors"
|
|
|
|
if settings.PORTAL_WORKFLOW not in ["public"]:
|
|
valid_states = ["unlisted", "private"]
|
|
if self.instance.state and self.instance.state not in valid_states:
|
|
valid_states.append(self.instance.state)
|
|
self.fields["state"].choices = [(state, dict(MEDIA_STATES).get(state, state)) for state in valid_states]
|
|
|
|
self.fields["shared"].initial = self.was_shared
|
|
self.initial["shared"] = self.was_shared
|
|
|
|
if getattr(settings, 'USE_RBAC', False) and 'category' in self.fields:
|
|
if is_mediacms_editor(user):
|
|
pass
|
|
else:
|
|
self.fields['category'].initial = self.instance.category.all()
|
|
|
|
non_rbac_categories = Category.objects.filter(is_rbac_category=False)
|
|
rbac_categories = user.get_rbac_categories_as_contributor()
|
|
combined_category_ids = list(non_rbac_categories.values_list('id', flat=True)) + list(rbac_categories.values_list('id', flat=True))
|
|
|
|
if self.instance.pk:
|
|
instance_category_ids = list(self.instance.category.all().values_list('id', flat=True))
|
|
combined_category_ids = list(set(combined_category_ids + instance_category_ids))
|
|
|
|
self.fields['category'].queryset = Category.objects.filter(id__in=combined_category_ids).order_by('title')
|
|
|
|
# Filter for LMS courses only when in embed mode
|
|
if self.request and 'category' in self.fields:
|
|
is_embed_mode = self._check_embed_mode()
|
|
if is_embed_mode:
|
|
current_queryset = self.fields['category'].queryset
|
|
self.fields['category'].queryset = current_queryset.filter(is_lms_course=True)
|
|
self.fields['category'].label = 'Course'
|
|
self.fields['category'].help_text = 'Media can be shared with one or more courses'
|
|
self.fields['category'].widget.is_lms_mode = True
|
|
|
|
self.helper = FormHelper()
|
|
self.helper.form_tag = True
|
|
self.helper.form_class = 'post-form'
|
|
self.helper.form_method = 'post'
|
|
self.helper.form_enctype = "multipart/form-data"
|
|
self.helper.form_show_errors = False
|
|
self.helper.layout = Layout(
|
|
CustomField('category'),
|
|
HTML(
|
|
"""
|
|
<div class="form-group{% if form.state.errors or form.confirm_state.errors %} has-error{% endif %}">
|
|
<div class="control-label-container">
|
|
<label class="control-label">State</label>
|
|
</div>
|
|
<div class="controls">
|
|
<div class="state-options">
|
|
{% for val, lbl in form.fields.state.choices %}{% if val == 'private' %}
|
|
<label class="state-option">
|
|
<input type="radio" name="state" value="private"
|
|
{% if form.state.value == 'private' %}checked{% endif %}>
|
|
Private
|
|
</label>
|
|
{% endif %}{% endfor %}
|
|
{% for val, lbl in form.fields.state.choices %}{% if val == 'unlisted' %}
|
|
<label class="state-option">
|
|
<input type="radio" name="state" value="unlisted"
|
|
{% if form.state.value == 'unlisted' %}checked{% endif %}>
|
|
Unlisted
|
|
</label>
|
|
{% endif %}{% endfor %}
|
|
<label class="state-option shared-option">
|
|
<input type="checkbox" name="shared" id="id_shared"
|
|
{% if form.shared.value %}checked{% endif %}>
|
|
Shared
|
|
</label>
|
|
{% for val, lbl in form.fields.state.choices %}{% if val == 'public' %}
|
|
<label class="state-option">
|
|
<input type="radio" name="state" value="public"
|
|
{% if form.state.value == 'public' %}checked{% endif %}>
|
|
Public
|
|
</label>
|
|
{% endif %}{% endfor %}
|
|
</div>
|
|
{% if form.state.errors %}
|
|
<div class="error-container" style="margin-top:0.5rem;">
|
|
{% for error in form.state.errors %}<p class="invalid-feedback">{{ error }}</p>{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if form.confirm_state.errors %}
|
|
<div class="confirm-state-section">
|
|
<label for="id_confirm_state">
|
|
<input type="checkbox" name="confirm_state" id="id_confirm_state"
|
|
{% if form.confirm_state.value %}checked{% endif %}>
|
|
<span>{{ form.confirm_state.errors.0 }}</span>
|
|
</label>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
"""
|
|
),
|
|
CustomField('featured'),
|
|
CustomField('reported_times'),
|
|
CustomField('is_reviewed'),
|
|
CustomField('allow_download'),
|
|
)
|
|
|
|
self.helper.layout.append(FormActions(Submit('submit', 'Publish Media', css_class='primaryAction')))
|
|
|
|
def _check_embed_mode(self):
|
|
"""Check if the current request is in embed mode"""
|
|
if not self.request:
|
|
return False
|
|
|
|
# Check query parameter
|
|
mode = self.request.GET.get('mode', '')
|
|
if mode == 'lms_embed_mode':
|
|
return True
|
|
|
|
# Check session storage
|
|
if self.request.session.get('lms_embed_mode') == 'true':
|
|
return True
|
|
|
|
return False
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
state = cleaned_data.get("state")
|
|
shared = cleaned_data.get("shared")
|
|
|
|
if self.was_shared and not shared and not cleaned_data.get('confirm_state'):
|
|
if state == 'private':
|
|
error_parts = []
|
|
rbac_cat_titles = list(self.instance.category.filter(is_rbac_category=True).values_list('title', flat=True))
|
|
if rbac_cat_titles:
|
|
error_parts.append(f"shared with users that have access to categories: {', '.join(rbac_cat_titles)}")
|
|
if self.instance.permissions.exists():
|
|
error_parts.append("shared by me with other users (visible in 'Shared by me' page)")
|
|
detail = f" Currently this media is {' and '.join(error_parts)}." if error_parts else ""
|
|
self.add_error('confirm_state', f"I understand that changing to Private will remove all sharing.{detail}")
|
|
else:
|
|
self.add_error('confirm_state', "I understand that unchecking Shared will affect existing sharing settings.")
|
|
|
|
return cleaned_data
|
|
|
|
def save(self, *args, **kwargs):
|
|
data = self.cleaned_data
|
|
state = data.get("state")
|
|
shared = data.get("shared")
|
|
|
|
if shared:
|
|
if self.request and self.request.user.is_authenticated:
|
|
MediaPermission.objects.get_or_create(
|
|
media=self.instance,
|
|
user=self.request.user,
|
|
defaults={'owner_user': self.request.user, 'permission': 'owner'},
|
|
)
|
|
elif state == 'private':
|
|
self.instance.permissions.all().delete()
|
|
rbac_cats = self.instance.category.filter(is_rbac_category=True)
|
|
self.instance.category.remove(*rbac_cats)
|
|
|
|
if state != self.initial.get("state"):
|
|
self.instance.state = get_next_state(self.user, self.initial.get("state"), self.instance.state)
|
|
|
|
media = super(MediaPublishForm, self).save(*args, **kwargs)
|
|
|
|
for course in media.category.filter(is_lms_course=True):
|
|
tag, _ = Tag.objects.get_or_create(title=course.title[:100])
|
|
media.tags.add(tag)
|
|
|
|
return media
|
|
|
|
|
|
class WhisperSubtitlesForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Media
|
|
fields = (
|
|
"allow_whisper_transcribe",
|
|
"allow_whisper_transcribe_and_translate",
|
|
)
|
|
labels = {
|
|
"allow_whisper_transcribe": "Transcription",
|
|
"allow_whisper_transcribe_and_translate": "English Translation",
|
|
}
|
|
help_texts = {
|
|
"allow_whisper_transcribe": "",
|
|
"allow_whisper_transcribe_and_translate": "",
|
|
}
|
|
|
|
def __init__(self, user, *args, **kwargs):
|
|
self.user = user
|
|
super(WhisperSubtitlesForm, self).__init__(*args, **kwargs)
|
|
|
|
if self.instance.allow_whisper_transcribe:
|
|
self.fields['allow_whisper_transcribe'].widget.attrs['readonly'] = True
|
|
self.fields['allow_whisper_transcribe'].widget.attrs['disabled'] = True
|
|
if self.instance.allow_whisper_transcribe_and_translate:
|
|
self.fields['allow_whisper_transcribe_and_translate'].widget.attrs['readonly'] = True
|
|
self.fields['allow_whisper_transcribe_and_translate'].widget.attrs['disabled'] = True
|
|
|
|
both_readonly = self.instance.allow_whisper_transcribe and self.instance.allow_whisper_transcribe_and_translate
|
|
|
|
self.helper = FormHelper()
|
|
self.helper.form_tag = True
|
|
self.helper.form_class = 'post-form'
|
|
self.helper.form_method = 'post'
|
|
self.helper.form_enctype = "multipart/form-data"
|
|
self.helper.form_show_errors = False
|
|
self.helper.layout = Layout(
|
|
CustomField('allow_whisper_transcribe'),
|
|
CustomField('allow_whisper_transcribe_and_translate'),
|
|
)
|
|
|
|
if not both_readonly:
|
|
self.helper.layout.append(FormActions(Submit('submit_whisper', 'Submit', css_class='primaryAction')))
|
|
else:
|
|
# Optional: Add a disabled button with explanatory text
|
|
self.helper.layout.append(
|
|
FormActions(Submit('submit_whisper', 'Submit', css_class='primaryAction', disabled=True), HTML('<small class="text-muted">Cannot submit - both options are already enabled</small>'))
|
|
)
|
|
|
|
def clean_allow_whisper_transcribe(self):
|
|
# Ensure the field value doesn't change if it was originally True
|
|
if self.instance and self.instance.allow_whisper_transcribe:
|
|
return self.instance.allow_whisper_transcribe
|
|
return self.cleaned_data['allow_whisper_transcribe']
|
|
|
|
def clean_allow_whisper_transcribe_and_translate(self):
|
|
# Ensure the field value doesn't change if it was originally True
|
|
if self.instance and self.instance.allow_whisper_transcribe_and_translate:
|
|
return self.instance.allow_whisper_transcribe_and_translate
|
|
return self.cleaned_data['allow_whisper_transcribe_and_translate']
|
|
|
|
|
|
class SubtitleForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Subtitle
|
|
fields = ["language", "subtitle_file"]
|
|
|
|
labels = {
|
|
"subtitle_file": "Upload Caption File",
|
|
}
|
|
help_texts = {
|
|
"subtitle_file": "SubRip (.srt) and WebVTT (.vtt) are supported file formats.",
|
|
}
|
|
|
|
def __init__(self, media_item, *args, **kwargs):
|
|
super(SubtitleForm, self).__init__(*args, **kwargs)
|
|
self.instance.media = media_item
|
|
|
|
self.helper = FormHelper()
|
|
self.helper.form_tag = True
|
|
self.helper.form_class = 'post-form'
|
|
self.helper.form_method = 'post'
|
|
self.helper.form_enctype = "multipart/form-data"
|
|
self.helper.form_show_errors = False
|
|
self.helper.layout = Layout(
|
|
CustomField('subtitle_file'),
|
|
CustomField('language'),
|
|
)
|
|
|
|
self.helper.layout.append(FormActions(Submit('submit', 'Submit', css_class='primaryAction')))
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.instance.user = self.instance.media.user
|
|
media = super(SubtitleForm, self).save(*args, **kwargs)
|
|
return media
|
|
|
|
|
|
class EditSubtitleForm(forms.Form):
|
|
subtitle = forms.CharField(widget=forms.Textarea, required=True)
|
|
|
|
def __init__(self, subtitle, *args, **kwargs):
|
|
super(EditSubtitleForm, self).__init__(*args, **kwargs)
|
|
self.fields["subtitle"].initial = subtitle.subtitle_file.read().decode("utf-8")
|
|
|
|
|
|
class ContactForm(forms.Form):
|
|
from_email = forms.EmailField(required=True)
|
|
name = forms.CharField(required=False)
|
|
message = forms.CharField(widget=forms.Textarea, required=True)
|
|
|
|
def __init__(self, user, *args, **kwargs):
|
|
super(ContactForm, self).__init__(*args, **kwargs)
|
|
self.fields["name"].label = "Your name:"
|
|
self.fields["from_email"].label = "Your email:"
|
|
self.fields["message"].label = "Please add your message here and submit:"
|
|
self.user = user
|
|
if user.is_authenticated:
|
|
self.fields.pop("name")
|
|
self.fields.pop("from_email")
|
|
|
|
|
|
class ReplaceMediaForm(forms.Form):
|
|
new_media_file = forms.FileField(
|
|
required=True,
|
|
label="New Media File",
|
|
help_text="Select a new file to replace the current media",
|
|
)
|
|
|
|
def __init__(self, media_instance, *args, **kwargs):
|
|
self.media_instance = media_instance
|
|
super(ReplaceMediaForm, self).__init__(*args, **kwargs)
|
|
|
|
self.helper = FormHelper()
|
|
self.helper.form_tag = True
|
|
self.helper.form_class = 'post-form'
|
|
self.helper.form_method = 'post'
|
|
self.helper.form_enctype = "multipart/form-data"
|
|
self.helper.form_show_errors = False
|
|
self.helper.layout = Layout(
|
|
CustomField('new_media_file'),
|
|
)
|
|
|
|
self.helper.layout.append(FormActions(Submit('submit', 'Replace Media', css_class='primaryAction')))
|
|
|
|
def clean_new_media_file(self):
|
|
file = self.cleaned_data.get("new_media_file", False)
|
|
if file:
|
|
if file.size > settings.UPLOAD_MAX_SIZE:
|
|
max_size_mb = settings.UPLOAD_MAX_SIZE / (1024 * 1024)
|
|
raise forms.ValidationError(f"File too large. Maximum size: {max_size_mb:.0f}MB")
|
|
return file
|