diff --git a/src/backoffice/forms/assemblies.py b/src/backoffice/forms/assemblies.py index 810ef2452b1d4e0cd64d81ad033f154ebdbb22e3..cb3d92f4d1f84daee102ded38c97a174cef9806a 100644 --- a/src/backoffice/forms/assemblies.py +++ b/src/backoffice/forms/assemblies.py @@ -1,8 +1,6 @@ import logging from django import forms -from django.core.exceptions import ValidationError -from django.core.validators import validate_slug from django.utils.translation import gettext_lazy as _ from core.base_forms import TranslatedFieldsForm @@ -11,6 +9,7 @@ from core.models import ( Assembly, AssemblyMember, ) +from core.utils import tags_from_string logger = logging.getLogger(__name__) @@ -99,22 +98,7 @@ class AssemblyEditForm(TranslatedFieldsForm): tags = forms.CharField(required=False) def clean_tags(self): - # try to detect people delimiting tags with space instead of comma - - tags = self.cleaned_data['tags'] - # allow empty tags - if tags.strip() == '': - return '' - - split_tags = tags.split(',') - if tags is not None and len(split_tags) == 1 and len(tags.split(' ')) > 2: - raise ValidationError(_('Assembly__tags__split_with_comma')) - - for tag in split_tags: - stripped_tag = tag.strip() - validate_slug(stripped_tag) - - return tags + return ','.join(tags_from_string(self.cleaned_data['tags'])) def clean(self): # call original .clean() which e.g. removes 'slug' from cleaned_data if that isn't a slug diff --git a/src/backoffice/forms/events.py b/src/backoffice/forms/events.py index 91695593dbc0f6c14389ee2cf3e4d7fa8edfbb7d..6430fce6fe08df42235ba9375d2b34b0a77ee1a3 100644 --- a/src/backoffice/forms/events.py +++ b/src/backoffice/forms/events.py @@ -11,7 +11,7 @@ from core.models import ( PlatformUser, Room, ) -from core.models.tags import clean_tags +from core.utils import tags_from_string logger = logging.getLogger(__name__) @@ -82,7 +82,7 @@ class EventForm(TranslatedFieldsForm): self.initial['tags_list'] = ', '.join(self.instance.sorted_tags) def clean_tags_list(self): - return clean_tags(self.cleaned_data['tags_list']) + return tags_from_string(self.cleaned_data['tags_list']) def clean(self): if self.cleaned_data.get('schedule_duration', None) is None: diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index fb8bd7e1b18b1c56a1fd2675c2c3b81f086e725b..4a5930c3afce0405d0bbd352eb0550ceab916964 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -23,9 +23,6 @@ msgstr "Bitte beachte den Hinweis und bestätige, dass du ihn gelesen und versta msgid "Assembly__slug__already_exists" msgstr "Dieser Kurzname wird bereits von einer anderen Assembly benutzt." -msgid "Assembly__tags__split_with_comma" -msgstr "Mehrere Tags bitte mit einem Komma trennen." - msgid "username" msgstr "Benutzername" @@ -434,6 +431,9 @@ msgstr "öffnet in neuem Fenster/Tab" msgid "Assembly__edit__children" msgstr "Zugeordnete Assemblies" +msgid "Assembly__tags__split_with_comma" +msgstr "Mehrere Tags bitte mit einem Komma trennen." + msgid "Assembly__edit__submit_btn" msgstr "Speichern" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index a32d0eabdf1e2e8c505021a0d6e82c3313bd348a..75c2068e7f9a97865a8b1e550b11a2c42fff0cae 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -23,9 +23,6 @@ msgstr "Please read the disclaimer and acknowledge that you have understood and msgid "Assembly__slug__already_exists" msgstr "This slug is already used by another assembly." -msgid "Assembly__tags__split_with_comma" -msgstr "Split multiple tags by comma." - msgid "username" msgstr "" @@ -434,6 +431,9 @@ msgstr "opens in a new tab or window" msgid "Assembly__edit__children" msgstr "Grouped Assemblies" +msgid "Assembly__tags__split_with_comma" +msgstr "Split multiple tags by comma." + msgid "Assembly__edit__submit_btn" msgstr "Save" diff --git a/src/backoffice/views/assemblies/assemblies.py b/src/backoffice/views/assemblies/assemblies.py index 9269bf662e1a45dca3c32dd2a67fd14c6172e21e..daf9e1124ca5dbb707c087bb9e9fdac2dce190b6 100644 --- a/src/backoffice/views/assemblies/assemblies.py +++ b/src/backoffice/views/assemblies/assemblies.py @@ -255,7 +255,7 @@ class AssemblyUpdateView(AssemblyMixin, UpdateView): # update tags: go through supplied list of tags given_tags = form.cleaned_data.get('tags').split(',') for raw_tag in given_tags: - tag = raw_tag.strip() # type: str + tag = raw_tag.strip().lower() # type: str if len(tag) == 0: # skip empty ones continue diff --git a/src/core/forms/projects.py b/src/core/forms/projects.py index 7f07da402553d2645445acd4d68e1a1413613934..264a69b4f2e125fe2a30a6366ceac2a946d51e74 100644 --- a/src/core/forms/projects.py +++ b/src/core/forms/projects.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from core.base_forms import TranslatedFieldsForm from core.models import Assembly, PlatformUser, Project -from core.models.tags import clean_tags +from core.utils import tags_from_string class ProjectForm(TranslatedFieldsForm): @@ -66,7 +66,7 @@ class ProjectForm(TranslatedFieldsForm): return self.cleaned_data['name'] def clean_tags_list(self): - return clean_tags(self.cleaned_data['tags_list']) + return tags_from_string(self.cleaned_data['tags_list']) def clean(self) -> dict[str, Any]: if not self.create: diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index 4649f5476a62c3e009274e7d2cc6ca218af080c5..fc92843e22a39728c411c3cd6f234bfe1a12016b 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -1969,9 +1969,6 @@ msgstr "erklärender Begleittext des Tags" msgid "ConferenceTag__description" msgstr "Beschreibung" -msgid "Tags__split_with_comma" -msgstr "Tags müssen mit Komma getrennt werden." - msgid "ConferenceMemberTicket__token_wrong_conference" msgstr "Das Ticket ist nicht für diese Konferenz." @@ -2469,6 +2466,9 @@ msgstr "Aktiviere dein %(safe_site_name)s Konto" msgid "Conference-day" msgstr "Tag" +msgid "Tags__split_with_comma" +msgstr "Tags müssen mit Komma getrennt werden." + #, python-format msgid "Validation__error_file_size_MB_exceeded %(max_size)d" msgstr "Die Datei darf nicht größer als %(max_size)dMb sein" diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index 794b6c1f7994a644428520491930c3322459fa71..dfb63b7f47bd35718e8786de74692374e07c9742 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -1967,9 +1967,6 @@ msgstr "additional explanation of this tag" msgid "ConferenceTag__description" msgstr "description" -msgid "Tags__split_with_comma" -msgstr "tags must be split with comma" - msgid "ConferenceMemberTicket__token_wrong_conference" msgstr "This ticket is for another conference." @@ -2451,6 +2448,9 @@ msgstr "" msgid "Conference-day" msgstr "day" +msgid "Tags__split_with_comma" +msgstr "tags must be split with comma" + #, python-format msgid "Validation__error_file_size_MB_exceeded %(max_size)d" msgstr "You aren't allowed to upload a file larger than %(max_size)dMb" diff --git a/src/core/models/tags.py b/src/core/models/tags.py index 630e5512f6180a189962b010cd718fe48ee904f4..82dfa11cee0af553b8efe4d2bebb92d7f812bf68 100644 --- a/src/core/models/tags.py +++ b/src/core/models/tags.py @@ -3,7 +3,6 @@ import contextlib from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError -from django.core.validators import validate_slug from django.db import models from django.db.models import QuerySet from django.utils.functional import cached_property @@ -193,20 +192,3 @@ class TaggedItemMixin: # TODO: check user's permission with contextlib.suppress(TagItem.DoesNotExist): self.tags.remove(self.tags.get(tag=tag)) - - -def clean_tags(tags: str): - # try to detect people delimiting tags with space instead of comma - - # allow empty tags - if tags.strip() == '': - return '' - - split_tags = tags.split(',') - if tags is not None and len(split_tags) == 1 and len(tags.split(' ')) > 2: - raise ValidationError(_('Tags__split_with_comma')) - stripped_tags = [tag.strip() for tag in split_tags] - for tag in stripped_tags: - validate_slug(tag) - - return stripped_tags diff --git a/src/core/utils.py b/src/core/utils.py index c084a7b45eec011da92cf4228d05c4c86e7714fe..3ec4c560b60f602ff686d5680be68db9a6368dcd 100644 --- a/src/core/utils.py +++ b/src/core/utils.py @@ -12,10 +12,13 @@ from urllib.parse import parse_qs, urlparse, urlunparse import requests +from django.core.exceptions import ValidationError from django.core.files.base import ContentFile +from django.core.validators import validate_slug from django.urls import NoReverseMatch from django.utils.functional import cached_property from django.utils.html import strip_tags +from django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) @@ -228,6 +231,22 @@ def mail2uuid(mail: str, prefix: str = 'acct:', suffix: str = '') -> uuid.UUID: return uuid.uuid5(uuid.NAMESPACE_URL, uri) +def tags_from_string(value: str) -> list[str]: + # allow empty tags + if value.strip() == '': + return [] + + split_tags = value.split(',') + if value is not None and len(split_tags) == 1 and len(value.split(' ')) > 2: + raise ValidationError(_('Tags__split_with_comma')) + + tags = [tag.strip().lower() for tag in split_tags] + for tag in tags: + validate_slug(tag) + + return tags + + class GitCloneError(Exception): pass diff --git a/src/plainui/views/general.py b/src/plainui/views/general.py index bc3f1074e19ccc541411782fffbddfc26074fe79..bbe2e23d79419c7a46e565cbb1c539850390447d 100644 --- a/src/plainui/views/general.py +++ b/src/plainui/views/general.py @@ -11,7 +11,7 @@ __all__ = ( from datetime import timedelta from django.contrib.contenttypes.models import ContentType -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_list_or_404, redirect from django.urls import reverse from django.utils import timezone from django.utils.timezone import now @@ -122,24 +122,25 @@ class TagView(ConferenceRequiredMixin, TemplateView): context = super().get_context_data(tag_slug=tag_slug, **kwargs) context['conf'] = self.conf - tag = get_object_or_404(ConferenceTag, slug=tag_slug) - context['tag'] = tag + # workaround: there can be multiple tags with different casing + tags = get_list_or_404(ConferenceTag, slug__iexact=tag_slug) + context['tag'] = tags[0] # TODO other types. What should we link here? # TODO: consider using views.utils.event_filter() here context['events'] = ( Event.objects.conference_accessible(self.conf) - .filter(id__in=TagItem.objects.filter(tag=tag, target_type=ContentType.objects.get_for_model(Event)).values_list('target_id')) + .filter(id__in=TagItem.objects.filter(tag__in=tags, target_type=ContentType.objects.get_for_model(Event)).values_list('target_id')) .filter(schedule_start__isnull=False, schedule_end__isnull=False) .order_by('schedule_start', 'schedule_end') ) context['my_favorite_events'] = session_get_favorite_events(self.request.session, self.request.user) context['assemblies'] = Assembly.objects.conference_accessible(self.conf).filter( - id__in=TagItem.objects.filter(tag=tag, target_type=ContentType.objects.get_for_model(Assembly)).values_list('target_id') + id__in=TagItem.objects.filter(tag__in=tags, target_type=ContentType.objects.get_for_model(Assembly)).values_list('target_id') ) context['my_favorite_assemblies'] = session_get_favorite_assemblies(self.request.session, self.request.user) - context['projects'] = Project.objects.conference_accessible(self.conf).filter(tags__tag=tag) + context['projects'] = Project.objects.conference_accessible(self.conf).filter(tags__tag__in=tags) context['my_favorite_projects'] = session_get_favorite_projects(self.request.session, self.request.user) return context