refactor: Matmatronch

This commit is contained in:
imperosol
2025-11-30 18:06:26 +01:00
parent fca6a58c5e
commit 559a904e0d
8 changed files with 64 additions and 221 deletions

View File

@@ -211,7 +211,7 @@
</li> </li>
<li> <li>
<i class="fa-solid fa-magnifying-glass fa-xl"></i> <i class="fa-solid fa-magnifying-glass fa-xl"></i>
<a href="{{ url("matmat:search_clear") }}">{% trans %}Matmatronch{% endtrans %}</a> <a href="{{ url("matmat:search") }}">{% trans %}Matmatronch{% endtrans %}</a>
</li> </li>
<li> <li>
<i class="fa-solid fa-check-to-slot fa-xl"></i> <i class="fa-solid fa-check-to-slot fa-xl"></i>

View File

@@ -1,3 +1,4 @@
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Annotated, Any from typing import Annotated, Any
@@ -8,12 +9,12 @@ from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from ninja import FilterSchema, ModelSchema, Schema, UploadedFile from ninja import FilterLookup, FilterSchema, ModelSchema, Schema, UploadedFile
from pydantic import AliasChoices, Field from pydantic import AliasChoices, Field, field_validator
from pydantic_core.core_schema import ValidationInfo from pydantic_core.core_schema import ValidationInfo
from core.models import Group, QuickUploadImage, SithFile, User from core.models import Group, QuickUploadImage, SithFile, User
from core.utils import is_image from core.utils import get_last_promo, is_image
NonEmptyStr = Annotated[str, MinLen(1)] NonEmptyStr = Annotated[str, MinLen(1)]
@@ -109,7 +110,11 @@ class GroupSchema(ModelSchema):
class UserFilterSchema(FilterSchema): class UserFilterSchema(FilterSchema):
search: Annotated[str, MinLen(1)] search: Annotated[str, MinLen(1)] | None = None
role: Annotated[str, FilterLookup("role__icontains")] | None = None
department: str | None = None
promo: int | None = None
date_of_birth: datetime | None = None
exclude: list[int] | None = Field( exclude: list[int] | None = Field(
None, validation_alias=AliasChoices("exclude", "exclude[]") None, validation_alias=AliasChoices("exclude", "exclude[]")
) )
@@ -138,6 +143,13 @@ class UserFilterSchema(FilterSchema):
return Q() return Q()
return ~Q(id__in=value) return ~Q(id__in=value)
@field_validator("promo", mode="after")
@classmethod
def validate_promo(cls, value: int) -> int:
if not 0 < value <= get_last_promo():
raise ValueError(f"{value} is not a valid promo")
return value
class MarkdownSchema(Schema): class MarkdownSchema(Schema):
text: str text: str

View File

@@ -23,7 +23,7 @@
<details name="navbar" class="menu"> <details name="navbar" class="menu">
<summary class="head">{% trans %}Services{% endtrans %}</summary> <summary class="head">{% trans %}Services{% endtrans %}</summary>
<ul class="content"> <ul class="content">
<li><a href="{{ url('matmat:search_clear') }}">{% trans %}Matmatronch{% endtrans %}</a></li> <li><a href="{{ url('matmat:search') }}">{% trans %}Matmatronch{% endtrans %}</a></li>
<li><a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a></li> <li><a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a></li>
<li><a href="{{ url('pedagogy:guide') }}">{% trans %}Pedagogy{% endtrans %}</a></li> <li><a href="{{ url('pedagogy:guide') }}">{% trans %}Pedagogy{% endtrans %}</a></li>
</ul> </ul>

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-24 11:05+0100\n" "POT-Creation-Date: 2025-11-30 18:23+0100\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -4384,6 +4384,10 @@ msgstr "Galaxie de %(user_name)s"
msgid "This citizen has not yet joined the galaxy" msgid "This citizen has not yet joined the galaxy"
msgstr "Ce citoyen n'a pas encore rejoint la galaxie" msgstr "Ce citoyen n'a pas encore rejoint la galaxie"
#: matmat/forms.py
msgid "Last/First name or nickname"
msgstr "Nom de famille, prénom ou surnom"
#: matmat/templates/matmat/search_form.jinja #: matmat/templates/matmat/search_form.jinja
msgid "Search user" msgid "Search user"
msgstr "Rechercher un utilisateur" msgstr "Rechercher un utilisateur"
@@ -4392,22 +4396,6 @@ msgstr "Rechercher un utilisateur"
msgid "Results" msgid "Results"
msgstr "Résultats" msgstr "Résultats"
#: matmat/templates/matmat/search_form.jinja
msgid "Search by profile"
msgstr "Recherche par profil"
#: matmat/templates/matmat/search_form.jinja
msgid "Inverted search"
msgstr "Recherche inversée"
#: matmat/templates/matmat/search_form.jinja
msgid "Quick search"
msgstr "Recherche rapide"
#: matmat/views.py
msgid "Last/First name or nickname"
msgstr "Nom de famille, prénom ou surnom"
#: pedagogy/forms.py #: pedagogy/forms.py
msgid "Do not vote" msgid "Do not vote"
msgstr "Ne pas voter" msgstr "Ne pas voter"

View File

@@ -20,6 +20,8 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from typing import Any
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -30,29 +32,17 @@ from core.views.forms import SelectDate
class SearchForm(forms.ModelForm): class SearchForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = [ fields = ["promo", "role", "department", "semester", "date_of_birth"]
"first_name",
"last_name",
"nick_name",
"role",
"department",
"semester",
"promo",
"date_of_birth",
"phone",
]
widgets = {"date_of_birth": SelectDate} widgets = {"date_of_birth": SelectDate}
quick = forms.CharField(label=_("Last/First name or nickname"), max_length=255) name = forms.CharField(
label=_("Last/First name or nickname"), min_length=1, max_length=255
)
field_order = ["name", "promo", "role", "department", "semester", "date_of_birth"]
def __init__(self, *args, **kwargs): def __init__(self, *args, initial: dict[str, Any], **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, initial=initial, **kwargs)
for key in self.fields: for key in self.fields:
self.fields[key].required = False self.fields[key].required = False
if key not in initial:
@property self.fields[key].initial = None
def cleaned_data_json(self):
data = self.cleaned_data
if "date_of_birth" in data and data["date_of_birth"] is not None:
data["date_of_birth"] = str(data["date_of_birth"])
return data

View File

@@ -1,12 +1,13 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_mini_profile, paginate_jinja %} {% from "core/macros.jinja" import user_mini_profile, paginate_jinja %}
{% from "core/macros.jinja" import user_mini_profile, paginate_jinja with context %}
{% block title %} {% block title %}
{% trans %}Search user{% endtrans %} {% trans %}Search user{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if result_exists %} {% if paginator.count > 0 %}
<h2>{% trans %}Results{% endtrans %}</h2> <h2>{% trans %}Results{% endtrans %}</h2>
<div class="matmat_results"> <div class="matmat_results">
{% for user in object_list %} {% for user in object_list %}
@@ -24,42 +25,8 @@
<hr> <hr>
{% endif %} {% endif %}
<h2>{% trans %}Search user{% endtrans %}</h2> <h2>{% trans %}Search user{% endtrans %}</h2>
<h3>{% trans %}Search by profile{% endtrans %}</h3> <form action="{{ url('matmat:search') }}" method="get">
<form action="{{ url('matmat:search') }}" method="post" enctype="multipart/form-data"> {{ form }}
{% csrf_token %} <input type="submit" value="{% trans %}Search{% endtrans %}" />
{% for field in form %}
{% if field.name not in ('phone', 'quick') %}
<p>
{{ field.errors }}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
</p>
{% endif %}
{% endfor %}
<p><input type="submit" value="{% trans %}Search{% endtrans %}" /></p>
</form>
<h3>{% trans %}Inverted search{% endtrans %}</h3>
<form action="{{ url('matmat:search_reverse') }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<p>
{{ form.phone.errors }}
<label for="{{ form.phone.id_for_label }}">{{ form.phone.label }}</label>
{{ form.phone }}
<p><input type="submit" value="{% trans %}Search{% endtrans %}" /></p>
</p>
</form>
<h3>{% trans %}Quick search{% endtrans %}</h3>
<form action="{{ url('matmat:search_quick') }}" method="post">
{% csrf_token %}
<p>
{{ form.quick.errors }}
<label for="{{ form.quick.id_for_label }}">{{ form.quick.label }}</label>
{{ form.quick }}
<p><input type="submit" value="{% trans %}Search{% endtrans %}" /></p>
</p>
</form> </form>
{% endblock %} {% endblock %}
{% block script %}
{{ super() }}
{% endblock %}

View File

@@ -23,16 +23,8 @@
from django.urls import path from django.urls import path
from matmat.views import ( from matmat.views import MatmatronchView
SearchClearFormView,
SearchNormalFormView,
SearchQuickFormView,
SearchReverseFormView,
)
urlpatterns = [ urlpatterns = [
path("", SearchNormalFormView.as_view(), name="search"), path("", MatmatronchView.as_view(), name="search"),
path("reverse/", SearchReverseFormView.as_view(), name="search_reverse"),
path("quick/", SearchQuickFormView.as_view(), name="search_quick"),
path("clear/", SearchClearFormView.as_view(), name="search_clear"),
] ]

View File

@@ -20,150 +20,44 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from ast import literal_eval
from enum import Enum
from django import forms
from django.db.models import F from django.db.models import F
from django.http.response import HttpResponseRedirect from django.views.generic import ListView
from django.urls import reverse from django.views.generic.edit import FormMixin
from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView, View
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import FormMixin, FormView
from core.auth.mixins import FormerSubscriberMixin from core.auth.mixins import FormerSubscriberMixin
from core.models import User from core.models import User, UserQuerySet
from core.schemas import UserFilterSchema from core.schemas import UserFilterSchema
from matmat.forms import SearchForm from matmat.forms import SearchForm
class SearchType(Enum): class MatmatronchView(FormerSubscriberMixin, FormMixin, ListView):
NORMAL = 1
REVERSE = 2
QUICK = 3
# Views
class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
model = User model = User
ordering = ["-id"] paginate_by = 20
paginate_by = 12
template_name = "matmat/search_form.jinja" template_name = "matmat/search_form.jinja"
def dispatch(self, request, *args, **kwargs):
self.form_class = kwargs["form"]
self.search_type = kwargs["search_type"]
self.session = request.session
self.last_search = self.session.get("matmat_search_result", str([]))
self.last_search = literal_eval(self.last_search)
self.valid_form = kwargs.get("valid_form")
self.init_query = self.model.objects
self.can_see_hidden = True
if not (request.user.is_board_member or request.user.is_root):
self.can_see_hidden = False
self.init_query = self.init_query.filter(is_viewable=True)
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
self.object = None
kwargs = super().get_context_data(**kwargs)
kwargs["form"] = self.form_class
kwargs["result_exists"] = self.result_exists
return kwargs
def get_queryset(self):
q = self.init_query
if self.valid_form is not None:
if self.search_type == SearchType.REVERSE:
q = q.filter(phone=self.valid_form["phone"]).all()
elif self.search_type == SearchType.QUICK:
if self.valid_form["quick"].strip():
q = list(
UserFilterSchema(search=self.valid_form["quick"])
.filter(User.objects.viewable_by(self.request.user))
.order_by(F("last_login").desc(nulls_last=True))
)
else:
q = []
else:
search_dict = {}
for key, value in self.valid_form.items():
if key not in ("phone", "quick") and not (
value == "" or value is None
):
search_dict[key + "__icontains"] = value
q = q.filter(**search_dict).all()
else:
q = q.filter(pk__in=self.last_search).all()
if isinstance(q, list):
self.result_exists = len(q) > 0
else:
self.result_exists = q.exists()
self.last_search = []
for user in q:
self.last_search.append(user.id)
self.session["matmat_search_result"] = str(self.last_search)
return q
class SearchFormView(FormerSubscriberMixin, FormView):
"""Allows users to search inside the user list."""
form_class = SearchForm form_class = SearchForm
def dispatch(self, request, *args, **kwargs):
self.session = request.session
self.init_query = User.objects
kwargs["form"] = self.get_form()
kwargs["search_type"] = self.search_type
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
view = SearchFormListView.as_view() self.form = self.get_form()
return view(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
form = self.get_form()
view = SearchFormListView.as_view()
if form.is_valid():
kwargs["valid_form"] = form.clean()
request.session["matmat_search_form"] = form.cleaned_data_json
return view(request, *args, **kwargs)
def get_initial(self): def get_initial(self):
init = self.session.get("matmat_search_form", {}) return self.request.GET
if not init:
init["department"] = ""
return init
def get_form_kwargs(self):
res = super().get_form_kwargs()
if self.request.GET:
res["data"] = self.request.GET
return res
class SearchNormalFormView(SearchFormView): def get_queryset(self) -> UserQuerySet:
search_type = SearchType.NORMAL if not self.form.is_valid():
return User.objects.none()
data = self.form.cleaned_data
data["search"] = data.get("name")
filters = UserFilterSchema(**{key: val for key, val in data.items() if val})
qs = User.objects.viewable_by(self.request.user).select_related("profile_pict")
return filters.filter(qs).order_by(F("last_login").desc(nulls_last=True))
def get_context_data(self, **kwargs):
class SearchReverseFormView(SearchFormView): return super().get_context_data(form=self.form, **kwargs)
search_type = SearchType.REVERSE
class SearchQuickFormView(SearchFormView):
search_type = SearchType.QUICK
class SearchClearFormView(FormerSubscriberMixin, View):
"""Clear SearchFormView and redirect to it."""
def dispatch(self, request, *args, **kwargs):
super().dispatch(request, *args, **kwargs)
if "matmat_search_form" in request.session:
request.session.pop("matmat_search_form")
if "matmat_search_result" in request.session:
request.session.pop("matmat_search_result")
return HttpResponseRedirect(reverse("matmat:search"))