From 2afafc1338f68c39d596a16eb2a230a3be676bb9 Mon Sep 17 00:00:00 2001 From: SebF Date: Wed, 7 May 2025 10:49:03 +0200 Subject: [PATCH] =?UTF-8?q?r=C3=A9organisation=20des=20forms=20en=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/forms/__init__.py | 9 + src/agenda_culturel/forms/category.py | 57 ++ .../{forms.py => forms/event.py} | 643 +++--------------- src/agenda_culturel/forms/imports.py | 61 ++ src/agenda_culturel/forms/message.py | 35 + src/agenda_culturel/forms/place.py | 116 ++++ src/agenda_culturel/forms/special_period.py | 32 + src/agenda_culturel/forms/tag.py | 56 ++ src/agenda_culturel/forms/user.py | 18 + src/agenda_culturel/forms/utils.py | 182 +++++ 10 files changed, 657 insertions(+), 552 deletions(-) create mode 100644 src/agenda_culturel/forms/__init__.py create mode 100644 src/agenda_culturel/forms/category.py rename src/agenda_culturel/{forms.py => forms/event.py} (60%) create mode 100644 src/agenda_culturel/forms/imports.py create mode 100644 src/agenda_culturel/forms/message.py create mode 100644 src/agenda_culturel/forms/place.py create mode 100644 src/agenda_culturel/forms/special_period.py create mode 100644 src/agenda_culturel/forms/tag.py create mode 100644 src/agenda_culturel/forms/user.py create mode 100644 src/agenda_culturel/forms/utils.py diff --git a/src/agenda_culturel/forms/__init__.py b/src/agenda_culturel/forms/__init__.py new file mode 100644 index 0000000..15fa6e6 --- /dev/null +++ b/src/agenda_culturel/forms/__init__.py @@ -0,0 +1,9 @@ +from .utils import * +from .user import * +from .category import * +from .tag import * +from .event import * +from .place import * +from .message import * +from .imports import * +from .special_period import * diff --git a/src/agenda_culturel/forms/category.py b/src/agenda_culturel/forms/category.py new file mode 100644 index 0000000..a5203e8 --- /dev/null +++ b/src/agenda_culturel/forms/category.py @@ -0,0 +1,57 @@ +import logging + +from django.forms import ( + BooleanField, + CharField, + Form, + HiddenInput, + ModelForm, +) +from django.utils.translation import gettext_lazy as _ + +from ..models import ( + CategorisationRule, + Event, +) + +logger = logging.getLogger(__name__) + + +class CategorisationRuleImportForm(ModelForm): + required_css_class = "required" + + class Meta: + model = CategorisationRule + fields = "__all__" + + +class CategorisationForm(Form): + required_css_class = "required" + + def __init__(self, *args, **kwargs): + if "events" in kwargs: + events = kwargs.pop("events", None) + else: + events = [] + for f in args[0]: + if "_" not in f: + if f + "_cat" in args[0]: + events.append( + (Event.objects.get(pk=int(f)), args[0][f + "_cat"]) + ) + super().__init__(*args, **kwargs) + + for e, c in events: + self.fields[str(e.pk)] = BooleanField( + initial=False, + label=_("Apply category {} to the event {}").format(c, e.title), + required=False, + ) + self.fields[str(e.pk) + "_cat"] = CharField(initial=c, widget=HiddenInput()) + + def get_validated(self): + return [ + (e, self.cleaned_data.get(e + "_cat")) + for e in self.fields + if "_" not in e and self.cleaned_data.get(e) + ] diff --git a/src/agenda_culturel/forms.py b/src/agenda_culturel/forms/event.py similarity index 60% rename from src/agenda_culturel/forms.py rename to src/agenda_culturel/forms/event.py index 7cc9574..b3d6cf5 100644 --- a/src/agenda_culturel/forms.py +++ b/src/agenda_culturel/forms/event.py @@ -1,285 +1,43 @@ -import logging import os from string import ascii_uppercase as auc from django.conf import settings from django.core.files import File from django.forms import ( - BooleanField, CharField, ChoiceField, - EmailField, Form, HiddenInput, - ModelChoiceField, ModelForm, MultipleChoiceField, MultipleHiddenInput, RadioSelect, - Textarea, TextInput, - URLField, ValidationError, - FileField, - formset_factory, ) from django.utils.formats import localize from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget -from .models import ( - CategorisationRule, +from . import ( + GroupFormMixin, + MultipleChoiceFieldAcceptAll, + DynamicArrayWidgetTags, + DynamicArrayWidgetURLs, + SimpleContactForm, +) +from ..models import ( Category, Event, - Message, - Place, - RecurrentImport, Tag, - UserProfile, - SpecialPeriod, ) -from .templatetags.event_extra import event_field_verbose_name, field_to_html -from .templatetags.utils_extra import int_to_abc - -from .models.constants import ( +from ..models.constants import ( TITLE_ISSUE_DATE_IMPORTATION, TITLE_ISSUE_TIME_IMPORTATION, PUBLICATION_ISSUE, ) - -logger = logging.getLogger(__name__) - - -class GroupFormMixin: - template_name = "agenda_culturel/forms/div_group.html" - - class FieldGroup: - def __init__( - self, - id, - label, - display_label=False, - maskable=False, - default_masked=True, - ): - self.id = id - self.label = label - self.display_label = display_label - self.maskable = maskable - self.default_masked = default_masked - - def toggle_field_name(self): - return "group_" + self.id - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.groups = [] - - def add_group(self, *args, **kwargs): - self.groups.append(GroupFormMixin.FieldGroup(*args, **kwargs)) - if self.groups[-1].maskable: - self.fields[self.groups[-1].toggle_field_name()] = BooleanField( - required=False - ) - self.fields[self.groups[-1].toggle_field_name()].toggle_group = True - - def get_fields_in_group(self, g): - return [ - f - for f in self.visible_fields() - if not hasattr(f.field, "toggle_group") - and hasattr(f.field, "group_id") - and f.field.group_id == g.id - ] - - def get_no_group_fields(self): - return [ - f - for f in self.visible_fields() - if not hasattr(f.field, "toggle_group") - and (not hasattr(f.field, "group_id") or f.field.group_id is None) - ] - - def fields_by_group(self): - return [(g, self.get_fields_in_group(g)) for g in self.groups] + [ - ( - GroupFormMixin.FieldGroup("other", _("Other")), - self.get_no_group_fields(), - ) - ] - - def clean(self): - result = super().clean() - - if result: - data = dict(self.data) - # for each masked group, we remove data - for g in self.groups: - if g.maskable and g.toggle_field_name() not in data: - fields = self.get_fields_in_group(g) - for f in fields: - self.cleaned_data[f.name] = None - - return result - - -class TagForm(ModelForm): - required_css_class = "required" - - class Meta: - model = Tag - fields = [ - "name", - "description", - "message", - "in_included_suggestions", - "in_excluded_suggestions", - "principal", - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if "name" in kwargs["initial"]: - self.fields["name"].widget = HiddenInput() - - -class TagRenameForm(Form): - required_css_class = "required" - - name = CharField(label=_("Name of new tag"), required=True) - - force = BooleanField( - label=_( - "Force renaming despite the existence of events already using the chosen tag." - ), - ) - - def __init__(self, *args, **kwargs): - force = kwargs.pop("force", False) - name = kwargs.pop("name", None) - super().__init__(*args, **kwargs) - if not (force or (not len(args) == 0 and "force" in args[0])): - del self.fields["force"] - if name is not None and self.fields["name"].initial is None: - self.fields["name"].initial = name - - def is_force(self): - return "force" in self.fields and self.cleaned_data["force"] is True - - -class SimpleContactForm(GroupFormMixin, Form): - email = EmailField( - label=_("Your email"), - help_text=_("Your email address"), - max_length=254, - required=False, - ) - - comments = CharField( - label=_("Comments"), - help_text=_( - "Your message for the moderation team (comments, clarifications, requests...)" - ), - widget=Textarea, - max_length=2048, - required=False, - ) - - def __init__(self, *args, **kwargs): - is_authenticated = "is_authenticated" in kwargs and kwargs["is_authenticated"] - super().__init__(*args, **kwargs) - - if not is_authenticated: - self.add_group( - "communication", - _( - "Receive notification of publication or leave a message for moderation" - ), - maskable=True, - default_masked=True, - ) - self.fields["email"].group_id = "communication" - self.fields["comments"].group_id = "communication" - else: - del self.fields["email"] - del self.fields["comments"] - - -class URLSubmissionSimpleForm(Form): - url = URLField(max_length=512) - - -class URLSubmissionForm(GroupFormMixin, Form): - required_css_class = "required" - - url = URLField(max_length=512) - category = ModelChoiceField( - label=_("Category"), - queryset=Category.objects.all().order_by("name"), - initial=None, - required=False, - ) - tags = MultipleChoiceField( - label=_("Tags"), initial=None, choices=[], required=False - ) - - def __init__(self, *args, **kwargs): - kwargs.pop("is_authenticated", False) - super().__init__(*args, **kwargs) - self.fields["tags"].choices = Tag.get_tag_groups(all=True) - - self.add_group("event", _("Event")) - self.fields["url"].group_id = "event" - self.fields["category"].group_id = "event" - self.fields["tags"].group_id = "event" - - -class URLSubmissionFormWithContact(SimpleContactForm, URLSubmissionForm): - pass - - -URLSubmissionFormSet = formset_factory(URLSubmissionForm, extra=9, min_num=1) - - -class DynamicArrayWidgetURLs(DynamicArrayWidget): - template_name = "agenda_culturel/widgets/widget-urls.html" - - -class DynamicArrayWidgetTags(DynamicArrayWidget): - template_name = "agenda_culturel/widgets/widget-tags.html" - - -class RecurrentImportForm(ModelForm): - required_css_class = "required" - - defaultTags = MultipleChoiceField( - label=_("Tags"), initial=None, choices=[], required=False - ) - - class Meta: - model = RecurrentImport - fields = "__all__" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["defaultTags"].choices = Tag.get_tag_groups(all=True) - - -class CategorisationRuleImportForm(ModelForm): - required_css_class = "required" - - class Meta: - model = CategorisationRule - fields = "__all__" - - -class MultipleChoiceFieldAcceptAll(MultipleChoiceField): - def validate(self, value): - # check if each element is without "/" - for item in value: - if "/" in item: - raise ValidationError(_("The '/' character is not allowed.")) +from ..templatetags.event_extra import event_field_verbose_name, field_to_html +from ..templatetags.utils_extra import int_to_abc class EventForm(GroupFormMixin, ModelForm): @@ -570,131 +328,6 @@ class EventModerateForm(ModelForm): self.cleaned_data["tags"] = list(set(self.cleaned_data["tags"])) -class BatchImportationForm(Form): - required_css_class = "required" - - data = CharField( - label=_("Data"), - widget=Textarea(attrs={"rows": "10"}), - help_text=_("Supported formats: json, html."), - required=True, - ) - - category = ModelChoiceField( - label=_("Category"), - queryset=Category.objects.all().order_by("name"), - help_text=_("Used only if data is html."), - initial=None, - required=False, - ) - tags = MultipleChoiceField( - label=_("Tags"), - initial=None, - choices=[], - help_text=_("Used only if data is html."), - required=False, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["tags"].choices = Tag.get_tag_groups(all=True) - - -class FixDuplicates(Form): - required_css_class = "required" - - action = ChoiceField() - - def __init__(self, *args, **kwargs): - edup = kwargs.pop("edup", None) - events = edup.get_duplicated() - len(events) - super().__init__(*args, **kwargs) - - choices = [] - initial = None - for i, e in enumerate(events): - if e.status != Event.STATUS.TRASH or e.modified(): - msg = "" - if e.local_version(): - msg = _(" (locally modified version)") - if e.status != Event.STATUS.TRASH: - initial = "Select-" + str(e.pk) - if e.pure_import(): - msg = _(" (synchronized on import version)") - choices += [ - ( - "Select-" + str(e.pk), - _("Select {} as representative version.").format(auc[i] + msg), - ) - ] - - for i, e in enumerate(events): - if e.status != Event.STATUS.TRASH and e.local_version(): - choices += [ - ( - "Update-" + str(e.pk), - _( - "Update {} using some fields from other versions (interactive mode)." - ).format(auc[i]), - ) - ] - - extra = "" - if edup.has_local_version(): - extra = _(" Warning: a version is already locally modified.") - - if initial is None: - initial = "Merge" - choices += [ - ( - "Merge", - _("Create a new version by merging (interactive mode).") + extra, - ) - ] - for i, e in enumerate(events): - if e.status != Event.STATUS.TRASH: - choices += [ - ( - "Remove-" + str(e.pk), - _("Make {} independent.").format(auc[i]), - ) - ] - choices += [("NotDuplicates", _("Make all versions independent."))] - - self.fields["action"].choices = choices - self.fields["action"].initial = initial - - def is_action_no_duplicates(self): - return self.cleaned_data["action"] == "NotDuplicates" - - def is_action_select(self): - return self.cleaned_data["action"].startswith("Select") - - def is_action_update(self): - return self.cleaned_data["action"].startswith("Update") - - def is_action_remove(self): - return self.cleaned_data["action"].startswith("Remove") - - def get_selected_event_code(self): - if ( - self.is_action_select() - or self.is_action_remove() - or self.is_action_update() - ): - return int(self.cleaned_data["action"].split("-")[-1]) - else: - return None - - def get_selected_event(self, edup): - selected = self.get_selected_event_code() - for e in edup.get_duplicated(): - if e.pk == selected: - return e - return None - - class SelectEventInList(Form): required_css_class = "required" @@ -888,190 +521,96 @@ class MergeDuplicates(Form): return None -class CategorisationForm(Form): +class FixDuplicates(Form): required_css_class = "required" + action = ChoiceField() + def __init__(self, *args, **kwargs): - if "events" in kwargs: - events = kwargs.pop("events", None) - else: - events = [] - for f in args[0]: - if "_" not in f: - if f + "_cat" in args[0]: - events.append( - (Event.objects.get(pk=int(f)), args[0][f + "_cat"]) - ) + edup = kwargs.pop("edup", None) + events = edup.get_duplicated() + len(events) super().__init__(*args, **kwargs) - for e, c in events: - self.fields[str(e.pk)] = BooleanField( - initial=False, - label=_("Apply category {} to the event {}").format(c, e.title), - required=False, + choices = [] + initial = None + for i, e in enumerate(events): + if e.status != Event.STATUS.TRASH or e.modified(): + msg = "" + if e.local_version(): + msg = _(" (locally modified version)") + if e.status != Event.STATUS.TRASH: + initial = "Select-" + str(e.pk) + if e.pure_import(): + msg = _(" (synchronized on import version)") + choices += [ + ( + "Select-" + str(e.pk), + _("Select {} as representative version.").format(auc[i] + msg), + ) + ] + + for i, e in enumerate(events): + if e.status != Event.STATUS.TRASH and e.local_version(): + choices += [ + ( + "Update-" + str(e.pk), + _( + "Update {} using some fields from other versions (interactive mode)." + ).format(auc[i]), + ) + ] + + extra = "" + if edup.has_local_version(): + extra = _(" Warning: a version is already locally modified.") + + if initial is None: + initial = "Merge" + choices += [ + ( + "Merge", + _("Create a new version by merging (interactive mode).") + extra, ) - self.fields[str(e.pk) + "_cat"] = CharField(initial=c, widget=HiddenInput()) - - def get_validated(self): - return [ - (e, self.cleaned_data.get(e + "_cat")) - for e in self.fields - if "_" not in e and self.cleaned_data.get(e) ] + for i, e in enumerate(events): + if e.status != Event.STATUS.TRASH: + choices += [ + ( + "Remove-" + str(e.pk), + _("Make {} independent.").format(auc[i]), + ) + ] + choices += [("NotDuplicates", _("Make all versions independent."))] + self.fields["action"].choices = choices + self.fields["action"].initial = initial -class EventAddPlaceForm(Form): - required_css_class = "required" + def is_action_no_duplicates(self): + return self.cleaned_data["action"] == "NotDuplicates" - place = ModelChoiceField( - label=_("Place"), - queryset=Place.objects.all().order_by("name"), - empty_label=_("Create a missing place"), - required=False, - ) - add_alias = BooleanField(initial=True, required=False) + def is_action_select(self): + return self.cleaned_data["action"].startswith("Select") - def __init__(self, *args, **kwargs): - self.instance = kwargs.pop("instance", False) - super().__init__(*args, **kwargs) - if self.instance.location: - self.fields["add_alias"].label = _( - 'Add "{}" to the aliases of the place' - ).format(self.instance.location) + def is_action_update(self): + return self.cleaned_data["action"].startswith("Update") + + def is_action_remove(self): + return self.cleaned_data["action"].startswith("Remove") + + def get_selected_event_code(self): + if ( + self.is_action_select() + or self.is_action_remove() + or self.is_action_update() + ): + return int(self.cleaned_data["action"].split("-")[-1]) else: - self.fields.pop("add_alias") - if self.instance.exact_location: - self.fields["place"].initial = self.instance.exact_location - self.fields["add_alias"].initial = False + return None - def modified_event(self): - return self.cleaned_data.get("place") - - def save(self): - if self.cleaned_data.get("place"): - place = self.cleaned_data.get("place") - self.instance.exact_location = place - self.instance.save(update_fields=["exact_location"]) - if self.cleaned_data.get("add_alias"): - if place.aliases: - place.aliases.append(self.instance.location.strip()) - else: - place.aliases = [self.instance.location.strip()] - place.save() - - return self.instance - - -class PlaceForm(GroupFormMixin, ModelForm): - required_css_class = "required" - - apply_to_all = BooleanField( - initial=True, - label=_( - "On saving, use aliases to detect all matching events with missing place" - ), - required=False, - ) - - class Meta: - model = Place - fields = "__all__" - widgets = {"location": TextInput()} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.add_group("header", _("Header")) - self.fields["name"].group_id = "header" - - self.add_group("address", _("Address")) - self.fields["address"].group_id = "address" - self.fields["postcode"].group_id = "address" - self.fields["city"].group_id = "address" - self.fields["location"].group_id = "address" - - self.add_group("meta", _("Meta")) - self.fields["aliases"].group_id = "meta" - - self.add_group("information", _("Information")) - self.fields["description"].group_id = "information" - - def as_grid(self): - result = ( - '
' - + super().as_p() - + """
-
-

Cliquez pour ajuster la position GPS

- Verrouiller la position - -
""" - ) - - return mark_safe(result) - - def apply(self): - return self.cleaned_data.get("apply_to_all") - - -class MessageForm(ModelForm): - class Meta: - model = Message - fields = ["subject", "name", "email", "message", "related_event"] - widgets = {"related_event": HiddenInput(), "user": HiddenInput()} - - def __init__(self, *args, **kwargs): - self.event = kwargs.pop("event", False) - self.internal = kwargs.pop("internal", False) - super().__init__(*args, **kwargs) - self.fields["related_event"].required = False - if self.internal: - self.fields.pop("name") - self.fields.pop("email") - - -class MessageEventForm(ModelForm): - class Meta: - model = Message - fields = ["message"] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["message"].label = _("Add a comment") - - -class UserProfileForm(ModelForm): - - user = CharField(widget=HiddenInput()) - - class Meta: - model = UserProfile - fields = "__all__" - - -class SpecialPeriodForm(ModelForm): - - class Meta: - model = SpecialPeriod - fields = "__all__" - widgets = { - "start_date": TextInput(attrs={"type": "date"}), - "end_date": TextInput(attrs={"type": "date"}), - } - - -class SpecialPeriodFileForm(Form): - periodtype = ChoiceField( - label=_("Period type"), - required=True, - choices=SpecialPeriod.PERIODTYPE.choices, - ) - file = FileField(label=_("ICAL file"), required=True) + def get_selected_event(self, edup): + selected = self.get_selected_event_code() + for e in edup.get_duplicated(): + if e.pk == selected: + return e + return None diff --git a/src/agenda_culturel/forms/imports.py b/src/agenda_culturel/forms/imports.py new file mode 100644 index 0000000..bb91bfc --- /dev/null +++ b/src/agenda_culturel/forms/imports.py @@ -0,0 +1,61 @@ +from django.forms import ( + CharField, + Form, + ModelChoiceField, + ModelForm, + MultipleChoiceField, + Textarea, +) +from django.utils.translation import gettext_lazy as _ + +from ..models import ( + Category, + RecurrentImport, + Tag, +) + + +class BatchImportationForm(Form): + required_css_class = "required" + + data = CharField( + label=_("Data"), + widget=Textarea(attrs={"rows": "10"}), + help_text=_("Supported formats: json, html."), + required=True, + ) + + category = ModelChoiceField( + label=_("Category"), + queryset=Category.objects.all().order_by("name"), + help_text=_("Used only if data is html."), + initial=None, + required=False, + ) + tags = MultipleChoiceField( + label=_("Tags"), + initial=None, + choices=[], + help_text=_("Used only if data is html."), + required=False, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["tags"].choices = Tag.get_tag_groups(all=True) + + +class RecurrentImportForm(ModelForm): + required_css_class = "required" + + defaultTags = MultipleChoiceField( + label=_("Tags"), initial=None, choices=[], required=False + ) + + class Meta: + model = RecurrentImport + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["defaultTags"].choices = Tag.get_tag_groups(all=True) diff --git a/src/agenda_culturel/forms/message.py b/src/agenda_culturel/forms/message.py new file mode 100644 index 0000000..054d465 --- /dev/null +++ b/src/agenda_culturel/forms/message.py @@ -0,0 +1,35 @@ +from django.forms import ( + HiddenInput, + ModelForm, +) +from django.utils.translation import gettext_lazy as _ + +from ..models import ( + Message, +) + + +class MessageForm(ModelForm): + class Meta: + model = Message + fields = ["subject", "name", "email", "message", "related_event"] + widgets = {"related_event": HiddenInput(), "user": HiddenInput()} + + def __init__(self, *args, **kwargs): + self.event = kwargs.pop("event", False) + self.internal = kwargs.pop("internal", False) + super().__init__(*args, **kwargs) + self.fields["related_event"].required = False + if self.internal: + self.fields.pop("name") + self.fields.pop("email") + + +class MessageEventForm(ModelForm): + class Meta: + model = Message + fields = ["message"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["message"].label = _("Add a comment") diff --git a/src/agenda_culturel/forms/place.py b/src/agenda_culturel/forms/place.py new file mode 100644 index 0000000..58d06c1 --- /dev/null +++ b/src/agenda_culturel/forms/place.py @@ -0,0 +1,116 @@ +from django.forms import ( + BooleanField, + Form, + ModelChoiceField, + ModelForm, + TextInput, +) +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ + +from . import GroupFormMixin +from ..models import ( + Place, +) + + +class EventAddPlaceForm(Form): + required_css_class = "required" + + place = ModelChoiceField( + label=_("Place"), + queryset=Place.objects.all().order_by("name"), + empty_label=_("Create a missing place"), + required=False, + ) + add_alias = BooleanField(initial=True, required=False) + + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop("instance", False) + super().__init__(*args, **kwargs) + if self.instance.location: + self.fields["add_alias"].label = _( + 'Add "{}" to the aliases of the place' + ).format(self.instance.location) + else: + self.fields.pop("add_alias") + if self.instance.exact_location: + self.fields["place"].initial = self.instance.exact_location + self.fields["add_alias"].initial = False + + def modified_event(self): + return self.cleaned_data.get("place") + + def save(self): + if self.cleaned_data.get("place"): + place = self.cleaned_data.get("place") + self.instance.exact_location = place + self.instance.save(update_fields=["exact_location"]) + if self.cleaned_data.get("add_alias"): + if place.aliases: + place.aliases.append(self.instance.location.strip()) + else: + place.aliases = [self.instance.location.strip()] + place.save() + + return self.instance + + +class PlaceForm(GroupFormMixin, ModelForm): + required_css_class = "required" + + apply_to_all = BooleanField( + initial=True, + label=_( + "On saving, use aliases to detect all matching events with missing place" + ), + required=False, + ) + + class Meta: + model = Place + fields = "__all__" + widgets = {"location": TextInput()} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.add_group("header", _("Header")) + self.fields["name"].group_id = "header" + + self.add_group("address", _("Address")) + self.fields["address"].group_id = "address" + self.fields["postcode"].group_id = "address" + self.fields["city"].group_id = "address" + self.fields["location"].group_id = "address" + + self.add_group("meta", _("Meta")) + self.fields["aliases"].group_id = "meta" + + self.add_group("information", _("Information")) + self.fields["description"].group_id = "information" + + def as_grid(self): + result = ( + '
' + + super().as_p() + + """
+
+

Cliquez pour ajuster la position GPS

+ Verrouiller la position + +
""" + ) + + return mark_safe(result) + + def apply(self): + return self.cleaned_data.get("apply_to_all") diff --git a/src/agenda_culturel/forms/special_period.py b/src/agenda_culturel/forms/special_period.py new file mode 100644 index 0000000..63d3b30 --- /dev/null +++ b/src/agenda_culturel/forms/special_period.py @@ -0,0 +1,32 @@ +from django.forms import ( + ChoiceField, + Form, + ModelForm, + TextInput, + FileField, +) +from django.utils.translation import gettext_lazy as _ + +from ..models import ( + SpecialPeriod, +) + + +class SpecialPeriodForm(ModelForm): + + class Meta: + model = SpecialPeriod + fields = "__all__" + widgets = { + "start_date": TextInput(attrs={"type": "date"}), + "end_date": TextInput(attrs={"type": "date"}), + } + + +class SpecialPeriodFileForm(Form): + periodtype = ChoiceField( + label=_("Period type"), + required=True, + choices=SpecialPeriod.PERIODTYPE.choices, + ) + file = FileField(label=_("ICAL file"), required=True) diff --git a/src/agenda_culturel/forms/tag.py b/src/agenda_culturel/forms/tag.py new file mode 100644 index 0000000..b76360b --- /dev/null +++ b/src/agenda_culturel/forms/tag.py @@ -0,0 +1,56 @@ +from django.forms import ( + BooleanField, + CharField, + Form, + HiddenInput, + ModelForm, +) +from django.utils.translation import gettext_lazy as _ + +from ..models import ( + Tag, +) + + +class TagForm(ModelForm): + required_css_class = "required" + + class Meta: + model = Tag + fields = [ + "name", + "description", + "message", + "in_included_suggestions", + "in_excluded_suggestions", + "principal", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if "name" in kwargs["initial"]: + self.fields["name"].widget = HiddenInput() + + +class TagRenameForm(Form): + required_css_class = "required" + + name = CharField(label=_("Name of new tag"), required=True) + + force = BooleanField( + label=_( + "Force renaming despite the existence of events already using the chosen tag." + ), + ) + + def __init__(self, *args, **kwargs): + force = kwargs.pop("force", False) + name = kwargs.pop("name", None) + super().__init__(*args, **kwargs) + if not (force or (not len(args) == 0 and "force" in args[0])): + del self.fields["force"] + if name is not None and self.fields["name"].initial is None: + self.fields["name"].initial = name + + def is_force(self): + return "force" in self.fields and self.cleaned_data["force"] is True diff --git a/src/agenda_culturel/forms/user.py b/src/agenda_culturel/forms/user.py new file mode 100644 index 0000000..3f72c62 --- /dev/null +++ b/src/agenda_culturel/forms/user.py @@ -0,0 +1,18 @@ +from django.forms import ( + CharField, + HiddenInput, + ModelForm, +) + +from ..models import ( + UserProfile, +) + + +class UserProfileForm(ModelForm): + + user = CharField(widget=HiddenInput()) + + class Meta: + model = UserProfile + fields = "__all__" diff --git a/src/agenda_culturel/forms/utils.py b/src/agenda_culturel/forms/utils.py new file mode 100644 index 0000000..628e971 --- /dev/null +++ b/src/agenda_culturel/forms/utils.py @@ -0,0 +1,182 @@ +from django.forms import ( + BooleanField, + CharField, + EmailField, + Form, + ModelChoiceField, + MultipleChoiceField, + Textarea, + URLField, + ValidationError, + formset_factory, +) +from django.utils.translation import gettext_lazy as _ +from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget + +from ..models import ( + Category, + Tag, +) + + +class GroupFormMixin: + template_name = "agenda_culturel/forms/div_group.html" + + class FieldGroup: + def __init__( + self, + id, + label, + display_label=False, + maskable=False, + default_masked=True, + ): + self.id = id + self.label = label + self.display_label = display_label + self.maskable = maskable + self.default_masked = default_masked + + def toggle_field_name(self): + return "group_" + self.id + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.groups = [] + + def add_group(self, *args, **kwargs): + self.groups.append(GroupFormMixin.FieldGroup(*args, **kwargs)) + if self.groups[-1].maskable: + self.fields[self.groups[-1].toggle_field_name()] = BooleanField( + required=False + ) + self.fields[self.groups[-1].toggle_field_name()].toggle_group = True + + def get_fields_in_group(self, g): + return [ + f + for f in self.visible_fields() + if not hasattr(f.field, "toggle_group") + and hasattr(f.field, "group_id") + and f.field.group_id == g.id + ] + + def get_no_group_fields(self): + return [ + f + for f in self.visible_fields() + if not hasattr(f.field, "toggle_group") + and (not hasattr(f.field, "group_id") or f.field.group_id is None) + ] + + def fields_by_group(self): + return [(g, self.get_fields_in_group(g)) for g in self.groups] + [ + ( + GroupFormMixin.FieldGroup("other", _("Other")), + self.get_no_group_fields(), + ) + ] + + def clean(self): + result = super().clean() + + if result: + data = dict(self.data) + # for each masked group, we remove data + for g in self.groups: + if g.maskable and g.toggle_field_name() not in data: + fields = self.get_fields_in_group(g) + for f in fields: + self.cleaned_data[f.name] = None + + return result + + +class SimpleContactForm(GroupFormMixin, Form): + email = EmailField( + label=_("Your email"), + help_text=_("Your email address"), + max_length=254, + required=False, + ) + + comments = CharField( + label=_("Comments"), + help_text=_( + "Your message for the moderation team (comments, clarifications, requests...)" + ), + widget=Textarea, + max_length=2048, + required=False, + ) + + def __init__(self, *args, **kwargs): + is_authenticated = "is_authenticated" in kwargs and kwargs["is_authenticated"] + super().__init__(*args, **kwargs) + + if not is_authenticated: + self.add_group( + "communication", + _( + "Receive notification of publication or leave a message for moderation" + ), + maskable=True, + default_masked=True, + ) + self.fields["email"].group_id = "communication" + self.fields["comments"].group_id = "communication" + else: + del self.fields["email"] + del self.fields["comments"] + + +class URLSubmissionSimpleForm(Form): + url = URLField(max_length=512) + + +class URLSubmissionForm(GroupFormMixin, Form): + required_css_class = "required" + + url = URLField(max_length=512) + category = ModelChoiceField( + label=_("Category"), + queryset=Category.objects.all().order_by("name"), + initial=None, + required=False, + ) + tags = MultipleChoiceField( + label=_("Tags"), initial=None, choices=[], required=False + ) + + def __init__(self, *args, **kwargs): + kwargs.pop("is_authenticated", False) + super().__init__(*args, **kwargs) + self.fields["tags"].choices = Tag.get_tag_groups(all=True) + + self.add_group("event", _("Event")) + self.fields["url"].group_id = "event" + self.fields["category"].group_id = "event" + self.fields["tags"].group_id = "event" + + +class URLSubmissionFormWithContact(SimpleContactForm, URLSubmissionForm): + pass + + +URLSubmissionFormSet = formset_factory(URLSubmissionForm, extra=9, min_num=1) + + +class DynamicArrayWidgetURLs(DynamicArrayWidget): + template_name = "agenda_culturel/widgets/widget-urls.html" + + +class DynamicArrayWidgetTags(DynamicArrayWidget): + template_name = "agenda_culturel/widgets/widget-tags.html" + + +class MultipleChoiceFieldAcceptAll(MultipleChoiceField): + def validate(self, value): + # check if each element is without "/" + for item in value: + if "/" in item: + raise ValidationError(_("The '/' character is not allowed."))