2025-02-19 16:30:25 +01:00

877 lines
29 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.forms import (
ModelForm,
ValidationError,
TextInput,
Form,
URLField,
MultipleHiddenInput,
Textarea,
CharField,
ChoiceField,
RadioSelect,
MultipleChoiceField,
BooleanField,
HiddenInput,
ModelChoiceField,
EmailField
)
from django.forms import formset_factory
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
from .utils import PlaceGuesser
from .models import (
Event,
RecurrentImport,
CategorisationRule,
Place,
Category,
Tag,
Message
)
from django.conf import settings
from django.core.files import File
from django.utils.translation import gettext_lazy as _
from string import ascii_uppercase as auc
from .templatetags.utils_extra import int_to_abc
from django.utils.safestring import mark_safe
from django.utils.formats import localize
from .templatetags.event_extra import event_field_verbose_name, field_to_html
import os
import logging
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 == 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 not g.toggle_field_name() 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", "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 not name is 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"] == 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 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):
is_authenticated = 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 EventForm(GroupFormMixin, ModelForm):
required_css_class = 'required'
old_local_image = CharField(widget=HiddenInput(), required=False)
simple_cloning = CharField(widget=HiddenInput(), required=False)
cloning = CharField(widget=HiddenInput(), required=False)
tags = MultipleChoiceField(
label=_("Tags"),
initial=None,
choices=[],
required=False
)
class Meta:
model = Event
exclude = [
"imported_date",
"modified_date",
"moderated_date",
"import_sources",
"image",
"moderated_by_user",
"modified_by_user",
"created_by_user",
"imported_by_user"
]
widgets = {
"start_day": TextInput(
attrs={
"type": "date",
"onchange": "update_datetimes(event);",
"onfocus": "this.oldvalue = this.value;",
}
),
"start_time": TextInput(
attrs={
"type": "time",
"onchange": "update_datetimes(event);",
"onfocus": "this.oldvalue = this.value;",
}
),
"end_day": TextInput(attrs={"type": "date"}),
"end_time": TextInput(attrs={"type": "time"}),
"other_versions": HiddenInput(),
"uuids": MultipleHiddenInput(),
"reference_urls": DynamicArrayWidgetURLs(),
}
def __init__(self, *args, **kwargs):
is_authenticated = kwargs.pop("is_authenticated", False)
self.cloning = kwargs.pop("is_cloning", False)
self.simple_cloning = kwargs.pop("is_simple_cloning", False)
super().__init__(*args, **kwargs)
if not is_authenticated:
del self.fields["status"]
del self.fields["organisers"]
self.fields['category'].queryset = self.fields['category'].queryset.order_by('name')
self.fields['category'].empty_label = None
self.fields['category'].initial = Category.get_default_category()
self.fields['tags'].choices = Tag.get_tag_groups(all=True)
# set groups
self.add_group('main', _('Main fields'))
self.fields['title'].group_id = 'main'
self.add_group('start', _('Start of event'))
self.fields['start_day'].group_id = 'start'
self.fields['start_time'].group_id = 'start'
self.add_group('end', _('End of event'))
self.fields['end_day'].group_id = 'end'
self.fields['end_time'].group_id = 'end'
self.add_group('recurrences',
_('This is a recurring event'),
maskable=True,
default_masked=not (self.instance and
self.instance.recurrences and
self.instance.recurrences.rrules and
len(self.instance.recurrences.rrules) > 0))
self.fields['recurrences'].group_id = 'recurrences'
self.add_group('details', _('Details'))
self.fields['description'].group_id = 'details'
if is_authenticated:
self.fields['organisers'].group_id = 'details'
self.add_group('location', _('Location'))
self.fields['location'].group_id = 'location'
self.fields['exact_location'].group_id = 'location'
self.add_group('illustration', _('Illustration'))
self.fields['local_image'].group_id = 'illustration'
self.fields['image_alt'].group_id = 'illustration'
self.add_group('urls', _('URLs'))
self.fields["reference_urls"].group_id = 'urls'
if is_authenticated:
self.add_group('meta-admin', _('Meta information'))
self.fields['category'].group_id = 'meta-admin'
self.fields['tags'].group_id = 'meta-admin'
self.fields['status'].group_id = 'meta-admin'
else:
self.add_group('meta', _('Meta information'))
self.fields['category'].group_id = 'meta'
self.fields['tags'].group_id = 'meta'
def is_clone_from_url(self):
return self.cloning
def is_simple_clone_from_url(self):
return self.simple_cloning
def clean_end_day(self):
start_day = self.cleaned_data.get("start_day")
end_day = self.cleaned_data.get("end_day")
if end_day is not None and start_day is not None and end_day < start_day:
raise ValidationError(_("The end date must be after the start date."))
return end_day
def clean_end_time(self):
start_day = self.cleaned_data.get("start_day")
end_day = self.cleaned_data.get("end_day")
start_time = self.cleaned_data.get("start_time")
end_time = self.cleaned_data.get("end_time")
# same day
if start_day is not None and (end_day is None or start_day == end_day):
# both start and end time are defined
if start_time is not None and end_time is not None:
if start_time > end_time:
raise ValidationError(
_("The end time cannot be earlier than the start time.")
)
return end_time
def clean(self):
super().clean()
# when cloning an existing event, we need to copy the local image
if ((not 'local_image' in self.cleaned_data) or (self.cleaned_data['local_image'] is None)) and \
not self.cleaned_data['old_local_image'] is None and \
self.cleaned_data['old_local_image'] != "":
basename = self.cleaned_data['old_local_image']
old = settings.MEDIA_ROOT + "/" + basename
if os.path.isfile(old):
self.cleaned_data['local_image'] = File(name=basename, file=open(old, "rb"))
class EventFormWithContact(SimpleContactForm, EventForm):
pass
class MultipleChoiceFieldAcceptAll(MultipleChoiceField):
def validate(self, value):
pass
class EventModerateForm(ModelForm):
required_css_class = 'required'
tags = MultipleChoiceField(
label=_("Tags"),
help_text=_('Select tags from existing ones.'),
required=False
)
new_tags = MultipleChoiceFieldAcceptAll(
label=_("New tags"),
help_text=_('Create new labels (sparingly). Note: by starting your tag with the characters “TW:”, you''ll create a “trigger warning” tag, and the associated events will be announced as such.'),
widget=DynamicArrayWidget(),
required=False
)
class Meta:
model = Event
fields = [
"status",
"category",
"organisers",
"exact_location",
"tags"
]
widgets = {
"status": RadioSelect
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.fields['category'].queryset.order_by('name')
self.fields['category'].empty_label = None
self.fields['category'].initial = Category.get_default_category()
self.fields['tags'].choices = Tag.get_tag_groups(all=True)
def clean_new_tags(self):
return list(set(self.cleaned_data.get("new_tags")))
def clean(self):
super().clean()
if self.cleaned_data['tags'] is None:
self.cleaned_data['tags'] = []
if not self.cleaned_data.get('new_tags') is None:
self.cleaned_data['tags'] += self.cleaned_data.get('new_tags')
self.cleaned_data['tags'] = list(set(self.cleaned_data['tags']))
class BatchImportationForm(Form):
required_css_class = 'required'
json = CharField(
label="JSON",
widget=Textarea(attrs={"rows": "10"}),
help_text=_("JSON in the format expected for the import."),
required=True,
)
class FixDuplicates(Form):
required_css_class = 'required'
action = ChoiceField()
def __init__(self, *args, **kwargs):
edup = kwargs.pop("edup", None)
events = edup.get_duplicated()
nb_events = 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'
event = ChoiceField(label=_('Event'))
def __init__(self, *args, **kwargs):
events = kwargs.pop("events", None)
super().__init__(*args, **kwargs)
self.fields["event"].choices = [
(e.pk, (e.start_time.strftime('%H:%M') + " : " if e.start_time else "") + e.title + ((", " + e.location) if e.location else "")) for e in events
]
class MergeDuplicates(Form):
required_css_class = 'required'
checkboxes_fields = ["reference_urls", "description", "tags"]
def __init__(self, *args, **kwargs):
self.duplicates = kwargs.pop("duplicates", None)
self.event = kwargs.pop("event", None)
self.events = list(self.duplicates.get_duplicated())
nb_events = len(self.events)
super().__init__(*args, **kwargs)
if self.event:
choices = [
("event_" + str(e.pk), _("Value of version {}").format(e.pk)) if e != self.event else
("event_" + str(e.pk), _("Value of the selected version"))
for e in self.events
]
else:
choices = [
("event_" + str(e.pk), _("Value of version {}").format(e.pk)) for e in self.events
]
for f in self.duplicates.get_items_comparison():
if not f["similar"]:
if f["key"] in MergeDuplicates.checkboxes_fields:
self.fields[f["key"]] = MultipleChoiceField(choices=choices)
self.fields[f["key"]].initial = choices[0][0]
else:
self.fields[f["key"]] = ChoiceField(
widget=RadioSelect, choices=choices
)
self.fields[f["key"]].initial = choices[0][0]
def as_grid(self):
result = '<div class="grid">'
for i, e in enumerate(self.events):
result += '<div class="grid entete-badge">'
result += '<div class="badge-large">' + int_to_abc(i) + "</div>"
result += "<ul>"
result += (
'<li><a href="' + e.get_absolute_url() + '">' + e.title + "</a></li>"
)
for step in e.chronology_dates():
if step["data"] == "created_date":
result += '<li><em>Création</em> le ' + localize(step["timestamp"]) + ' par ' + str(step["user"]) + '</li>'
if step["data"] == "modified_date":
result += '<li><em>Dernière modification</em> le ' + localize(step["timestamp"])
if e.modified_by_user:
result += ' par ' + e.modified_by_user.username
else:
result += ' par import récurrent'
result += '</li>'
if step["data"] == "moderated_date":
result += '<li><em>Dernière modération</em> le ' + localize(step["timestamp"])
if e.moderated_by_user:
result += ' par ' + e.moderated_by_user.username
result += '</li>'
if step["data"] == "imported_date":
result += '<li><em>Dernière importation</em> le ' + localize(step["timestamp"])
if e.imported_by_user:
result += ' par ' + e.imported_by_user.username
else:
result += ' par import récurrent'
result += '</li>'
result += "</ul>"
result += "</div>"
result += "</div>"
for e in self.duplicates.get_items_comparison():
key = e["key"]
result += "<h3>" + event_field_verbose_name(e["key"]) + "</h3>"
if e["similar"]:
result += (
'<div class="comparison-item">Identique&nbsp;:'
+ str(field_to_html(e["values"], e["key"]))
+ "</div>"
)
else:
result += "<fieldset>"
if key in self.errors:
result += '<div class="message error"><ul>'
for err in self.errors[key]:
result += "<li>" + err + "</li>"
result += "</ul></div>"
result += '<div class="grid comparison-item">'
if hasattr(self, "cleaned_data"):
checked = self.cleaned_data.get(key)
else:
checked = self.fields[key].initial
i = 0
if self.event:
idx = self.events.index(self.event)
result += self.comparison_item(key, i, e["values"][idx], self.fields[e["key"]].choices[idx], self.event, checked)
i += 1
for (v, radio, ev) in zip(e["values"], self.fields[e["key"]].choices, self.events):
if self.event is None or ev != self.event:
result += self.comparison_item(key, i, v, radio, ev, checked)
i += 1
result += "</div></fieldset>"
return mark_safe(result)
def comparison_item(self, key, i, v, radio, ev, checked):
result = '<div class="duplicated">'
id = "id_" + key + "_" + str(ev.pk)
value = "event_" + str(ev.pk)
result += '<input id="' + id + '" name="' + key + '"'
if key in MergeDuplicates.checkboxes_fields:
result += ' type="checkbox"'
if checked and value in checked:
result += " checked"
else:
result += ' type="radio"'
if checked == value:
result += " checked"
result += ' value="' + value + '"'
result += ">"
result += (
'<div class="badge-small">'
+ int_to_abc(i)
+ "</div>")
result += "<div>"
if key == "image":
result += str(field_to_html(ev.local_image, "local_image")) + "</div>"
result += "<div>Lien d'import&nbsp;: "
result += (str(field_to_html(v, key)) + "</div>")
result += "</div>"
return result
def get_selected_events(self, key):
value = self.cleaned_data.get(key)
if key not in self.fields:
return None
else:
if isinstance(value, list):
selected = [int(v.split("_")[-1]) for v in value]
result = []
for s in selected:
for e in self.duplicates.get_duplicated():
if e.pk == s:
result.append(e)
break
return result
else:
selected = int(value.split("_")[-1])
for e in self.duplicates.get_duplicated():
if e.pk == selected:
return e
return None
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)
]
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")
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 = ('<div class="grid"><div>'
+ super().as_p()
+ '''</div><div><div class="map-widget">
<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div>
<p>Cliquez pour ajuster la position GPS</p></div>
<input type="checkbox" role="switch" id="lock_position">Verrouiller la position</lock>
<script>
document.getElementById("lock_position").onclick = function() {
const field = document.getElementById("id_location");
if (this.checked)
field.setAttribute("readonly", true);
else
field.removeAttribute("readonly");
}
</script>
</div></div>''')
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")