1+ from pathlib import PurePath
2+ from urllib .parse import quote
3+
4+ import requests
15from django .contrib import admin
6+ from django .core .files .base import ContentFile
27from django .db import transaction
8+ from django .utils .html import format_html
39
410from program import pretalx , pretalx_sync
511from 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" )
212287def 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 )
321396class 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 )
347421class 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 )
367441class 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"
0 commit comments