This commit is contained in:
Markos Gogoulos
2026-04-18 14:01:36 +03:00
parent b3f7669f53
commit 3486ed0e15
10 changed files with 113 additions and 20 deletions
+1 -2
View File
@@ -9,8 +9,7 @@ repos:
- id: isort - id: isort
args: ["--profile", "black"] args: ["--profile", "black"]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.1.0 rev: 24.10.0
hooks: hooks:
- id: black - id: black
language_version: python3 language_version: python3
additional_dependencies: [ 'click==8.0.4' ]
+1 -1
View File
@@ -1 +1 @@
VERSION = "8.93" VERSION = "8.97"
+2 -1
View File
@@ -191,6 +191,7 @@ class MediaList(APIView):
conditions |= Q(category__in=rbac_categories) conditions |= Q(category__in=rbac_categories)
media = base_queryset.filter(conditions).exclude(user=request.user).distinct() media = base_queryset.filter(conditions).exclude(user=request.user).distinct()
include_sharing_info = True
elif author_param: elif author_param:
user_queryset = User.objects.all() user_queryset = User.objects.all()
user = get_object_or_404(user_queryset, username=author_param) user = get_object_or_404(user_queryset, username=author_param)
@@ -268,7 +269,7 @@ class MediaList(APIView):
if include_sharing_info: if include_sharing_info:
prefetch_related_objects( prefetch_related_objects(
page, page,
Prefetch('permissions', queryset=MediaPermission.objects.select_related('user')), Prefetch('permissions', queryset=MediaPermission.objects.select_related('user').exclude(user=request.user)),
Prefetch('category', queryset=Category.objects.filter(is_rbac_category=True).prefetch_related('rbac_groups'), to_attr='rbac_categories_prefetched'), Prefetch('category', queryset=Category.objects.filter(is_rbac_category=True).prefetch_related('rbac_groups'), to_attr='rbac_categories_prefetched'),
) )
+41 -11
View File
@@ -7,6 +7,7 @@ from django.contrib.auth.decorators import login_required
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.utils.html import mark_safe, strip_tags
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from cms.version import VERSION from cms.version import VERSION
@@ -262,20 +263,42 @@ def video_chapters(request, friendly_token):
data = request_data.get("chapters") data = request_data.get("chapters")
if data is None: if data is None:
return JsonResponse({'success': False, 'error': 'Request must contain "chapters" array'}, status=400) return JsonResponse({'success': False, 'error': 'Request must contain "chapters" array'}, status=400)
if not isinstance(data, list):
return JsonResponse({'success': False, 'error': '"chapters" must be an array'}, status=400)
if len(data) > 200:
return JsonResponse({'success': False, 'error': 'Too many chapters (max 200)'}, status=400)
chapters = [] chapters = []
for _, chapter_data in enumerate(data): for chapter_data in data:
if not isinstance(chapter_data, dict):
continue
start_time = chapter_data.get('startTime') start_time = chapter_data.get('startTime')
end_time = chapter_data.get('endTime') end_time = chapter_data.get('endTime')
chapter_title = chapter_data.get('chapterTitle') chapter_title = chapter_data.get('chapterTitle')
if start_time and end_time and chapter_title:
chapters.append( if not isinstance(start_time, (int, float)):
{ continue
'startTime': start_time, if not isinstance(end_time, (int, float)):
'endTime': end_time, continue
'chapterTitle': chapter_title, if not isinstance(chapter_title, str) or not chapter_title.strip():
} continue
)
start_time = float(start_time)
end_time = float(end_time)
if start_time < 0 or end_time < 0 or start_time >= end_time:
continue
chapter_title = strip_tags(chapter_title).strip()[:500]
if not chapter_title:
continue
chapters.append(
{
'startTime': start_time,
'endTime': end_time,
'chapterTitle': chapter_title,
}
)
except Exception as e: # noqa except Exception as e: # noqa
return JsonResponse({'success': False, 'error': 'Request data must be a list of video chapters with startTime, endTime, chapterTitle'}, status=400) return JsonResponse({'success': False, 'error': 'Request data must be a list of video chapters with startTime, endTime, chapterTitle'}, status=400)
@@ -449,11 +472,18 @@ def edit_chapters(request):
if not (is_mediacms_editor(request.user) or request.user.has_contributor_access_to_media(media)): if not (is_mediacms_editor(request.user) or request.user.has_contributor_access_to_media(media)):
return HttpResponseRedirect("/") return HttpResponseRedirect("/")
chapters = media.chapter_data _html_escapes = str.maketrans({'<': r'\u003C', '>': r'\u003E', '&': r'\u0026'})
chapters_json = mark_safe(json.dumps(media.chapter_data).translate(_html_escapes))
return render( return render(
request, request,
"cms/edit_chapters.html", "cms/edit_chapters.html",
{"media_object": media, "add_subtitle_url": media.add_subtitle_url, "media_file_path": helpers.url_from_path(media.media_file.path), "media_id": media.friendly_token, "chapters": chapters}, {
"media_object": media,
"add_subtitle_url": media.add_subtitle_url,
"media_file_path": helpers.url_from_path(media.media_file.path),
"media_id": media.friendly_token,
"chapters": chapters_json,
},
) )
@@ -492,7 +492,7 @@ class NavMenuInlineTabs extends React.PureComponent {
</li> </li>
) : null} ) : null}
{this.props.onToggleSharingClick && {this.props.onToggleSharingClick &&
['media', 'shared_by_me'].includes(this.props.type) ? ( ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-sharing-toggle"> <li className="media-sharing-toggle">
<span <span
style={{ style={{
@@ -59,7 +59,7 @@ export function ProfileMediaSharing(props) {
<div ref={innerContainerRef} className="mi-filters-row-inner"> <div ref={innerContainerRef} className="mi-filters-row-inner">
{hasUsers ? ( {hasUsers ? (
<div className="mi-filter mi-filter-full-width"> <div className="mi-filter mi-filter-full-width">
<div className="mi-filter-title">{translateString('SHARED WITH USERS')}</div> <div className="mi-filter-title">{translateString('USERS SHARING')}</div>
<div className="mi-filter-options mi-filter-options-horizontal mi-sharing-filter-options"> <div className="mi-filter-options mi-filter-options-horizontal mi-sharing-filter-options">
<FilterOptions id="shared_user" options={usersOptions} selected={selectedUser} onSelect={onUserSelect} /> <FilterOptions id="shared_user" options={usersOptions} selected={selectedUser} onSelect={onUserSelect} />
</div> </div>
@@ -9,6 +9,7 @@ import ProfilePagesContent from '../components/profile-page/ProfilePagesContent'
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync'; import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync';
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters'; import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags'; import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
import { ProfileMediaSharing } from '../components/search-filters/ProfileMediaSharing';
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting'; import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers'; import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers';
@@ -45,11 +46,16 @@ export class ProfileSharedWithMePage extends Page {
hiddenFilters: true, hiddenFilters: true,
hiddenTags: true, hiddenTags: true,
hiddenSorting: true, hiddenSorting: true,
hiddenSharing: true,
filterArgs: '', filterArgs: '',
availableTags: [], availableTags: [],
selectedTag: 'all', selectedTag: 'all',
selectedSort: 'date_added_desc', selectedSort: 'date_added_desc',
selectedMedia: new Set(), // For select media mode selectedMedia: new Set(), // For select media mode
sharedUsers: [],
sharedGroups: [],
selectedSharingType: null,
selectedSharingValue: null,
}; };
this.authorDataLoad = this.authorDataLoad.bind(this); this.authorDataLoad = this.authorDataLoad.bind(this);
@@ -59,9 +65,11 @@ export class ProfileSharedWithMePage extends Page {
this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this); this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this);
this.onToggleTagsClick = this.onToggleTagsClick.bind(this); this.onToggleTagsClick = this.onToggleTagsClick.bind(this);
this.onToggleSortingClick = this.onToggleSortingClick.bind(this); this.onToggleSortingClick = this.onToggleSortingClick.bind(this);
this.onToggleSharingClick = this.onToggleSharingClick.bind(this);
this.onFiltersUpdate = this.onFiltersUpdate.bind(this); this.onFiltersUpdate = this.onFiltersUpdate.bind(this);
this.onTagSelect = this.onTagSelect.bind(this); this.onTagSelect = this.onTagSelect.bind(this);
this.onSortSelect = this.onSortSelect.bind(this); this.onSortSelect = this.onSortSelect.bind(this);
this.onSharingSelect = this.onSharingSelect.bind(this);
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this); this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
this.handleMediaSelection = this.handleMediaSelection.bind(this); this.handleMediaSelection = this.handleMediaSelection.bind(this);
@@ -175,6 +183,7 @@ export class ProfileSharedWithMePage extends Page {
hiddenFilters: !this.state.hiddenFilters, hiddenFilters: !this.state.hiddenFilters,
hiddenTags: true, hiddenTags: true,
hiddenSorting: true, hiddenSorting: true,
hiddenSharing: true,
}); });
} }
@@ -183,6 +192,7 @@ export class ProfileSharedWithMePage extends Page {
hiddenFilters: true, hiddenFilters: true,
hiddenTags: !this.state.hiddenTags, hiddenTags: !this.state.hiddenTags,
hiddenSorting: true, hiddenSorting: true,
hiddenSharing: true,
}); });
} }
@@ -191,6 +201,16 @@ export class ProfileSharedWithMePage extends Page {
hiddenFilters: true, hiddenFilters: true,
hiddenTags: true, hiddenTags: true,
hiddenSorting: !this.state.hiddenSorting, hiddenSorting: !this.state.hiddenSorting,
hiddenSharing: true,
});
}
onToggleSharingClick() {
this.setState({
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: true,
hiddenSharing: !this.state.hiddenSharing,
}); });
} }
@@ -203,6 +223,8 @@ export class ProfileSharedWithMePage extends Page {
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1], publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: this.state.selectedSort, sort_by: this.state.selectedSort,
tag: tag, tag: tag,
sharing_type: this.state.selectedSharingType,
sharing_value: this.state.selectedSharingValue,
}); });
}); });
} }
@@ -216,6 +238,23 @@ export class ProfileSharedWithMePage extends Page {
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1], publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: sortBy, sort_by: sortBy,
tag: this.state.selectedTag, tag: this.state.selectedTag,
sharing_type: this.state.selectedSharingType,
sharing_value: this.state.selectedSharingValue,
});
});
}
onSharingSelect(type, value) {
this.setState({ selectedSharingType: type, selectedSharingValue: value }, () => {
this.onFiltersUpdate({
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: this.state.selectedSort,
tag: this.state.selectedTag,
sharing_type: type,
sharing_value: value,
}); });
}); });
} }
@@ -229,6 +268,8 @@ export class ProfileSharedWithMePage extends Page {
sort_by: null, sort_by: null,
ordering: null, ordering: null,
t: null, t: null,
shared_user: null,
shared_group: null,
}; };
switch (updatedArgs.media_type) { switch (updatedArgs.media_type) {
@@ -290,6 +331,12 @@ export class ProfileSharedWithMePage extends Page {
args.t = updatedArgs.tag; args.t = updatedArgs.tag;
} }
if (updatedArgs.sharing_type === 'user' && updatedArgs.sharing_value) {
args.shared_user = updatedArgs.sharing_value;
} else if (updatedArgs.sharing_type === 'group' && updatedArgs.sharing_value) {
args.shared_group = updatedArgs.sharing_value;
}
const newArgs = []; const newArgs = [];
for (let arg in args) { for (let arg in args) {
@@ -341,6 +388,12 @@ export class ProfileSharedWithMePage extends Page {
.filter((tag) => tag); .filter((tag) => tag);
this.setState({ availableTags: tags }); this.setState({ availableTags: tags });
} }
if (responseData && responseData.shared_users !== undefined) {
this.setState({
sharedUsers: responseData.shared_users || [],
sharedGroups: responseData.shared_groups || [],
});
}
} }
handleMediaSelection(mediaId, isSelected) { handleMediaSelection(mediaId, isSelected) {
@@ -411,9 +464,11 @@ export class ProfileSharedWithMePage extends Page {
onToggleFiltersClick={this.onToggleFiltersClick} onToggleFiltersClick={this.onToggleFiltersClick}
onToggleTagsClick={this.onToggleTagsClick} onToggleTagsClick={this.onToggleTagsClick}
onToggleSortingClick={this.onToggleSortingClick} onToggleSortingClick={this.onToggleSortingClick}
onToggleSharingClick={this.onToggleSharingClick}
hasActiveFilters={hasActiveFilters} hasActiveFilters={hasActiveFilters}
hasActiveTags={this.state.selectedTag !== 'all'} hasActiveTags={this.state.selectedTag !== 'all'}
hasActiveSort={this.state.selectedSort !== 'date_added_desc'} hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
hasActiveSharing={!!this.state.selectedSharingValue}
hideChannelBanner={inEmbeddedApp()} hideChannelBanner={inEmbeddedApp()}
/> />
) : null, ) : null,
@@ -435,6 +490,14 @@ export class ProfileSharedWithMePage extends Page {
onTagSelect={this.onTagSelect} onTagSelect={this.onTagSelect}
/> />
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} /> <ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
<ProfileMediaSharing
hidden={this.state.hiddenSharing}
sharedUsers={this.state.sharedUsers}
sharedGroups={this.state.sharedGroups}
onSharingSelect={this.onSharingSelect}
selectedSharingType={this.state.selectedSharingType}
selectedSharingValue={this.state.selectedSharingValue}
/>
<LazyLoadItemListAsync <LazyLoadItemListAsync
key={this.state.requestUrl} key={this.state.requestUrl}
requestUrl={this.state.requestUrl} requestUrl={this.state.requestUrl}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,7 +16,7 @@
mediaId: "{{ media_object.friendly_token }}", mediaId: "{{ media_object.friendly_token }}",
redirectURL: "{{ media_object.get_absolute_url }}", redirectURL: "{{ media_object.get_absolute_url }}",
redirectUserMediaURL: "{{ media_object.user.get_absolute_url }}", redirectUserMediaURL: "{{ media_object.user.get_absolute_url }}",
chapters: {{ chapters|safe }}, chapters: {{ chapters }},
}; };
</script> </script>
{%endblock topimports %} {%endblock topimports %}