Amélioration UX

This commit is contained in:
Jean-Marie Favreau 2024-11-24 18:59:21 +01:00
parent a3e13429eb
commit 3001685937
7 changed files with 523 additions and 357 deletions

View File

@ -40,6 +40,54 @@ import logging
logger = logging.getLogger(__name__) 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] + [(None, 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): class TagForm(ModelForm):
required_css_class = 'required' required_css_class = 'required'
@ -68,16 +116,12 @@ class TagRenameForm(Form):
name = kwargs.pop("name", None) name = kwargs.pop("name", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if not (force or (not len(args) == 0 and 'force' in args[0])): if not (force or (not len(args) == 0 and 'force' in args[0])):
logger.warning('on delete ' + str(args))
del self.fields["force"] del self.fields["force"]
else:
logger.warning('on delete pas')
if not name is None and self.fields["name"].initial is None: if not name is None and self.fields["name"].initial is None:
self.fields["name"].initial = name self.fields["name"].initial = name
def is_force(self): def is_force(self):
logger.warning(self.cleaned_data)
return "force" in self.fields and self.cleaned_data["force"] == True return "force" in self.fields and self.cleaned_data["force"] == True
class URLSubmissionForm(Form): class URLSubmissionForm(Form):
@ -88,14 +132,12 @@ class URLSubmissionForm(Form):
label=_("Category"), label=_("Category"),
queryset=Category.objects.all().order_by("name"), queryset=Category.objects.all().order_by("name"),
initial=None, initial=None,
help_text=_('Optional. If you don''t specify a category, we''ll find it for you.'),
required=False, required=False,
) )
tags = MultipleChoiceField( tags = MultipleChoiceField(
label=_("Tags"), label=_("Tags"),
initial=None, initial=None,
choices=[], choices=[],
help_text=_('Optional. If you offer labels, they''ll make it easier for your girlfriends to find your event.'),
required=False required=False
) )
@ -133,7 +175,7 @@ class CategorisationRuleImportForm(ModelForm):
fields = "__all__" fields = "__all__"
class EventForm(ModelForm): class EventForm(GroupFormMixin, ModelForm):
required_css_class = 'required' required_css_class = 'required'
old_local_image = CharField(widget=HiddenInput(), required=False) old_local_image = CharField(widget=HiddenInput(), required=False)
@ -143,7 +185,6 @@ class EventForm(ModelForm):
label=_("Tags"), label=_("Tags"),
initial=None, initial=None,
choices=[], choices=[],
help_text=_('Optional. If you offer labels, they''ll make it easier for your girlfriends to find your event.'),
required=False required=False
) )
@ -192,6 +233,45 @@ class EventForm(ModelForm):
self.fields['category'].initial = Category.get_default_category() self.fields['category'].initial = Category.get_default_category()
self.fields['tags'].choices = Tag.get_tag_groups(all=True) 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=True)
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'
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): def is_clone_from_url(self):
return self.cloning return self.cloning

File diff suppressed because it is too large Load Diff

View File

@ -212,8 +212,8 @@ class Tag(models.Model):
) )
class Meta: class Meta:
verbose_name = _("Étiquette") verbose_name = _("Tag")
verbose_name_plural = _("Étiquettes") verbose_name_plural = _("Tags")
indexes = [ indexes = [
models.Index(fields=['name']), models.Index(fields=['name']),
] ]
@ -555,7 +555,7 @@ class Event(models.Model):
recurrence_dtend = models.DateTimeField(editable=False, blank=True, null=True) recurrence_dtend = models.DateTimeField(editable=False, blank=True, null=True)
title = models.CharField( title = models.CharField(
verbose_name=_("Title"), help_text=_("Short title"), max_length=512 verbose_name=_("Title"), max_length=512
) )
status = models.CharField( status = models.CharField(
@ -565,32 +565,27 @@ class Event(models.Model):
category = models.ForeignKey( category = models.ForeignKey(
Category, Category,
verbose_name=_("Category"), verbose_name=_("Category"),
help_text=_("Category of the event"),
null=True, null=True,
default=None, default=None,
on_delete=models.SET_DEFAULT, on_delete=models.SET_DEFAULT,
) )
start_day = models.DateField( start_day = models.DateField(
verbose_name=_("Day of the event"), help_text=_("Day of the event") verbose_name=_("Start day")
) )
start_time = models.TimeField( start_time = models.TimeField(
verbose_name=_("Starting time"), verbose_name=_("Start time"),
help_text=_("Starting time"),
blank=True, blank=True,
null=True, null=True,
) )
end_day = models.DateField( end_day = models.DateField(
verbose_name=_("End day of the event"), verbose_name=_("End day"),
help_text=_(
"End day of the event, only required if different from the start day."
),
blank=True, blank=True,
null=True, null=True,
) )
end_time = models.TimeField( end_time = models.TimeField(
verbose_name=_("Final time"), help_text=_("Final time"), blank=True, null=True verbose_name=_("End time"), blank=True, null=True
) )
recurrences = recurrence.fields.RecurrenceField( recurrences = recurrence.fields.RecurrenceField(
@ -600,7 +595,6 @@ class Event(models.Model):
exact_location = models.ForeignKey( exact_location = models.ForeignKey(
Place, Place,
verbose_name=_("Location"), verbose_name=_("Location"),
help_text=_("Address of the event"),
null=True, null=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
blank=True, blank=True,
@ -616,6 +610,13 @@ class Event(models.Model):
blank=True blank=True
) )
description = models.TextField(
verbose_name=_("Description"),
blank=True,
null=True,
)
organisers = models.ManyToManyField(Organisation, organisers = models.ManyToManyField(Organisation,
related_name='organised_events', related_name='organised_events',
verbose_name=_("Organisers"), verbose_name=_("Organisers"),
@ -625,24 +626,16 @@ class Event(models.Model):
blank=True blank=True
) )
description = models.TextField(
verbose_name=_("Description"),
help_text=_("General description of the event"),
blank=True,
null=True,
)
local_image = models.ImageField( local_image = models.ImageField(
verbose_name=_("Illustration (local image)"), verbose_name=_("Illustration"),
help_text=_("Illustration image stored in the agenda server"),
max_length=1024, max_length=1024,
blank=True, blank=True,
null=True, null=True,
) )
image = models.URLField( image = models.URLField(
verbose_name=_("Illustration"), verbose_name=_("Illustration (URL)"),
help_text=_("URL of the illustration image"), help_text=_("External URL of the illustration image"),
max_length=1024, max_length=1024,
blank=True, blank=True,
null=True, null=True,
@ -680,7 +673,6 @@ class Event(models.Model):
tags = ArrayField( tags = ArrayField(
models.CharField(max_length=64), models.CharField(max_length=64),
verbose_name=_("Tags"), verbose_name=_("Tags"),
help_text=_("A list of tags that describe the event."),
blank=True, blank=True,
null=True, null=True,
) )

View File

@ -43,6 +43,10 @@ const update_datetimes = (event) => {
start_day.oldvalue = start_day.value; start_day.oldvalue = start_day.value;
} }
else {
new_date = new Date(start_day.value);
end_day.value = formatDate(new_date);
}
} }
else { else {
if (end_day.value && end_time.value && start_day.value) { if (end_day.value && end_time.value && start_day.value) {

View File

@ -972,7 +972,7 @@ aside nav.paragraph li a, aside .no-breakline li a {
} }
/* mise en forme pour les récurrences */ /* mise en forme pour les récurrences */
article form p .recurrence-widget { article form div .recurrence-widget {
width: 100%; width: 100%;
border: 0; border: 0;
@ -1451,4 +1451,48 @@ form.messages div, form.moderation-events {
label.required::after { label.required::after {
content: ' *'; content: ' *';
color: red; color: red;
} }
.contenu #event_form {
div#group_start, div#group_end {
grid-column: auto;
.body_group {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(2, 1fr);
}
}
}
/* group form */
@media only screen and (min-width: 992px) {
.contenu #event_form form {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 10px;
>div {
grid-column: 1 / 3;
}
div#group_meta, div#group_meta-admin {
.body_group {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
div {
grid-column: auto;
}
}
}
div#group_meta {
.body_group {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
}
}
.maskable_group .body_group.closed {
display: none;
}

View File

@ -48,7 +48,7 @@ Duplication de {% else %}
{% block content %} {% block content %}
{% load static_content_extra %} {% load static_content_extra %}
<article> <article id="event_form">
<header> <header>
{% if object %} {% if object %}
<h1>{% if form.is_clone_from_url %} <h1>{% if form.is_clone_from_url %}
@ -78,8 +78,8 @@ Duplication de {% else %}
{% endif %} {% endif %}
<form method="post" action="{{ urlparam }}" enctype="multipart/form-data">{% csrf_token %} <form method="post" action="{{ urlparam }}" enctype="multipart/form-data">{% csrf_token %}
{{ form.media }} {{ form.media }}
{{ form.as_p }} {{ form }}
<div class="grid buttons"> <div class="grid buttons stick-bottom">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a> <a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Enregistrer"> <input type="submit" value="Enregistrer">
</div> </div>

View File

@ -0,0 +1,59 @@
{{ errors }}
{% if errors and not fields %}
<p>{% for field in hidden_fields %}{{ field }}{% endfor %}</p>
{% endif %}
{% for group, fields in form.fields_by_group %}
<div {% if group.maskable %}class="maskable_group"{% endif %} id="group_{{ group.id }}">
{% if group.maskable %}
<input class="toggle_body" type="checkbox" id="maskable_group_{{ group.id }}" name="group_{{ group.id }}"><label for="maskable_group_{{ group.id }}">{{ group.label }}</label>
{% endif %}
<div class="error_group">
{% for field in fields %}
<div id="error_{{ field.auto_id }}">
{{ field.errors }}
</div>
{% endfor %}
</div>
<div class="body_group">
{% for field in fields %}
<div id="div_{{ field.auto_id }}">
<div{% with classes=field.css_classes %}{% if classes %} class="{{ classes }}"{% endif %}{% endwith %}>
{% if field.label %}{{ field.label_tag }}{% endif %}
{{ field }}
{% if field.help_text %}
<span class="helptext"{% if field.auto_id %} id="{{ field.auto_id }}_helptext"{% endif %}>{{ field.help_text|safe }}</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% if not errors %}
{% for field in hidden_fields %}{{ field }}{% endfor %}
{% endif %}
<script>
const maskables = document.querySelectorAll('.maskable_group');
maskables.forEach(function (item) {
item.querySelector('.body_group').classList.add('closed');
console.log('item ' + item);
item.querySelector('.toggle_body').addEventListener('change', (event) => {
if (event.currentTarget.checked) {
item.querySelector('.body_group').classList.remove('closed');
} else {
item.querySelector('.body_group').classList.add('closed');
}
})
window.addEventListener('load', function(event) {
if (item.querySelector('.toggle_body').checked) {
item.querySelector('.body_group').classList.remove('closed');
}
else {
item.querySelector('.body_group').classList.add('closed');
}
});
})
</script>