Skip to content

Commit 2bbda6a

Browse files
authored
Merge pull request #66 from pyvec/session-slides-video
Session details: videos + slides
2 parents df97de8 + 312dfa6 commit 2bbda6a

25 files changed

+650
-235
lines changed

program/admin.py

Lines changed: 112 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
from pathlib import PurePath
2+
from urllib.parse import quote
3+
4+
import requests
15
from django.contrib import admin
6+
from django.core.files.base import ContentFile
27
from django.db import transaction
8+
from django.utils.html import format_html
39

410
from program import pretalx, pretalx_sync
511
from program.models import Room, Slot, Speaker, Talk, Utility, Workshop
@@ -140,7 +146,6 @@ class TalkAdmin(admin.ModelAdmin):
140146
"fields": [
141147
"pretalx_code",
142148
"og_image",
143-
"video_id",
144149
],
145150
},
146151
),
@@ -153,6 +158,17 @@ class TalkAdmin(admin.ModelAdmin):
153158
],
154159
},
155160
),
161+
(
162+
"Slides and Video",
163+
{
164+
"fields": [
165+
"video_url",
166+
"video_image_html",
167+
"slides_file",
168+
"slides_description",
169+
],
170+
},
171+
),
156172
(
157173
"Talk info (edit in pretalx)",
158174
{
@@ -181,10 +197,29 @@ class TalkAdmin(admin.ModelAdmin):
181197
"minimum_python_knowledge",
182198
"minimum_topic_knowledge",
183199
"type",
200+
"video_image_html",
184201
]
185202
actions = [make_public, make_not_public, talk_update_from_pretalx]
186203
change_form_template = "program/admin/change_form_session.html"
187204

205+
@admin.display(description="Video Image")
206+
def video_image_html(self, obj: Talk):
207+
if not obj.video_image:
208+
return "(no image)"
209+
210+
html = (
211+
'<a href="{image_url}" style="display: inline-block">'
212+
'<img src="{image_url}" height="180"/><br>'
213+
'<span style="display: inline-block; margin-top: 1ex;">{image_name}</span>'
214+
"</a>"
215+
)
216+
217+
return format_html(
218+
html,
219+
image_url=obj.video_image.url,
220+
image_name=obj.video_image.name,
221+
)
222+
188223
def get_queryset(self, request):
189224
qs = super().get_queryset(request)
190225
qs = qs.prefetch_related("talk_speakers")
@@ -201,12 +236,52 @@ def get_readonly_fields(self, request, obj=None):
201236
return ro_fields
202237

203238
def save_model(self, request, obj: Talk, form, change: bool) -> None:
239+
if change:
240+
self._update_video_image(obj)
241+
204242
obj.save()
205243

206244
if not change and obj.pretalx_code:
207245
sync = create_pretalx_sync()
208246
sync.update_talks([obj])
209247

248+
def _update_video_image(self, talk: Talk):
249+
video_id = talk.video_id
250+
if not video_id:
251+
# Delete the existing image, if any.
252+
if talk.video_image:
253+
talk.video_image.delete(save=False)
254+
return
255+
256+
# Get the video ID of the current image:
257+
# the image is always named <video_id>.jpg
258+
image_video_id: str | None = None
259+
if talk.video_image:
260+
image_path = PurePath(talk.video_image.name)
261+
image_video_id = image_path.stem
262+
263+
# Check if the video ID has changed and download a new image when necessary.
264+
if video_id != image_video_id:
265+
image_data = self._download_youtube_video_image(video_id)
266+
talk.video_image.save(
267+
name=image_data.name,
268+
content=image_data,
269+
save=False,
270+
)
271+
272+
def _download_youtube_video_image(self, video_id: str) -> ContentFile:
273+
image_url = self._format_youtube_video_image_url(video_id)
274+
275+
with requests.get(image_url, timeout=30) as image_response:
276+
image_response.raise_for_status()
277+
return ContentFile(
278+
content=image_response.content,
279+
name=f"{video_id}.jpg",
280+
)
281+
282+
def _format_youtube_video_image_url(self, video_id):
283+
return f"https://img.youtube.com/vi/{quote(video_id)}/maxresdefault.jpg"
284+
210285

211286
@admin.action(description="Update from pretalx")
212287
def workshop_update_from_pretalx(modeladmin, request, queryset):
@@ -319,93 +394,92 @@ def save_model(self, request, obj: Workshop, form, change: bool) -> None:
319394

320395
@admin.register(Utility)
321396
class UtilityAdmin(admin.ModelAdmin):
322-
empty_value_display = 'not set'
397+
empty_value_display = "not set"
323398
list_display = [
324-
'title',
325-
'short_description',
326-
'url',
327-
'is_streamed',
399+
"title",
400+
"short_description",
401+
"url",
402+
"is_streamed",
328403
]
329404
list_editable = [
330-
'is_streamed',
405+
"is_streamed",
331406
]
332407
prepopulated_fields = {
333-
'slug': ['title'],
408+
"slug": ["title"],
334409
}
335410

336-
337411
@admin.display(description="Description", empty_value="not set")
338412
def short_description(self, obj: Utility) -> str | None:
339413
"""Shorten the description for admin inline.
340414
341415
If there is no description, return None, so the "empty_value" fires.
342416
"""
343-
return obj.description[:180] + '...' if obj.description else None
417+
return obj.description[:180] + "..." if obj.description else None
344418

345419

346420
@admin.register(Room)
347421
class RoomAdmin(admin.ModelAdmin):
348422
list_display = [
349-
'label',
350-
'order',
351-
'slug',
423+
"label",
424+
"order",
425+
"slug",
352426
]
353427
list_editable = [
354-
'order',
428+
"order",
355429
]
356430
fields = [
357-
'label',
358-
'order',
359-
'slug',
431+
"label",
432+
"order",
433+
"slug",
360434
]
361435
prepopulated_fields = {
362-
'slug': ['label'],
436+
"slug": ["label"],
363437
}
364438

365439

366440
@admin.register(Slot)
367441
class SlotAdmin(admin.ModelAdmin):
368442
list_display = [
369-
'event',
370-
'start',
371-
'end',
372-
'room',
443+
"event",
444+
"start",
445+
"end",
446+
"room",
373447
]
374448
list_filter = [
375-
'room',
449+
"room",
376450
]
377451
list_editable = [
378-
'room',
379-
'start',
380-
'end',
452+
"room",
453+
"start",
454+
"end",
381455
]
382456
fieldsets = [
383457
(
384-
'Event',
458+
"Event",
385459
{
386-
'description': 'Select only one of the following.',
387-
'fields': [
388-
'talk',
389-
'workshop',
390-
'utility',
460+
"description": "Select only one of the following.",
461+
"fields": [
462+
"talk",
463+
"workshop",
464+
"utility",
391465
],
392466
},
393467
),
394468
(
395-
'Times',
469+
"Times",
396470
{
397-
'fields': [
398-
('start', 'end'),
471+
"fields": [
472+
("start", "end"),
399473
],
400474
},
401475
),
402476
(
403477
None,
404478
{
405-
'fields': [
406-
'room',
479+
"fields": [
480+
"room",
407481
],
408482
},
409483
),
410484
]
411-
date_hierarchy = 'start'
485+
date_hierarchy = "start"

program/management/commands/pretalx_sync_submissions.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from django.core.management.base import BaseCommand
22

3-
from program import pretalx
4-
from program import pretalx_sync
3+
from program import pretalx, pretalx_sync
54

65

76
class Command(BaseCommand):

program/management/commands/program_generate_og_images.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def _generate_og_images(
5151
"""
5252
objects = model_type.objects.filter(is_public=True)
5353
self.stdout.write(
54-
f"Generating OG images for {len(objects)} {model_type._meta.verbose_name_plural}"
54+
f"Generating OG images for {len(objects)} "
55+
f"{model_type._meta.verbose_name_plural}",
5556
)
5657
for obj in objects:
5758
result = generator.generate_image(

program/management/commands/program_import_schedule.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ def add_arguments(self, parser: ArgumentParser) -> None:
1717

1818
def handle(self, xlsx: str, output: str, *args, **options) -> None:
1919
# Lazy imports to save a bit of memory.
20-
from program.schedule_import import ScheduleImporter
2120
from django.core import serializers
2221

22+
from program.schedule_import import ScheduleImporter
23+
2324
importer = ScheduleImporter()
2425
new_objects = importer.import_xlsx(xlsx)
2526
data = serializers.serialize("json", new_objects)

program/management/commands/program_link_og_images.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import pathlib
2+
import re
23
from functools import cached_property
34
from typing import Type
45

6+
from django.conf import settings
57
from django.core.management.base import BaseCommand
68

79
from program import models
810

9-
import re
10-
11-
12-
from django.conf import settings
13-
1411

1512
class Command(BaseCommand):
1613
@cached_property

program/migrations/0017_slot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Generated by Django 4.2.1 on 2023-08-28 19:37
22

3-
from django.db import migrations, models
43
import django.db.models.deletion
4+
from django.db import migrations, models
55

66

77
class Migration(migrations.Migration):

program/migrations/0018_room_alter_slot_room.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Generated by Django 4.2.1 on 2023-08-28 19:46
22

3-
from django.db import migrations, models
43
import django.db.models.deletion
4+
from django.db import migrations, models
55

66

77
class Migration(migrations.Migration):

program/migrations/0019_remove_slot_content_type_remove_slot_object_id_and_more.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Generated by Django 4.2.1 on 2023-08-28 20:22
22

3-
from django.db import migrations, models
43
import django.db.models.deletion
4+
from django.db import migrations, models
55

66

77
class Migration(migrations.Migration):

program/migrations/0020_room_slug.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@ class Migration(migrations.Migration):
3131
migrations.RunPython(
3232
code=populate_room_slugs,
3333
reverse_code=migrations.RunPython.noop,
34-
)
34+
),
3535
]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Generated by Django 4.2.1 on 2023-11-01 10:05
2+
3+
import re
4+
5+
import django.core.validators
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
dependencies = [
11+
("program", "0021_alter_room_options_remove_room_floor_room_order_and_more"),
12+
]
13+
14+
operations = [
15+
migrations.RemoveField(
16+
model_name="talk",
17+
name="video_id",
18+
),
19+
migrations.AddField(
20+
model_name="talk",
21+
name="video_image",
22+
field=models.ImageField(
23+
blank=True, null=True, upload_to="video-images/session/"
24+
),
25+
),
26+
migrations.AddField(
27+
model_name="talk",
28+
name="video_url",
29+
field=models.CharField(
30+
blank=True,
31+
help_text="YouTube video URL: <code>https://www.youtube.com/watch?v=&lt;VIDEO_ID&gt;</code>. Do not include any additional parameters, such as start time or tracking (UTM) parameters.",
32+
max_length=1024,
33+
null=True,
34+
validators=[
35+
django.core.validators.RegexValidator(
36+
code="invalid_youtube_url",
37+
message="Invalid YouTube URL format.",
38+
regex=re.compile(
39+
"^https://www\\.youtube\\.com/watch\\?v=(?P<video_id>[0-9A-Za-z_-]{11,})$"
40+
),
41+
)
42+
],
43+
verbose_name="Video URL",
44+
),
45+
),
46+
]

0 commit comments

Comments
 (0)