mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-05-05 20:23:26 -04:00
state
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
VERSION = "8.991"
|
VERSION = "8.995"
|
||||||
|
|||||||
+82
-74
@@ -5,7 +5,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from .methods import get_next_state, is_mediacms_editor
|
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
|
from .widgets import CategoryModalWidget
|
||||||
|
|
||||||
|
|
||||||
@@ -116,6 +116,7 @@ class MediaMetadataForm(forms.ModelForm):
|
|||||||
|
|
||||||
class MediaPublishForm(forms.ModelForm):
|
class MediaPublishForm(forms.ModelForm):
|
||||||
confirm_state = forms.BooleanField(required=False, initial=False, label="Acknowledge sharing status", help_text="")
|
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:
|
class Meta:
|
||||||
model = Media
|
model = Media
|
||||||
@@ -131,10 +132,7 @@ class MediaPublishForm(forms.ModelForm):
|
|||||||
self.request = kwargs.pop('request', None)
|
self.request = kwargs.pop('request', None)
|
||||||
super(MediaPublishForm, self).__init__(*args, **kwargs)
|
super(MediaPublishForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.has_custom_permissions = self.instance.permissions.exists() if self.instance.pk else False
|
self.was_shared = self.instance.is_shared 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
|
|
||||||
|
|
||||||
if not is_mediacms_editor(user):
|
if not is_mediacms_editor(user):
|
||||||
for field in ["featured", "reported_times", "is_reviewed"]:
|
for field in ["featured", "reported_times", "is_reviewed"]:
|
||||||
@@ -148,12 +146,8 @@ class MediaPublishForm(forms.ModelForm):
|
|||||||
valid_states.append(self.instance.state)
|
valid_states.append(self.instance.state)
|
||||||
self.fields["state"].choices = [(state, dict(MEDIA_STATES).get(state, state)) for state in valid_states]
|
self.fields["state"].choices = [(state, dict(MEDIA_STATES).get(state, state)) for state in valid_states]
|
||||||
|
|
||||||
if self.is_shared:
|
self.fields["shared"].initial = self.was_shared
|
||||||
current_choices = list(self.fields["state"].choices)
|
self.initial["shared"] = self.was_shared
|
||||||
current_choices.insert(0, ("shared", "Shared"))
|
|
||||||
self.fields["state"].choices = current_choices
|
|
||||||
self.fields["state"].initial = "shared"
|
|
||||||
self.initial["state"] = "shared"
|
|
||||||
|
|
||||||
if getattr(settings, 'USE_RBAC', False) and 'category' in self.fields:
|
if getattr(settings, 'USE_RBAC', False) and 'category' in self.fields:
|
||||||
if is_mediacms_editor(user):
|
if is_mediacms_editor(user):
|
||||||
@@ -189,7 +183,59 @@ class MediaPublishForm(forms.ModelForm):
|
|||||||
self.helper.form_show_errors = False
|
self.helper.form_show_errors = False
|
||||||
self.helper.layout = Layout(
|
self.helper.layout = Layout(
|
||||||
CustomField('category'),
|
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('featured'),
|
||||||
CustomField('reported_times'),
|
CustomField('reported_times'),
|
||||||
CustomField('is_reviewed'),
|
CustomField('is_reviewed'),
|
||||||
@@ -217,80 +263,42 @@ class MediaPublishForm(forms.ModelForm):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
state = cleaned_data.get("state")
|
state = cleaned_data.get("state")
|
||||||
categories = cleaned_data.get("category")
|
shared = cleaned_data.get("shared")
|
||||||
|
|
||||||
if self.is_shared and state != "shared":
|
if self.was_shared and not shared and not cleaned_data.get('confirm_state'):
|
||||||
self.fields['confirm_state'].widget = forms.CheckboxInput()
|
if state == 'private':
|
||||||
state_index = None
|
error_parts = []
|
||||||
for i, layout_item in enumerate(self.helper.layout):
|
rbac_cat_titles = list(self.instance.category.filter(is_rbac_category=True).values_list('title', flat=True))
|
||||||
if isinstance(layout_item, CustomField) and layout_item.fields[0] == 'state':
|
if rbac_cat_titles:
|
||||||
state_index = i
|
error_parts.append(f"shared with users that have access to categories: {', '.join(rbac_cat_titles)}")
|
||||||
break
|
if self.instance.permissions.exists():
|
||||||
|
error_parts.append("shared by me with other users (visible in 'Shared by me' page)")
|
||||||
if state_index is not None:
|
detail = f" Currently this media is {' and '.join(error_parts)}." if error_parts else ""
|
||||||
layout_items = list(self.helper.layout)
|
self.add_error('confirm_state', f"I understand that changing to Private will remove all sharing.{detail}")
|
||||||
layout_items.insert(state_index + 1, CustomField('confirm_state'))
|
else:
|
||||||
self.helper.layout = Layout(*layout_items)
|
self.add_error('confirm_state', "I understand that unchecking Shared will affect existing sharing settings.")
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
state = data.get("state")
|
state = data.get("state")
|
||||||
|
shared = data.get("shared")
|
||||||
|
|
||||||
# If transitioning from shared to private, remove all sharing
|
if shared:
|
||||||
if self.is_shared and state == 'private' and data.get('confirm_state'):
|
if self.request and self.request.user.is_authenticated:
|
||||||
# Remove all custom permissions
|
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()
|
self.instance.permissions.all().delete()
|
||||||
# Remove RBAC categories
|
|
||||||
rbac_cats = self.instance.category.filter(is_rbac_category=True)
|
rbac_cats = self.instance.category.filter(is_rbac_category=True)
|
||||||
self.instance.category.remove(*rbac_cats)
|
self.instance.category.remove(*rbac_cats)
|
||||||
|
|
||||||
if state != self.initial["state"]:
|
if state != self.initial.get("state"):
|
||||||
self.instance.state = get_next_state(self.user, self.initial["state"], self.instance.state)
|
self.instance.state = get_next_state(self.user, self.initial.get("state"), self.instance.state)
|
||||||
|
|
||||||
media = super(MediaPublishForm, self).save(*args, **kwargs)
|
media = super(MediaPublishForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -965,6 +965,12 @@ class Media(models.Model):
|
|||||||
return chapter_data.chapter_data
|
return chapter_data.chapter_data
|
||||||
return 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):
|
class MediaPermission(models.Model):
|
||||||
"""Model to store user permissions for media"""
|
"""Model to store user permissions for media"""
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="form-group{% if field.errors %} has-error{% endif %}">
|
<div class="form-group{% if field.errors %} has-error{% endif %}">
|
||||||
|
|||||||
@@ -17,28 +17,53 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
#div_id_state ul {
|
.state-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 1.5rem;
|
||||||
list-style: none;
|
align-items: center;
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
#div_id_state ul li {
|
.state-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
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>
|
</style>
|
||||||
{% endblock extra_head %}
|
{% endblock extra_head %}
|
||||||
|
|
||||||
{% block innercontent %}
|
{% block innercontent %}
|
||||||
<div class="user-action-form-wrap">
|
<div class="user-action-form-wrap">
|
||||||
{% include "cms/media_nav.html" with active_tab="publish" %}
|
{% 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);">
|
<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 %}
|
||||||
{% crispy form %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock innercontent %}
|
{% endblock innercontent %}
|
||||||
Reference in New Issue
Block a user