This commit is contained in:
Markos Gogoulos
2026-04-20 09:30:27 +03:00
parent 7c7441045d
commit 1261719996
5 changed files with 159 additions and 119 deletions
+1 -1
View File
@@ -1 +1 @@
VERSION = "8.991"
VERSION = "8.995"
+82 -74
View File
@@ -5,7 +5,7 @@ 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, Subtitle, Tag
from .models import MEDIA_STATES, Category, Media, MediaPermission, Subtitle, Tag
from .widgets import CategoryModalWidget
@@ -116,6 +116,7 @@ class MediaMetadataForm(forms.ModelForm):
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
@@ -131,10 +132,7 @@ class MediaPublishForm(forms.ModelForm):
self.request = kwargs.pop('request', None)
super(MediaPublishForm, self).__init__(*args, **kwargs)
self.has_custom_permissions = self.instance.permissions.exists() if self.instance.pk else False
self.has_rbac_categories = self.instance.category.filter(is_rbac_category=True).exists() if self.instance.pk else False
self.is_shared = self.has_custom_permissions or self.has_rbac_categories
self.actual_state = self.instance.state if self.instance.pk else None
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"]:
@@ -148,12 +146,8 @@ class MediaPublishForm(forms.ModelForm):
valid_states.append(self.instance.state)
self.fields["state"].choices = [(state, dict(MEDIA_STATES).get(state, state)) for state in valid_states]
if self.is_shared:
current_choices = list(self.fields["state"].choices)
current_choices.insert(0, ("shared", "Shared"))
self.fields["state"].choices = current_choices
self.fields["state"].initial = "shared"
self.initial["state"] = "shared"
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):
@@ -189,7 +183,59 @@ class MediaPublishForm(forms.ModelForm):
self.helper.form_show_errors = False
self.helper.layout = Layout(
CustomField('category'),
CustomField('state'),
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'),
@@ -217,80 +263,42 @@ class MediaPublishForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
state = cleaned_data.get("state")
categories = cleaned_data.get("category")
shared = cleaned_data.get("shared")
if self.is_shared and state != "shared":
self.fields['confirm_state'].widget = forms.CheckboxInput()
state_index = None
for i, layout_item in enumerate(self.helper.layout):
if isinstance(layout_item, CustomField) and layout_item.fields[0] == 'state':
state_index = i
break
if state_index is not None:
layout_items = list(self.helper.layout)
layout_items.insert(state_index + 1, CustomField('confirm_state'))
self.helper.layout = Layout(*layout_items)
if not cleaned_data.get('confirm_state'):
if state == 'private':
error_parts = []
if self.has_rbac_categories:
rbac_cat_titles = self.instance.category.filter(is_rbac_category=True).values_list('title', flat=True)
error_parts.append(f"shared with users that have access to categories: {', '.join(rbac_cat_titles)}")
if self.has_custom_permissions:
error_parts.append("shared by me with other users (visible in 'Shared by me' page)")
error_message = f"I understand that changing to Private will remove all sharing. Currently this media is {' and '.join(error_parts)}. All this sharing will be removed."
self.add_error('confirm_state', error_message)
else:
error_message = f"I understand that changing to {state.title()} will maintain existing sharing settings."
self.add_error('confirm_state', error_message)
elif state in ['private', 'unlisted']:
custom_permissions = self.instance.permissions.exists()
rbac_categories = categories.filter(is_rbac_category=True).values_list('title', flat=True)
if rbac_categories or custom_permissions:
self.fields['confirm_state'].widget = forms.CheckboxInput()
state_index = None
for i, layout_item in enumerate(self.helper.layout):
if isinstance(layout_item, CustomField) and layout_item.fields[0] == 'state':
state_index = i
break
if state_index is not None:
layout_items = list(self.helper.layout)
layout_items.insert(state_index + 1, CustomField('confirm_state'))
self.helper.layout = Layout(*layout_items)
if not cleaned_data.get('confirm_state'):
if rbac_categories:
error_message = f"I understand that although media state is {state}, the media is also shared with users that have access to categories: {', '.join(rbac_categories)}"
self.add_error('confirm_state', error_message)
if custom_permissions:
error_message = f"I understand that although media state is {state}, the media is also shared by me with other users, that I can see in the 'Shared by me' page"
self.add_error('confirm_state', error_message)
# Convert "shared" state to actual underlying state for saving. we dont keep shared state in DB
if state == "shared":
cleaned_data["state"] = self.actual_state
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 transitioning from shared to private, remove all sharing
if self.is_shared and state == 'private' and data.get('confirm_state'):
# Remove all custom permissions
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()
# Remove RBAC categories
rbac_cats = self.instance.category.filter(is_rbac_category=True)
self.instance.category.remove(*rbac_cats)
if state != self.initial["state"]:
self.instance.state = get_next_state(self.user, self.initial["state"], self.instance.state)
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)
+6
View File
@@ -965,6 +965,12 @@ class Media(models.Model):
return chapter_data.chapter_data
return data
@property
def is_shared(self):
if not self.pk:
return False
return self.permissions.exists() or self.category.filter(is_rbac_category=True).exists()
class MediaPermission(models.Model):
"""Model to store user permissions for media"""
+1
View File
@@ -78,6 +78,7 @@
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
</style>
<div class="form-group{% if field.errors %} has-error{% endif %}">
+69 -44
View File
@@ -1,44 +1,69 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% block headtitle %}Publish media - {{PORTAL_NAME}}{% endblock headtitle %}
{% block headermeta %}{% endblock headermeta %}
{% block extra_head %}
<script>
(function() {
var url = new URL(window.location.href);
if (sessionStorage.getItem('lms_embed_mode') === 'true' && url.searchParams.get('mode') !== 'lms_embed_mode') {
url.searchParams.set('mode', 'lms_embed_mode');
window.location.replace(url.toString());
}
})();
</script>
<style>
#div_id_state ul {
display: flex;
flex-wrap: wrap;
gap: 12px;
list-style: none;
padding: 0;
margin: 0;
}
#div_id_state ul li {
display: flex;
align-items: center;
gap: 6px;
}
</style>
{% endblock extra_head %}
{% block innercontent %}
<div class="user-action-form-wrap">
{% include "cms/media_nav.html" with active_tab="publish" %}
<div style="max-width: 900px; margin: 0 auto; padding: 20px; border-radius: 8px; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);">
{% csrf_token %}
{% crispy form %}
</div>
</div>
{% endblock innercontent %}
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% block headtitle %}Publish media - {{PORTAL_NAME}}{% endblock headtitle %}
{% block headermeta %}{% endblock headermeta %}
{% block extra_head %}
<script>
(function() {
var url = new URL(window.location.href);
if (sessionStorage.getItem('lms_embed_mode') === 'true' && url.searchParams.get('mode') !== 'lms_embed_mode') {
url.searchParams.set('mode', 'lms_embed_mode');
window.location.replace(url.toString());
}
})();
</script>
<style>
.state-options {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
align-items: center;
}
.state-option {
display: flex;
align-items: center;
gap: 0.4rem;
cursor: pointer;
font-weight: normal;
margin-bottom: 0;
}
.state-option input { cursor: pointer; }
.shared-option {
border-left: 1px solid #dee2e6;
padding-left: 1.5rem;
margin-left: 0.25rem;
}
.confirm-state-section {
margin-top: 0.75rem;
padding: 0.75rem 1rem;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
}
.confirm-state-section label {
display: flex;
align-items: flex-start;
gap: 0.5rem;
cursor: pointer;
font-weight: normal;
margin-bottom: 0;
}
.confirm-state-section input[type="checkbox"] {
margin-top: 3px;
flex-shrink: 0;
}
</style>
{% endblock extra_head %}
{% block innercontent %}
<div class="user-action-form-wrap">
{% include "cms/media_nav.html" with active_tab="publish" %}
<div style="max-width: 900px; margin: 0 auto; padding: 20px; border-radius: 8px; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);">
{% crispy form %}
</div>
</div>
{% endblock innercontent %}