Skip to content

Commit 47729b6

Browse files
committed
✨(back) display in admin video duration and size
In the admin, we want to display for an organization, consumer site and playlist the addition of videos size and duration.
1 parent 228a8a8 commit 47729b6

File tree

3 files changed

+124
-18
lines changed

3 files changed

+124
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
1212

1313
- Reject transcoding job for video with too high resolution
1414
- Add fields to video to store its size and duration
15+
- Display in admin video duration and size
1516

1617
### Fixed
1718

src/backend/marsha/core/admin.py

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.contrib import admin
66
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin
77
from django.contrib.sites.models import Site
8+
from django.db.models import Sum
89
from django.urls import reverse
910
from django.utils.html import format_html
1011
from django.utils.translation import gettext_lazy as _
@@ -79,6 +80,27 @@ def _link_field(obj):
7980
return _link_field
8081

8182

83+
def display_human_size(size):
84+
"""Format a size in bytes into a human readable string."""
85+
if size is None:
86+
return "-"
87+
88+
suffixes = ["B", "KB", "MB", "GB", "TB", "PB"]
89+
while size >= 1024:
90+
size /= 1024
91+
suffixes.pop(0)
92+
93+
return f"{size:.2f} {suffixes[0]}"
94+
95+
96+
def display_human_duration(duration):
97+
"""Format a duration in seconds into a human readable string."""
98+
if duration is None:
99+
return "-"
100+
101+
return str(datetime.timedelta(seconds=duration))
102+
103+
82104
class BaseFileAdmin(admin.ModelAdmin):
83105
"""Base admin class for file model."""
84106

@@ -227,8 +249,34 @@ class ConsumerSiteAdmin(admin.ModelAdmin):
227249
"video_show_download_default",
228250
"inactive_resources",
229251
"inactive_features",
252+
"duration_usage",
253+
"size_usage",
230254
)
231-
readonly_fields = ["id", "created_on", "updated_on"]
255+
readonly_fields = [
256+
"id",
257+
"created_on",
258+
"updated_on",
259+
"duration_usage",
260+
"size_usage",
261+
]
262+
263+
@admin.display(description=_("Size"))
264+
def size_usage(self, instance):
265+
"""Return the size of the videos in the consumer site."""
266+
size_usage = Video.objects.filter(playlist__consumer_site=instance).aggregate(
267+
Sum("size")
268+
)["size__sum"]
269+
270+
return display_human_size(size_usage)
271+
272+
@admin.display(description=_("Duration"))
273+
def duration_usage(self, instance):
274+
"""Return the duration of the videos in the consumer site."""
275+
duration_usage = Video.objects.filter(
276+
playlist__consumer_site=instance
277+
).aggregate(Sum("duration"))["duration__sum"]
278+
279+
return display_human_duration(duration_usage)
232280

233281

234282
class OrganizationUsersInline(admin.TabularInline):
@@ -254,6 +302,29 @@ class OrganizationAdmin(admin.ModelAdmin):
254302
list_display = ("name",)
255303
inlines = [OrganizationUsersInline, OrganizationConsumerSitesInline]
256304

305+
readonly_fields = [
306+
"duration_usage",
307+
"size_usage",
308+
]
309+
310+
@admin.display(description=_("Size"))
311+
def size_usage(self, instance):
312+
"""Return the size of the videos in the organization."""
313+
size_usage = Video.objects.filter(playlist__organization=instance).aggregate(
314+
Sum("size")
315+
)["size__sum"]
316+
317+
return display_human_size(size_usage)
318+
319+
@admin.display(description=_("Duration"))
320+
def duration_usage(self, instance):
321+
"""Return the duration of the videos in the organization."""
322+
duration_usage = Video.objects.filter(
323+
playlist__organization=instance
324+
).aggregate(Sum("duration"))["duration__sum"]
325+
326+
return display_human_duration(duration_usage)
327+
257328

258329
class AudioTrackInline(admin.TabularInline):
259330
"""Inline for audio tracks of a video."""
@@ -317,24 +388,12 @@ class VideoAdmin(BaseFileAdmin):
317388
@admin.display(description=_("Size"))
318389
def display_size(self, obj):
319390
"""Return the size of the video."""
320-
if obj.size is None:
321-
return "-"
322-
323-
suffixes = ["B", "KB", "MB", "GB", "TB", "PB"]
324-
size = obj.size
325-
while size >= 1024:
326-
size /= 1024
327-
suffixes.pop(0)
328-
329-
return f"{size:.2f} {suffixes[0]}"
391+
return display_human_size(obj.size)
330392

331393
@admin.display(description=_("Duration"))
332394
def display_duration(self, obj):
333395
"""Return the duration of the video."""
334-
if obj.duration is None:
335-
return "-"
336-
337-
return str(datetime.timedelta(seconds=obj.duration))
396+
return display_human_duration(obj.duration)
338397

339398

340399
@admin.register(TimedTextTrack)
@@ -440,13 +499,17 @@ class PlaylistAdmin(admin.ModelAdmin):
440499
"retention_duration",
441500
"updated_on",
442501
"created_on",
502+
"duration_usage",
503+
"size_usage",
443504
)
444505
readonly_fields = [
445506
"id",
446507
"created_by",
447508
"created_on",
448509
"duplicated_from",
449510
"updated_on",
511+
"duration_usage",
512+
"size_usage",
450513
]
451514
list_filter = (
452515
"consumer_site__domain",
@@ -467,6 +530,24 @@ class PlaylistAdmin(admin.ModelAdmin):
467530
)
468531
verbose_name = _("Playlist")
469532

533+
@admin.display(description=_("Size"))
534+
def size_usage(self, instance):
535+
"""Return the size of the videos in the playlist."""
536+
size_usage = Video.objects.filter(playlist=instance).aggregate(Sum("size"))[
537+
"size__sum"
538+
]
539+
540+
return display_human_size(size_usage)
541+
542+
@admin.display(description=_("Duration"))
543+
def duration_usage(self, instance):
544+
"""Return the duration of the videos in the playlist."""
545+
duration_usage = Video.objects.filter(playlist=instance).aggregate(
546+
Sum("duration")
547+
)["duration__sum"]
548+
549+
return display_human_duration(duration_usage)
550+
470551

471552
@admin.register(LTIPassport)
472553
class LTIPassportAdmin(admin.ModelAdmin):

src/backend/marsha/core/tests/test_admin.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.test import TestCase
55
from django.urls import NoReverseMatch
66

7-
from marsha.core.admin import link_field
7+
from marsha.core.admin import display_human_duration, display_human_size, link_field
88
from marsha.core.factories import (
99
ConsumerSiteLTIPassportFactory,
1010
PlaylistLTIPassportFactory,
@@ -16,8 +16,8 @@
1616
# pylint: disable=unused-argument
1717

1818

19-
class AdminLinkFieldTestCase(TestCase):
20-
"""Test the `link_field` helper works as expected."""
19+
class AdminTestCase(TestCase):
20+
"""Test all admin helpers."""
2121

2222
def test_link_field_no_linked_object(self):
2323
"""Assert `link_field` works properly when no instance is linked."""
@@ -64,3 +64,27 @@ class Meta:
6464
link_field("linked_object")(instance_with_linked_object)
6565
reverse_exception = exc_context_mgr.exception
6666
self.assertIn("one_app_amodel_change", str(reverse_exception))
67+
68+
def test_display_human_duration(self):
69+
"""Assert `display_human_duration` works properly."""
70+
71+
self.assertEqual(display_human_duration(0), "0:00:00")
72+
self.assertEqual(display_human_duration(1), "0:00:01")
73+
self.assertEqual(display_human_duration(60), "0:01:00")
74+
self.assertEqual(display_human_duration(61), "0:01:01")
75+
self.assertEqual(display_human_duration(3600), "1:00:00")
76+
self.assertEqual(display_human_duration(3661), "1:01:01")
77+
self.assertEqual(display_human_duration(7500), "2:05:00")
78+
self.assertEqual(display_human_duration(None), "-")
79+
80+
def test_display_human_size(self):
81+
"""Assert `display_human_size` works properly."""
82+
83+
self.assertEqual(display_human_size(0), "0.00 B")
84+
self.assertEqual(display_human_size(1), "1.00 B")
85+
self.assertEqual(display_human_size(1024), "1.00 KB")
86+
self.assertEqual(display_human_size(1024**2), "1.00 MB")
87+
self.assertEqual(display_human_size(1024**3), "1.00 GB")
88+
self.assertEqual(display_human_size(1024**4), "1.00 TB")
89+
self.assertEqual(display_human_size(1024**5), "1.00 PB")
90+
self.assertEqual(display_human_size(None), "-")

0 commit comments

Comments
 (0)