Skip to content

Commit e212791

Browse files
committed
shared
1 parent ff3699f commit e212791

File tree

10 files changed

+133
-25
lines changed

10 files changed

+133
-25
lines changed

cms/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "7.4"
1+
VERSION = "7.8"

files/forms.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ def __init__(self, user, *args, **kwargs):
129129
self.user = user
130130
super(MediaPublishForm, self).__init__(*args, **kwargs)
131131

132+
# Check if media has sharing (custom permissions or rbac categories)
133+
self.has_custom_permissions = self.instance.permissions.exists() if self.instance.pk else False
134+
self.has_rbac_categories = self.instance.category.filter(is_rbac_category=True).exists() if self.instance.pk else False
135+
self.is_shared = self.has_custom_permissions or self.has_rbac_categories
136+
self.actual_state = self.instance.state if self.instance.pk else None
137+
132138
if not is_mediacms_editor(user):
133139
for field in ["featured", "reported_times", "is_reviewed"]:
134140
self.fields[field].disabled = True
@@ -141,6 +147,15 @@ def __init__(self, user, *args, **kwargs):
141147
valid_states.append(self.instance.state)
142148
self.fields["state"].choices = [(state, dict(MEDIA_STATES).get(state, state)) for state in valid_states]
143149

150+
# If media is shared, add "shared" as a state option and set it as initial
151+
if self.is_shared:
152+
current_choices = list(self.fields["state"].choices)
153+
current_choices.insert(0, ("shared", "Shared"))
154+
self.fields["state"].choices = current_choices
155+
self.fields["state"].initial = "shared"
156+
# Override the instance value to show "shared" in the form
157+
self.initial["state"] = "shared"
158+
144159
if getattr(settings, 'USE_RBAC', False) and 'category' in self.fields:
145160
if is_mediacms_editor(user):
146161
pass
@@ -179,7 +194,39 @@ def clean(self):
179194
state = cleaned_data.get("state")
180195
categories = cleaned_data.get("category")
181196

182-
if state in ['private', 'unlisted']:
197+
# If transitioning from "shared" state, handle appropriately
198+
if self.is_shared and state != "shared":
199+
self.fields['confirm_state'].widget = forms.CheckboxInput()
200+
state_index = None
201+
for i, layout_item in enumerate(self.helper.layout):
202+
if isinstance(layout_item, CustomField) and layout_item.fields[0] == 'state':
203+
state_index = i
204+
break
205+
206+
if state_index is not None:
207+
layout_items = list(self.helper.layout)
208+
layout_items.insert(state_index + 1, CustomField('confirm_state'))
209+
self.helper.layout = Layout(*layout_items)
210+
211+
if not cleaned_data.get('confirm_state'):
212+
if state == 'private':
213+
# When moving from shared to private, all sharing will be removed
214+
error_parts = []
215+
if self.has_rbac_categories:
216+
rbac_cat_titles = self.instance.category.filter(is_rbac_category=True).values_list('title', flat=True)
217+
error_parts.append(f"shared with users that have access to categories: {', '.join(rbac_cat_titles)}")
218+
if self.has_custom_permissions:
219+
error_parts.append("shared by me with other users (visible in 'Shared by me' page)")
220+
221+
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."
222+
self.add_error('confirm_state', error_message)
223+
else:
224+
# When moving from shared to unlisted/public, sharing is maintained
225+
error_message = f"I understand that changing to {state.title()} will maintain existing sharing settings."
226+
self.add_error('confirm_state', error_message)
227+
228+
# Original logic for non-shared states
229+
elif state in ['private', 'unlisted']:
183230
custom_permissions = self.instance.permissions.exists()
184231
rbac_categories = categories.filter(is_rbac_category=True).values_list('title', flat=True)
185232
if rbac_categories or custom_permissions:
@@ -190,7 +237,7 @@ def clean(self):
190237
state_index = i
191238
break
192239

193-
if state_index:
240+
if state_index is not None:
194241
layout_items = list(self.helper.layout)
195242
layout_items.insert(state_index + 1, CustomField('confirm_state'))
196243
self.helper.layout = Layout(*layout_items)
@@ -203,11 +250,24 @@ def clean(self):
203250
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"
204251
self.add_error('confirm_state', error_message)
205252

253+
# Convert "shared" state to actual underlying state for saving
254+
if state == "shared":
255+
cleaned_data["state"] = self.actual_state
256+
206257
return cleaned_data
207258

208259
def save(self, *args, **kwargs):
209260
data = self.cleaned_data
210261
state = data.get("state")
262+
263+
# If transitioning from shared to private, remove all sharing
264+
if self.is_shared and state == 'private' and data.get('confirm_state'):
265+
# Remove all custom permissions
266+
self.instance.permissions.all().delete()
267+
# Remove RBAC categories
268+
rbac_cats = self.instance.category.filter(is_rbac_category=True)
269+
self.instance.category.remove(*rbac_cats)
270+
211271
if state != self.initial["state"]:
212272
self.instance.state = get_next_state(self.user, self.initial["state"], self.instance.state)
213273

files/serializers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,17 @@ def __init__(self, *args, **kwargs):
101101
class SingleMediaSerializer(serializers.ModelSerializer):
102102
user = serializers.ReadOnlyField(source="user.username")
103103
url = serializers.SerializerMethodField()
104+
is_shared = serializers.SerializerMethodField()
104105

105106
def get_url(self, obj):
106107
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
107108

109+
def get_is_shared(self, obj):
110+
"""Check if media has custom permissions or RBAC categories"""
111+
custom_permissions = obj.permissions.exists()
112+
rbac_categories = obj.category.filter(is_rbac_category=True).exists()
113+
return custom_permissions or rbac_categories
114+
108115
class Meta:
109116
model = Media
110117
read_only_fields = (
@@ -133,6 +140,7 @@ class Meta:
133140
"edit_date",
134141
"media_type",
135142
"state",
143+
"is_shared",
136144
"duration",
137145
"thumbnail_url",
138146
"poster_url",

files/views/media.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,13 @@ def get(self, request, format=None):
226226
elif duration == '60-120':
227227
media = media.filter(duration__gte=3600)
228228

229-
if publish_state and publish_state in ['private', 'public', 'unlisted']:
230-
media = media.filter(state=publish_state)
229+
if publish_state:
230+
if publish_state == 'shared':
231+
# Filter media that have custom permissions OR RBAC categories
232+
shared_conditions = Q(permissions__isnull=False) | Q(category__is_rbac_category=True)
233+
media = media.filter(shared_conditions).distinct()
234+
elif publish_state in ['private', 'public', 'unlisted']:
235+
media = media.filter(state=publish_state)
231236

232237
if not already_sorted:
233238
media = media.order_by(f"{ordering}{sort_by}")

frontend/src/static/js/components/list-item/PlaylistItem.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ export function PlaylistItem(props) {
5353
<UnderThumbWrapper title={props.title} link={props.link}>
5454
{titleComponent()}
5555
{metaComponents()}
56-
<a href={props.link} title="" className="view-full-playlist">
56+
<span className="view-full-playlist">
5757
VIEW FULL PLAYLIST
58-
</a>
58+
</span>
5959
</UnderThumbWrapper>
6060
</div>
6161
</div>

frontend/src/static/js/components/media-page/ViewerInfoTitleBanner.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ export default class ViewerInfoTitleBanner extends React.PureComponent {
108108
render() {
109109
const displayViews = PageStore.get('config-options').pages.media.displayViews && void 0 !== this.props.views;
110110

111-
const mediaState = MediaPageStore.get('media-data').state;
111+
const mediaData = MediaPageStore.get('media-data');
112+
const mediaState = mediaData.state;
113+
const isShared = mediaData.is_shared;
112114

113115
let stateTooltip = '';
114116

@@ -121,6 +123,8 @@ export default class ViewerInfoTitleBanner extends React.PureComponent {
121123
break;
122124
}
123125

126+
const sharedTooltip = 'This media is shared with specific users or categories';
127+
124128
return (
125129
<div className="media-title-banner">
126130
{displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
@@ -129,15 +133,28 @@ export default class ViewerInfoTitleBanner extends React.PureComponent {
129133

130134
{void 0 !== this.props.title ? <h1>{this.props.title}</h1> : null}
131135

132-
{'public' !== mediaState ? (
136+
{isShared || 'public' !== mediaState ? (
133137
<div className="media-labels-area">
134138
<div className="media-labels-area-inner">
135-
<span className="media-label-state">
136-
<span>{mediaState}</span>
137-
</span>
138-
<span className="helper-icon" data-tooltip={stateTooltip}>
139-
<i className="material-icons">help_outline</i>
140-
</span>
139+
{isShared ? (
140+
<>
141+
<span className="media-label-state">
142+
<span>shared</span>
143+
</span>
144+
<span className="helper-icon" data-tooltip={sharedTooltip}>
145+
<i className="material-icons">help_outline</i>
146+
</span>
147+
</>
148+
) : 'public' !== mediaState ? (
149+
<>
150+
<span className="media-label-state">
151+
<span>{mediaState}</span>
152+
</span>
153+
<span className="helper-icon" data-tooltip={stateTooltip}>
154+
<i className="material-icons">help_outline</i>
155+
</span>
156+
</>
157+
) : null}
141158
</div>
142159
</div>
143160
) : null}

frontend/src/static/js/components/media-page/ViewerInfoVideoTitleBanner.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export default class ViewerInfoVideoTitleBanner extends ViewerInfoTitleBanner {
1010
render() {
1111
const displayViews = PageStore.get('config-options').pages.media.displayViews && void 0 !== this.props.views;
1212

13-
const mediaState = MediaPageStore.get('media-data').state;
13+
const mediaData = MediaPageStore.get('media-data');
14+
const mediaState = mediaData.state;
15+
const isShared = mediaData.is_shared;
1416

1517
let stateTooltip = '';
1618

@@ -23,6 +25,8 @@ export default class ViewerInfoVideoTitleBanner extends ViewerInfoTitleBanner {
2325
break;
2426
}
2527

28+
const sharedTooltip = 'This media is shared with specific users or categories';
29+
2630
return (
2731
<div className="media-title-banner">
2832
{displayViews && PageStore.get('config-options').pages.media.categoriesWithTitle
@@ -31,15 +35,28 @@ export default class ViewerInfoVideoTitleBanner extends ViewerInfoTitleBanner {
3135

3236
{void 0 !== this.props.title ? <h1>{this.props.title}</h1> : null}
3337

34-
{'public' !== mediaState ? (
38+
{isShared || 'public' !== mediaState ? (
3539
<div className="media-labels-area">
3640
<div className="media-labels-area-inner">
37-
<span className="media-label-state">
38-
<span>{mediaState}</span>
39-
</span>
40-
<span className="helper-icon" data-tooltip={stateTooltip}>
41-
<i className="material-icons">help_outline</i>
42-
</span>
41+
{isShared ? (
42+
<>
43+
<span className="media-label-state">
44+
<span>shared</span>
45+
</span>
46+
<span className="helper-icon" data-tooltip={sharedTooltip}>
47+
<i className="material-icons">help_outline</i>
48+
</span>
49+
</>
50+
) : 'public' !== mediaState ? (
51+
<>
52+
<span className="media-label-state">
53+
<span>{mediaState}</span>
54+
</span>
55+
<span className="helper-icon" data-tooltip={stateTooltip}>
56+
<i className="material-icons">help_outline</i>
57+
</span>
58+
</>
59+
) : null}
4360
</div>
4461
</div>
4562
) : null}

frontend/src/static/js/components/search-filters/ProfileMediaFilters.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const filters = {
3232
{ id: 'private', title: translateString('Private') },
3333
{ id: 'unlisted', title: translateString('Unlisted') },
3434
{ id: 'public', title: translateString('Published') },
35+
{ id: 'shared', title: translateString('Shared') },
3536
],
3637
sort_by: [
3738
{ id: 'date_added_desc', title: translateString('Upload date (newest)') },

static/js/_commons.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

static/js/media.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)