diff --git a/tests/videodinges/factories.py b/tests/videodinges/factories.py index 0867ab2..fffccd8 100644 --- a/tests/videodinges/factories.py +++ b/tests/videodinges/factories.py @@ -34,6 +34,13 @@ def create(model: Type[T], **kwargs) -> T: if model is models.Upload: return _create_with_defaults(models.Upload, kwargs, file=SimpleUploadedFile('some_file.txt', b'some contents')) + if model is models.Track: + return _create_with_defaults(models.Track, kwargs, + video=lambda: create(models.Video), + lang='en', + upload=lambda: create(models.Upload) + ) + raise NotImplementedError('Factory for %s not implemented' % model) diff --git a/tests/videodinges/models/test_track.py b/tests/videodinges/models/test_track.py new file mode 100644 index 0000000..3bb0efa --- /dev/null +++ b/tests/videodinges/models/test_track.py @@ -0,0 +1,96 @@ +from django.db.utils import IntegrityError + +from django.test import TestCase + +from tests.videodinges import factories, UploadMixin +from videodinges.models import Track, Video, Upload + + +class TrackTestCase(UploadMixin, TestCase): + def setUp(self): + super().setUp() + self.video = Video.objects.create(title='Title', slug='slug', description='Description') + + def test_model_is_created_with_required_fields(self): + Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload)) + track = Track.objects.all()[0] + self.assertEqual(track.video.slug, 'slug') + self.assertEqual(track.default, False) + self.assertEqual(track.kind, 'subtitles') + self.assertEqual(track.lang, 'en') + self.assertEqual(track.label, None) + self.assertEqual(track.upload.file.name, 'some_file.txt') + + def test_model_is_created_with_nonrequired_fields(self): + Track.objects.create( + video=self.video, + lang='en', + upload=factories.create(Upload), + default=True, + label='Something', + kind='chapters', + ) + track = Track.objects.all()[0] + self.assertEqual(track.video.slug, 'slug') + self.assertEqual(track.default, True) + self.assertEqual(track.kind, 'chapters') + self.assertEqual(track.lang, 'en') + self.assertEqual(track.label, 'Something') + self.assertEqual(track.upload.file.name, 'some_file.txt') + + def test_can_create_two_models(self): + model1 = Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload)) + model2 = Track.objects.create(video=self.video, lang='nl', upload=factories.create(Upload)) + self.assertEqual({model1, model2}, set((m for m in Track.objects.all()))) + + self.assertEqual(model1.video, self.video) + self.assertEqual(model1.default, False) + self.assertEqual(model1.kind, 'subtitles') + self.assertEqual(model1.lang, 'en') + + self.assertEqual(model2.video, self.video) + self.assertEqual(model2.default, False) + self.assertEqual(model2.kind, 'subtitles') + self.assertEqual(model2.lang, 'nl') + + def test_can_create_two_models_with_one_default(self): + model1 = Track.objects.create(video=self.video, default=True, lang='en', upload=factories.create(Upload)) + model2 = Track.objects.create(video=self.video, lang='nl', upload=factories.create(Upload)) + self.assertEqual({model1, model2}, set((m for m in Track.objects.all()))) + + self.assertEqual(model1.video, self.video) + self.assertEqual(model1.default, True) + self.assertEqual(model1.kind, 'subtitles') + self.assertEqual(model1.lang, 'en') + + self.assertEqual(model2.video, self.video) + self.assertEqual(model2.default, False) + self.assertEqual(model2.kind, 'subtitles') + self.assertEqual(model2.lang, 'nl') + + def test_cannot_set_default_twice(self): + video = factories.create(Video) + Track.objects.create(video=video, default=True, lang='en', upload=factories.create(Upload)) + with self.assertRaisesMessage(IntegrityError, 'UNIQUE constraint failed: tracks.video_id'): + Track.objects.create(video=video, default=True, lang='nl', upload=factories.create(Upload)) + + def test_cannot_set_two_subtitles_with_same_lang(self): + video = factories.create(Video) + Track.objects.create(video=video, lang='en', upload=factories.create(Upload)) + with self.assertRaisesMessage(IntegrityError, 'UNIQUE constraint failed: tracks.video_id'): + Track.objects.create(video=video, lang='en', upload=factories.create(Upload)) + + def test_can_create_two_models_with_same_lang_but_different_kind(self): + model1 = Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload)) + model2 = Track.objects.create(video=self.video, lang='en', kind='chapters', upload=factories.create(Upload)) + self.assertEqual({model1, model2}, set((m for m in Track.objects.all()))) + + self.assertEqual(model1.video, self.video) + self.assertEqual(model1.default, False) + self.assertEqual(model1.kind, 'subtitles') + self.assertEqual(model1.lang, 'en') + + self.assertEqual(model2.video, self.video) + self.assertEqual(model2.default, False) + self.assertEqual(model2.kind, 'chapters') + self.assertEqual(model2.lang, 'en') diff --git a/tests/videodinges/models/test_transcoding.py b/tests/videodinges/models/test_transcoding.py index 9e0fcbc..af4aa56 100644 --- a/tests/videodinges/models/test_transcoding.py +++ b/tests/videodinges/models/test_transcoding.py @@ -8,6 +8,7 @@ from videodinges.models import Transcoding, Video, qualities, transcoding_types, class TranscodingTestCase(TestCase): def setUp(self): + super().setUp() video = Video.objects.create(title='Title', slug='slug', description='Description') Transcoding.objects.create(video=video, quality=qualities[0].name, type=str(transcoding_types[0]), url='https://some_url') diff --git a/tests/videodinges/views/test_video.py b/tests/videodinges/views/test_video.py index b5dedb8..4804f36 100644 --- a/tests/videodinges/views/test_video.py +++ b/tests/videodinges/views/test_video.py @@ -246,3 +246,191 @@ class VideoTestCase(UploadMixin, TestCase): ) +class VideoWithTrackTestCase(UploadMixin, TestCase): + def setUp(self): + super().setUp() + self.client = Client() + self.video = factories.create( + models.Video, + title='Vid 1', + slug='vid-1', + default_quality='480p', + ) + factories.create( + models.Transcoding, + video=self.video, + quality='480p', + type='video/webm; codecs="vp9, opus"', + url='http://480p.webm', + ) + factories.create( + models.Transcoding, + video=self.video, + quality='480p', + type='video/mp4', + url='http://480p.mp4', + ) + + def test_video_view_renders_track_properly(self): + + factories.create( + models.Track, + video=self.video, + lang='en', + ) + + resp:HttpResponse = self.client.get(reverse('video', args=['vid-1'])) + + self.assertEqual(resp.status_code, 200) + + content:str = resp.content.decode(resp.charset) + + self.assertInHTML( + """""", + content, + ) + + def test_video_view_renders_multiple_tracks_properly(self): + + track_en = factories.create( + models.Track, + video=self.video, + lang='en', + ) + + track_nl = factories.create( + models.Track, + video=self.video, + lang='nl', + ) + + resp:HttpResponse = self.client.get(reverse('video', args=['vid-1'])) + + self.assertEqual(resp.status_code, 200) + + content:str = resp.content.decode(resp.charset) + + self.assertInHTML( + f"""""", + content, + ) + + + def test_video_view_renders_default_track_properly(self): + + factories.create( + models.Track, + video=self.video, + lang='en', + default=True + ) + + resp:HttpResponse = self.client.get(reverse('video', args=['vid-1'])) + + self.assertEqual(resp.status_code, 200) + + content:str = resp.content.decode(resp.charset) + + self.assertInHTML( + """""", + content, + ) + + def test_video_view_renders_multiple_tracks_properly_with_one_default(self): + + track_en = factories.create( + models.Track, + video=self.video, + lang='en', + default=True + ) + + track_nl = factories.create( + models.Track, + video=self.video, + lang='nl', + ) + + resp:HttpResponse = self.client.get(reverse('video', args=['vid-1'])) + + self.assertEqual(resp.status_code, 200) + + content:str = resp.content.decode(resp.charset) + + self.assertInHTML( + f"""""", + content, + ) + + def test_video_view_renders_track_label(self): + + factories.create( + models.Track, + video=self.video, + lang='en', + label='Some Label', + ) + + resp:HttpResponse = self.client.get(reverse('video', args=['vid-1'])) + + self.assertEqual(resp.status_code, 200) + + content:str = resp.content.decode(resp.charset) + + self.assertInHTML( + """""", + content, + ) + + + def test_video_view_renders_track_kind(self): + + factories.create( + models.Track, + video=self.video, + lang='en', + kind='captions', + ) + + resp:HttpResponse = self.client.get(reverse('video', args=['vid-1'])) + + self.assertEqual(resp.status_code, 200) + + content:str = resp.content.decode(resp.charset) + + self.assertInHTML( + """""", + content, + ) diff --git a/videodinges/admin.py b/videodinges/admin.py index bd352ae..8736f49 100644 --- a/videodinges/admin.py +++ b/videodinges/admin.py @@ -1,3 +1,5 @@ +from typing import Iterable + from django import forms from django.contrib import admin from django.core.exceptions import ValidationError @@ -17,16 +19,37 @@ class TranscodingsForm(forms.ModelForm): self.add_error('upload', validation_error) return cleaned_data +class TrackInlineFormset(forms.BaseInlineFormSet): + forms: Iterable[forms.ModelForm] + def clean(self): + cleaned_data = super().clean() + default_cnt = 0 + for form in self.forms: + try: + if form.cleaned_data['default'] is True: + default_cnt += 1 + except AttributeError: + pass + if default_cnt > 1: + form.add_error('default', ValidationError('Can set only one track as default')) + return cleaned_data + class TranscodingsInline(admin.StackedInline): model = models.Transcoding form = TranscodingsForm fields = ['quality', 'type', 'url', 'upload'] extra = 0 +class TracksInline(admin.StackedInline): + model = models.Track + formset = TrackInlineFormset + fields = ('default', 'kind', 'lang', 'label', 'upload') + extra = 0 + class VideoAdmin(admin.ModelAdmin): model = models.Video fields = ['title', 'description', 'slug', 'poster', 'og_image', 'created_at', 'default_quality'] - inlines = [TranscodingsInline] + inlines = (TranscodingsInline, TracksInline) list_display = ('title', 'slug', 'created_at', 'updated_at') ordering = ('-created_at', ) diff --git a/videodinges/models.py b/videodinges/models.py index eba1104..885bb70 100644 --- a/videodinges/models.py +++ b/videodinges/models.py @@ -108,6 +108,33 @@ class Transcoding(models.Model): ] db_table = 'transcodings' +class Track(models.Model): + KINDS = ( + 'subtitles', + 'captions', + 'descriptions', + 'chapters', + 'metadata', + ) + id = models.AutoField(primary_key=True) + video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='tracks') + default = models.BooleanField(default=False) + kind = models.CharField(choices=((kind, kind) for kind in KINDS), max_length=128, default=KINDS[0]) + lang = models.CharField(max_length=128) + label = models.CharField(max_length=128, blank=True, null=True) + upload = models.OneToOneField(Upload, on_delete=models.PROTECT) + + def __str__(self): + return '%s_%s' % (self.kind, self.lang) + + class Meta: + unique_together = ('video', 'kind', 'lang') + constraints = ( + constraints.UniqueConstraint( + fields=('video',), condition=Q(default=True), name='only_one_default_per_video'), + ) + db_table = 'tracks' + def get_quality_by_name(name: str) -> Optional[Quality]: for quality in qualities: if quality.name == name: diff --git a/videodinges/templates/video.html.j2 b/videodinges/templates/video.html.j2 index 102a51e..57f16c4 100644 --- a/videodinges/templates/video.html.j2 +++ b/videodinges/templates/video.html.j2 @@ -11,6 +11,9 @@
diff --git a/videodinges/views.py b/videodinges/views.py index 39bad45..0e2a938 100644 --- a/videodinges/views.py +++ b/videodinges/views.py @@ -51,6 +51,16 @@ def video(request: HttpRequest, slug: str) -> HttpResponse: template_data['qualities'] = qualities.keys() + template_data['tracks'] = [ + { + 'default': track.default, + 'src': track.upload.file.url, + 'srclang': track.lang, + 'kind': track.kind, + 'label': track.label or track.lang, + } for track in video.tracks.all() + ] + return render(request, 'video.html.j2', template_data, using='jinja2') def index(request: HttpRequest) -> HttpResponse: