diff --git a/src/core/admin.py b/src/core/admin.py index 1494e08a694ed197a187173fc6569e4a7692f65a..bf17eb836cacce598e207f7beba249f72ac08a87 100644 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -678,7 +678,7 @@ class EventAdmin(admin.ModelAdmin): ), ( 'Data', - {'fields': ['name', 'slug', 'language', 'abstract', 'description_de', 'description_en', 'banner_image', 'additional_data']}, + {'fields': ['name', 'slug', 'language', 'abstract', 'description_de', 'description_en', 'banner_image', 'banner_image_url', 'additional_data']}, ), ) diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index b20ead0ddeaf3f61471cdadacbf872eeaf02ec94..b4539d5ac811cda06e218c61a9fdf19a389ea814 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -994,6 +994,12 @@ msgstr "Symbolbild für die Veranstaltung welches als Banner angezeigt wird" msgid "Event__banner_image" msgstr "Banner" +msgid "Event__banner_image_url__help" +msgstr "URL welche beim Import des Banner Images verwendet wurde" + +msgid "Event__banner_image_url" +msgstr "Banner Quell-URL" + msgid "Event__is_public__help" msgstr "wird im Fahrplan öffentlich angezeigt" diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index 8e2d4907c0aa41bceaf58c354c7e1430635c4f95..a14e196906e77746c661aa02694907522064148a 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -994,6 +994,12 @@ msgstr "representational image for this event, will be used as a banner" msgid "Event__banner_image" msgstr "banner" +msgid "Event__banner_image_url__help" +msgstr "URL of the image which was imported as banner" + +msgid "Event__banner_image_url" +msgstr "banner source URL" + msgid "Event__is_public__help" msgstr "shall the event be shown in the public timetable?" diff --git a/src/core/migrations/0159_event_banner_image_url_alter_event_banner_image.py b/src/core/migrations/0159_event_banner_image_url_alter_event_banner_image.py new file mode 100644 index 0000000000000000000000000000000000000000..86121be48e3b7c4477a395a357a5ff80f889811a --- /dev/null +++ b/src/core/migrations/0159_event_banner_image_url_alter_event_banner_image.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.2 on 2024-11-17 13:41 + +import core.models.events +import core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0158_invitation'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='banner_image_url', + field=models.URLField(blank=True, help_text='Event__banner_image_url__help', null=True, verbose_name='Event__banner_image_url'), + ), + migrations.AlterField( + model_name='event', + name='banner_image', + field=models.ImageField(blank=True, height_field='banner_image_height', help_text='Event__banner_image__help', null=True, upload_to=core.models.events.get_event_banner_filename, validators=[core.validators.ImageDimensionValidator(max_size=(2048, 1024), min_size=(100, 100), square=False)], verbose_name='Event__banner_image', width_field='banner_image_width'), + ), + ] diff --git a/src/core/models/events.py b/src/core/models/events.py index 36c12777f231b00f3ce52084ecea37bfb37da595..ff707ee50d40204a7992c0a116a3d6e8d118ea01 100644 --- a/src/core/models/events.py +++ b/src/core/models/events.py @@ -1,6 +1,7 @@ import logging import random from datetime import timedelta +from pathlib import Path from re import compile as re_compile from typing import Any from uuid import UUID, uuid4 @@ -30,13 +31,19 @@ from core.models.rooms import Room from core.models.shared import BackendMixin from core.models.tags import TaggedItemMixin, TagItem from core.models.users import PlatformUser -from core.utils import str2bool, str2timedelta +from core.utils import download_from_url, str2bool, str2timedelta +from core.validators import ImageDimensionValidator SIMPLE_TIME_RE = re_compile(r'(.*\s)?(\d+:\d+)$') logger = logging.getLogger(__name__) +def get_event_banner_filename(instance: 'Event', filename: Path | str): + ext = str(filename).rsplit('.', maxsplit=1)[-1] + return Path('events').joinpath(f'{instance.id}.{ext}') + + class EventDurationFormField(DurationField): def to_python(self, value): try: @@ -132,11 +139,26 @@ class Event(TaggedItemMixin, BackendMixin, ActivityLogMixin, models.Model): banner_image = models.ImageField( blank=True, null=True, + upload_to=get_event_banner_filename, height_field='banner_image_height', width_field='banner_image_width', help_text=_('Event__banner_image__help'), verbose_name=_('Event__banner_image'), + validators=[ + ImageDimensionValidator( + min_size=( + settings.EVENT_BANNER_WIDTH_MINIMUM, + settings.EVENT_BANNER_HEIGHT_MINIMUM, + ), + max_size=( + settings.EVENT_BANNER_WIDTH_MAXIMUM, + settings.EVENT_BANNER_HEIGHT_MAXIMUM, + ), + square=False, + ), + ], ) + banner_image_url = models.URLField(blank=True, null=True, help_text=_('Event__banner_image_url__help'), verbose_name=_('Event__banner_image_url')) is_public = models.BooleanField(default=False, help_text=_('Event__is_public__help'), verbose_name=_('Event__is_public')) recording = models.CharField( @@ -423,6 +445,15 @@ class Event(TaggedItemMixin, BackendMixin, ActivityLogMixin, models.Model): if obj.additional_data and obj.additional_data.get('do_not_record', False) is True: obj.recording = Event.Recording.NO + # load banner_image, if a URL is given (and has not changed since last time) + if banner_image_url := data.get('banner_image_url'): + if obj.banner_image_url != banner_image_url: + try: + obj.load_banner_image_from_url(banner_image_url) + except Exception: + # just log the error, if the event has no banner_image (yet), we don't care too much + logger.exception('Failed to download banner_image for (new) event %s: %s', obj.pk, banner_image_url) + obj.clean() return obj @@ -626,6 +657,19 @@ class Event(TaggedItemMixin, BackendMixin, ActivityLogMixin, models.Model): self.pk, ) + def load_banner_image_from_url(self, url: str, save: bool = True): + logger.debug('Loading banner_image for event %s (%s) from URL: %s', self.slug, self.pk, url) + + filename, data = download_from_url(url) + target_filename = get_event_banner_filename(self, filename) + + logger.debug('Storing banner_image for user %s (%s): %s', self.slug, self.pk, target_filename) + self.banner_image.save(target_filename, data) + self.banner_image_url = url + if save: + self.save(update_fields=['banner_image', 'banner_image_url']) + logger.debug('Updated banner_image for user %s (%s) as "%s" from URL: %s', self.slug, self.pk, target_filename, url) + def _EventAttachment_upload_path(instance, filename): # file will be uploaded to MEDIA_ROOT/user_<id>/<filename> diff --git a/src/core/models/users.py b/src/core/models/users.py index 65d89795a317dd8919f34ef60688b26a7b162e09..64502faea558f7a34d9a20f0ba1190a5a20d548f 100644 --- a/src/core/models/users.py +++ b/src/core/models/users.py @@ -418,7 +418,6 @@ class PlatformUser(AbstractUser): if obj.avatar_url != avatar_url: try: obj.load_avatar_from_url(avatar_url) - obj.avatar_url = avatar_url except Exception: # just log the error, if the speaker has no avatar (yet), we don't care too much logger.exception('Failed to download avatar for (new) speaker user %s: %s', obj.pk, avatar_url) @@ -528,8 +527,9 @@ class PlatformUser(AbstractUser): logger.debug('Storing avatar for user %s (%s): %s', self.username, self.pk, target_filename) self.avatar.save(target_filename, data) + self.avatar_url = url if save: - self.save(update_fields=['avatar']) + self.save(update_fields=['avatar', 'avatar_url']) logger.debug('Updated avatar for user %s (%s) as "%s" from URL: %s', self.username, self.pk, target_filename, url) diff --git a/src/hub/settings/base.py b/src/hub/settings/base.py index 234f57d16e5d2cf402ba2e5f4b67d362acb3ed6f..d32ac92f72b99957f2976cc8be3ce880d2e2cd7f 100644 --- a/src/hub/settings/base.py +++ b/src/hub/settings/base.py @@ -116,6 +116,10 @@ env = environ.FileAwareEnv( BADGE_IMAGE_WIDTH_MAXIMUM=(int, 512), BADGE_IMAGE_HEIGHT_MAXIMUM=(int, 512), INVITATION_REJECTION_DEFAULT_TIMEOUT=(int, 3), + EVENT_BANNER_WIDTH_MINIMUM=(int, 100), + EVENT_BANNER_HEIGHT_MINIMUM=(int, 100), + EVENT_BANNER_WIDTH_MAXIMUM=(int, 2048), + EVENT_BANNER_HEIGHT_MAXIMUM=(int, 1024), USER_AVATAR_WIDTH_MINIMUM=(int, 64), USER_AVATAR_HEIGHT_MINIMUM=(int, 64), USER_AVATAR_WIDTH_MAXIMUM=(int, 1024), @@ -684,6 +688,11 @@ BADGE_IMAGE_HEIGHT_MINIMUM = env('BADGE_IMAGE_HEIGHT_MINIMUM') BADGE_IMAGE_WIDTH_MAXIMUM = env('BADGE_IMAGE_WIDTH_MAXIMUM') BADGE_IMAGE_HEIGHT_MAXIMUM = env('BADGE_IMAGE_HEIGHT_MAXIMUM') +EVENT_BANNER_WIDTH_MINIMUM = env('EVENT_BANNER_WIDTH_MINIMUM') +EVENT_BANNER_HEIGHT_MINIMUM = env('EVENT_BANNER_HEIGHT_MINIMUM') +EVENT_BANNER_WIDTH_MAXIMUM = env('EVENT_BANNER_WIDTH_MAXIMUM') +EVENT_BANNER_HEIGHT_MAXIMUM = env('EVENT_BANNER_HEIGHT_MAXIMUM') + USER_AVATAR_WIDTH_MINIMUM = env('USER_AVATAR_WIDTH_MINIMUM') USER_AVATAR_HEIGHT_MINIMUM = env('USER_AVATAR_HEIGHT_MINIMUM') USER_AVATAR_WIDTH_MAXIMUM = env('USER_AVATAR_WIDTH_MAXIMUM')