Ajout de suggestions dans les filtres d'inclusion et exclusion

This commit is contained in:
Jean-Marie Favreau 2024-11-13 23:52:02 +01:00
parent ed2f530f0c
commit fe1061e638
7 changed files with 424 additions and 351 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.9 on 2024-11-13 17:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0109_delete_moderationanswer_delete_moderationquestion'),
]
operations = [
migrations.AddField(
model_name='tag',
name='in_excluded_suggestions',
field=models.BooleanField(default=False, help_text='This tag will be part of the excluded suggestions.', verbose_name='In excluded suggestions'),
),
migrations.AddField(
model_name='tag',
name='in_included_suggestions',
field=models.BooleanField(default=False, help_text='This tag will be part of the included suggestions.', verbose_name='In included suggestions'),
),
]

View File

@ -1,4 +1,5 @@
from django.db import models, connection from django.db import models, connection
from django.core.exceptions import FieldDoesNotExist
from django_better_admin_arrayfield.models.fields import ArrayField from django_better_admin_arrayfield.models.fields import ArrayField
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -186,6 +187,18 @@ class Tag(models.Model):
default=True, default=True,
) )
in_excluded_suggestions = models.BooleanField(
verbose_name=_("In excluded suggestions"),
help_text=_("This tag will be part of the excluded suggestions."),
default=False,
)
in_included_suggestions = models.BooleanField(
verbose_name=_("In included suggestions"),
help_text=_("This tag will be part of the included suggestions."),
default=False,
)
category = models.ForeignKey( category = models.ForeignKey(
Category, Category,
verbose_name=_("Category"), verbose_name=_("Category"),
@ -199,6 +212,42 @@ class Tag(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("view_tag", kwargs={"t": self.name}) return reverse("view_tag", kwargs={"t": self.name})
def get_tag_groups(nb_suggestions=10, exclude=False, include=False):
free_tags = Event.get_all_tags(False)
obj_tags = Tag.objects
try:
if exclude:
obj_tags = obj_tags.filter(Q(in_excluded_suggestions=True))
if include:
obj_tags = obj_tags.filter(Q(in_included_suggestions=True)|Q(principal=True))
except FieldDoesNotExist:
pass
if not exclude and not include:
obj_tags = obj_tags.filter(principal=True)
obj_tags = obj_tags.values_list("name", flat=True)
if len(obj_tags) > nb_suggestions:
nb_suggestions = len(obj_tags)
tags = [{"tag": t["tag"], "count": 1000000 if t["tag"] in obj_tags else t["count"]} for t in free_tags]
tags.sort(key=lambda x: -x["count"])
tags1 = tags[0:nb_suggestions]
tags1.sort(key=lambda x: remove_accents(x["tag"]).lower())
tags2 = tags[nb_suggestions:]
tags2.sort(key=lambda x: remove_accents(x["tag"]).lower())
result = ((_('Suggestions'), [(t["tag"], t["tag"]) for t in tags1]),
(_('Others'), [(t["tag"], t["tag"]) for t in tags2]))
return result
class DuplicatedEvents(models.Model): class DuplicatedEvents(models.Model):
@ -626,14 +675,17 @@ class Event(models.Model):
verbose_name_plural = _("Events") verbose_name_plural = _("Events")
permissions = [("set_duplicated_event", "Can set an event as duplicated")] permissions = [("set_duplicated_event", "Can set an event as duplicated")]
def get_all_tags(): def get_all_tags(sort=True):
cursor = connection.cursor() cursor = connection.cursor()
raw_query = """ raw_query = """
select unnest(subquery_alias.tags) as distinct_tags, count(*) as tags_group_by_count select unnest(subquery_alias.tags) as distinct_tags, count(*) as tags_group_by_count
from (select tags from agenda_culturel_event) as subquery_alias group by distinct_tags; from (select tags from agenda_culturel_event) as subquery_alias group by distinct_tags;
""" """
cursor.execute(raw_query) cursor.execute(raw_query)
return [{"tag": row[0], "count": row[1]} for row in cursor] result = [{"tag": row[0], "count": row[1]} for row in cursor]
if sort:
result.sort(key=lambda x: remove_accents(x["tag"].lower()))
return result
def is_draft(self): def is_draft(self):
return self.status == Event.STATUS.DRAFT return self.status == Event.STATUS.DRAFT

View File

@ -1252,15 +1252,31 @@ img.preview {
} }
} }
.choices__list--dropdown { .choices__list--dropdown {
display: none; font-size: 80%;
display: block;
will-change: display; will-change: display;
background: var(--background-color);
padding: 0.4em;
border-radius: 0.2em;
box-shadow: 0 -1px 0 rgba(115, 130, 140, 0.2);
position: relative; position: relative;
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
will-change: scroll-position; will-change: scroll-position;
} }
.choices__list--dropdown.is-active { .choices__list--dropdown .choices__group,
.choices__list--dropdown .choices__item {
display: none;
}
.choices__list--dropdown .choices__item.visible,
.choices__list--dropdown.is-active .choices__item {
display: inline-block;
}
.choices__list--dropdown .choices__group.visible,
.choices__list--dropdown.is-active .choices__group {
display: block; display: block;
} }
} }

View File

@ -20,7 +20,7 @@
{{ t | tag_button:"true" }} {{ t | tag_button:"true" }}
{% endfor %} {% endfor %}
{% for t in filter.get_exclude_tags %} {% for t in filter.get_exclude_tags %}
{{ t | tag_button_strike }} {{ t | tag_button_strike:"true" }}
{% endfor %} {% endfor %}
{% for s in filter.get_status_names %} {% for s in filter.get_status_names %}
{{ s }} {{ s }}
@ -74,6 +74,23 @@
</div> </div>
<script src="{% static 'choicejs/choices.min.js' %}"></script> <script src="{% static 'choicejs/choices.min.js' %}"></script>
<script> <script>
show_firstgroup = {
choice(classes, choice) {
const i = Choices.defaults.templates.choice.call(this, classes, choice);
if (this.first_group !== null && choice.groupId == this.first_group)
i.classList.add("visible");
return i;
},
choiceGroup(classes, group) {
const g = Choices.defaults.templates.choiceGroup.call(this, classes, group);
if (this.first_group === undefined && group.value == "Suggestions")
this.first_group = group.id;
if (this.first_group !== null && group.id == this.first_group)
g.classList.add("visible");
return g;
}
};
const position = document.querySelector('#id_position'); const position = document.querySelector('#id_position');
const choices_position = new Choices(position); const choices_position = new Choices(position);
const tags = document.querySelector('#id_tags'); const tags = document.querySelector('#id_tags');
@ -84,7 +101,9 @@
delimiter: ',', delimiter: ',',
editItems: true, editItems: true,
removeItemButton: true, removeItemButton: true,
} shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
}
); );
const exclude_tags = document.querySelector('#id_exclude_tags'); const exclude_tags = document.querySelector('#id_exclude_tags');
const choices_exclude_tags = new Choices(exclude_tags, const choices_exclude_tags = new Choices(exclude_tags,
@ -94,13 +113,11 @@
delimiter: ',', delimiter: ',',
editItems: true, editItems: true,
removeItemButton: true, removeItemButton: true,
shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
} }
); );
document.querySelector("#id_position .choices__inner").classList.add("contrast");
document.querySelector("#id_position .choices__inner").classList.add("outline");
document.querySelector("#id_position .choices__inner").setAttribute("role", "button");
</script> </script>
{% if noarticle == 0 %} {% if noarticle == 0 %}

View File

@ -26,11 +26,23 @@
{% endif %} {% endif %}
{% else %} {% else %}
{% if perms.agenda_culturel.add_tag %} {% if perms.agenda_culturel.add_tag %}
<a href="{% url 'add_tag' %}?name={{ tag }}" role="button">Renseigner l'étiquette</a> <a href="{% url 'add_tag' %}?name={{ tag|urlencode }}" role="button">Renseigner l'étiquette</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
<h1>Les événements <em>{{ tag }}</em></h1></header> <h1>Les événements <em>{{ tag }}</em></h1>
{% if user.is_authenticated and object %}
{% if object.in_excluded_suggestions %}
<p class="remarque">Cette étiquette fait partie des étiquettes suggérées pour l'exclusion.</p>
{% endif %}
{% if object.in_included_suggestions %}
<p class="remarque">Cette étiquette fait partie des étiquettes suggérées pour l'inclusion.</p>
{% endif %}
{% if object.principal %}
<p class="remarque">Cette étiquette fait partie des étiquettes principales mises en avant.</p>
{% endif %}
{% endif %}
</header>
{% if object %} {% if object %}

View File

@ -164,7 +164,7 @@ class EventFilter(django_filters.FilterSet):
position = django_filters.ModelChoiceFilter( position = django_filters.ModelChoiceFilter(
label="À proximité de", label="À proximité de",
method="no_filter", method="no_filter",
empty_label=_("Choisir une localité"), empty_label=_("Select a location"),
queryset=ReferenceLocation.objects.all().order_by("-main", "name__unaccent") queryset=ReferenceLocation.objects.all().order_by("-main", "name__unaccent")
) )
@ -178,7 +178,7 @@ class EventFilter(django_filters.FilterSet):
exclude_tags = django_filters.MultipleChoiceFilter( exclude_tags = django_filters.MultipleChoiceFilter(
label="Exclure les étiquettes", label="Exclure les étiquettes",
choices=[(t["tag"], t["tag"]) for t in Event.get_all_tags()], choices=[],
lookup_expr="icontains", lookup_expr="icontains",
field_name="tags", field_name="tags",
exclude=True, exclude=True,
@ -187,7 +187,7 @@ class EventFilter(django_filters.FilterSet):
tags = django_filters.MultipleChoiceFilter( tags = django_filters.MultipleChoiceFilter(
label="Inclure les étiquettes", label="Inclure les étiquettes",
choices=[(t["tag"], t["tag"]) for t in Event.get_all_tags()], choices=[],
lookup_expr="icontains", lookup_expr="icontains",
conjoined=True, conjoined=True,
field_name="tags", field_name="tags",
@ -224,6 +224,8 @@ class EventFilter(django_filters.FilterSet):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if not kwargs["request"].user.is_authenticated: if not kwargs["request"].user.is_authenticated:
self.form.fields.pop("status") self.form.fields.pop("status")
self.form.fields["exclude_tags"].choices = Tag.get_tag_groups(exclude=True, nb_suggestions=0)
self.form.fields["tags"].choices = Tag.get_tag_groups(include=True)
def filter_recurrences(self, queryset, name, value): def filter_recurrences(self, queryset, name, value):
# construct the full lookup expression # construct the full lookup expression
@ -396,7 +398,7 @@ class EventFilter(django_filters.FilterSet):
return result return result
def tag_exists(self, tag): def tag_exists(self, tag):
return tag in [t[0] for t in self.form.fields["tags"].choices] return tag in [t[0] for g in self.form.fields["tags"].choices for t in g[1]]
def set_default_values(request): def set_default_values(request):
if request.user.is_authenticated: if request.user.is_authenticated:
@ -2145,14 +2147,14 @@ class PlaceFromEventCreateView(PlaceCreateView):
class TagUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): class TagUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView):
model = Tag model = Tag
permission_required = "agenda_culturel.change_tag" permission_required = "agenda_culturel.change_tag"
fields = ["name", "description", "principal", "category"] fields = ["name", "description", "principal", "category", "in_excluded_suggestions", "in_included_suggestions"]
success_message = _("The tag has been successfully updated.") success_message = _("The tag has been successfully updated.")
class TagCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): class TagCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
model = Tag model = Tag
permission_required = "agenda_culturel.add_tag" permission_required = "agenda_culturel.add_tag"
fields = ["name", "description", "principal", "category"] fields = ["name", "description", "principal", "category", "in_excluded_suggestions", "in_included_suggestions"]
success_message = _("The tag has been successfully created.") success_message = _("The tag has been successfully created.")
def get_initial(self, *args, **kwargs): def get_initial(self, *args, **kwargs):