1
+ from pathlib import PurePath
2
+ from urllib .parse import quote
3
+
4
+ import requests
1
5
from django .contrib import admin
6
+ from django .core .files .base import ContentFile
2
7
from django .db import transaction
8
+ from django .utils .html import format_html
3
9
4
10
from program import pretalx , pretalx_sync
5
11
from program .models import Room , Slot , Speaker , Talk , Utility , Workshop
@@ -140,7 +146,6 @@ class TalkAdmin(admin.ModelAdmin):
140
146
"fields" : [
141
147
"pretalx_code" ,
142
148
"og_image" ,
143
- "video_id" ,
144
149
],
145
150
},
146
151
),
@@ -153,6 +158,17 @@ class TalkAdmin(admin.ModelAdmin):
153
158
],
154
159
},
155
160
),
161
+ (
162
+ "Slides and Video" ,
163
+ {
164
+ "fields" : [
165
+ "video_url" ,
166
+ "video_image_html" ,
167
+ "slides_file" ,
168
+ "slides_description" ,
169
+ ],
170
+ },
171
+ ),
156
172
(
157
173
"Talk info (edit in pretalx)" ,
158
174
{
@@ -181,10 +197,29 @@ class TalkAdmin(admin.ModelAdmin):
181
197
"minimum_python_knowledge" ,
182
198
"minimum_topic_knowledge" ,
183
199
"type" ,
200
+ "video_image_html" ,
184
201
]
185
202
actions = [make_public , make_not_public , talk_update_from_pretalx ]
186
203
change_form_template = "program/admin/change_form_session.html"
187
204
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
+
188
223
def get_queryset (self , request ):
189
224
qs = super ().get_queryset (request )
190
225
qs = qs .prefetch_related ("talk_speakers" )
@@ -201,12 +236,52 @@ def get_readonly_fields(self, request, obj=None):
201
236
return ro_fields
202
237
203
238
def save_model (self , request , obj : Talk , form , change : bool ) -> None :
239
+ if change :
240
+ self ._update_video_image (obj )
241
+
204
242
obj .save ()
205
243
206
244
if not change and obj .pretalx_code :
207
245
sync = create_pretalx_sync ()
208
246
sync .update_talks ([obj ])
209
247
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
+
210
285
211
286
@admin .action (description = "Update from pretalx" )
212
287
def workshop_update_from_pretalx (modeladmin , request , queryset ):
@@ -319,93 +394,92 @@ def save_model(self, request, obj: Workshop, form, change: bool) -> None:
319
394
320
395
@admin .register (Utility )
321
396
class UtilityAdmin (admin .ModelAdmin ):
322
- empty_value_display = ' not set'
397
+ empty_value_display = " not set"
323
398
list_display = [
324
- ' title' ,
325
- ' short_description' ,
326
- ' url' ,
327
- ' is_streamed' ,
399
+ " title" ,
400
+ " short_description" ,
401
+ " url" ,
402
+ " is_streamed" ,
328
403
]
329
404
list_editable = [
330
- ' is_streamed' ,
405
+ " is_streamed" ,
331
406
]
332
407
prepopulated_fields = {
333
- ' slug' : [' title' ],
408
+ " slug" : [" title" ],
334
409
}
335
410
336
-
337
411
@admin .display (description = "Description" , empty_value = "not set" )
338
412
def short_description (self , obj : Utility ) -> str | None :
339
413
"""Shorten the description for admin inline.
340
414
341
415
If there is no description, return None, so the "empty_value" fires.
342
416
"""
343
- return obj .description [:180 ] + ' ...' if obj .description else None
417
+ return obj .description [:180 ] + " ..." if obj .description else None
344
418
345
419
346
420
@admin .register (Room )
347
421
class RoomAdmin (admin .ModelAdmin ):
348
422
list_display = [
349
- ' label' ,
350
- ' order' ,
351
- ' slug' ,
423
+ " label" ,
424
+ " order" ,
425
+ " slug" ,
352
426
]
353
427
list_editable = [
354
- ' order' ,
428
+ " order" ,
355
429
]
356
430
fields = [
357
- ' label' ,
358
- ' order' ,
359
- ' slug' ,
431
+ " label" ,
432
+ " order" ,
433
+ " slug" ,
360
434
]
361
435
prepopulated_fields = {
362
- ' slug' : [' label' ],
436
+ " slug" : [" label" ],
363
437
}
364
438
365
439
366
440
@admin .register (Slot )
367
441
class SlotAdmin (admin .ModelAdmin ):
368
442
list_display = [
369
- ' event' ,
370
- ' start' ,
371
- ' end' ,
372
- ' room' ,
443
+ " event" ,
444
+ " start" ,
445
+ " end" ,
446
+ " room" ,
373
447
]
374
448
list_filter = [
375
- ' room' ,
449
+ " room" ,
376
450
]
377
451
list_editable = [
378
- ' room' ,
379
- ' start' ,
380
- ' end' ,
452
+ " room" ,
453
+ " start" ,
454
+ " end" ,
381
455
]
382
456
fieldsets = [
383
457
(
384
- ' Event' ,
458
+ " Event" ,
385
459
{
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" ,
391
465
],
392
466
},
393
467
),
394
468
(
395
- ' Times' ,
469
+ " Times" ,
396
470
{
397
- ' fields' : [
398
- (' start' , ' end' ),
471
+ " fields" : [
472
+ (" start" , " end" ),
399
473
],
400
474
},
401
475
),
402
476
(
403
477
None ,
404
478
{
405
- ' fields' : [
406
- ' room' ,
479
+ " fields" : [
480
+ " room" ,
407
481
],
408
482
},
409
483
),
410
484
]
411
- date_hierarchy = ' start'
485
+ date_hierarchy = " start"
0 commit comments