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>
<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>
<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 typing import Annotated, Any
@@ -8,12 +9,12 @@ from django.urls import reverse
from django.utils.text import slugify
from django.utils.translation import gettext as _
from haystack.query import SearchQuerySet
from ninja import FilterSchema, ModelSchema, Schema, UploadedFile
from pydantic import AliasChoices, Field
from ninja import FilterLookup, FilterSchema, ModelSchema, Schema, UploadedFile
from pydantic import AliasChoices, Field, field_validator
from pydantic_core.core_schema import ValidationInfo
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)]
@@ -109,7 +110,11 @@ class GroupSchema(ModelSchema):
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(
None, validation_alias=AliasChoices("exclude", "exclude[]")
)
@@ -138,6 +143,13 @@ class UserFilterSchema(FilterSchema):
return Q()
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):
text: str

View File

@@ -23,7 +23,7 @@
<details name="navbar" class="menu">
<summary class="head">{% trans %}Services{% endtrans %}</summary>
<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('pedagogy:guide') }}">{% trans %}Pedagogy{% endtrans %}</a></li>
</ul>

View File

@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"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"
"Last-Translator: Maréchal <thomas.girod@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"
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
msgid "Search user"
msgstr "Rechercher un utilisateur"
@@ -4392,22 +4396,6 @@ msgstr "Rechercher un utilisateur"
msgid "Results"
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
msgid "Do not vote"
msgstr "Ne pas voter"

View File

@@ -20,6 +20,8 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from typing import Any
from django import forms
from django.utils.translation import gettext_lazy as _
@@ -30,29 +32,17 @@ from core.views.forms import SelectDate
class SearchForm(forms.ModelForm):
class Meta:
model = User
fields = [
"first_name",
"last_name",
"nick_name",
"role",
"department",
"semester",
"promo",
"date_of_birth",
"phone",
]
fields = ["promo", "role", "department", "semester", "date_of_birth"]
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):
super().__init__(*args, **kwargs)
def __init__(self, *args, initial: dict[str, Any], **kwargs):
super().__init__(*args, initial=initial, **kwargs)
for key in self.fields:
self.fields[key].required = False
@property
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
if key not in initial:
self.fields[key].initial = None

View File

@@ -1,12 +1,13 @@
{% extends "core/base.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 %}
{% trans %}Search user{% endtrans %}
{% endblock %}
{% block content %}
{% if result_exists %}
{% if paginator.count > 0 %}
<h2>{% trans %}Results{% endtrans %}</h2>
<div class="matmat_results">
{% for user in object_list %}
@@ -24,42 +25,8 @@
<hr>
{% endif %}
<h2>{% trans %}Search user{% endtrans %}</h2>
<h3>{% trans %}Search by profile{% endtrans %}</h3>
<form action="{{ url('matmat:search') }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% 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 action="{{ url('matmat:search') }}" method="get">
{{ form }}
<input type="submit" value="{% trans %}Search{% endtrans %}" />
</form>
{% endblock %}
{% block script %}
{{ super() }}
{% endblock %}

View File

@@ -23,16 +23,8 @@
from django.urls import path
from matmat.views import (
SearchClearFormView,
SearchNormalFormView,
SearchQuickFormView,
SearchReverseFormView,
)
from matmat.views import MatmatronchView
urlpatterns = [
path("", SearchNormalFormView.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"),
path("", MatmatronchView.as_view(), name="search"),
]

View File

@@ -20,150 +20,44 @@
# 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.http.response import HttpResponseRedirect
from django.urls import reverse
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 django.views.generic import ListView
from django.views.generic.edit import FormMixin
from core.auth.mixins import FormerSubscriberMixin
from core.models import User
from core.models import User, UserQuerySet
from core.schemas import UserFilterSchema
from matmat.forms import SearchForm
class SearchType(Enum):
NORMAL = 1
REVERSE = 2
QUICK = 3
# Views
class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
class MatmatronchView(FormerSubscriberMixin, FormMixin, ListView):
model = User
ordering = ["-id"]
paginate_by = 12
paginate_by = 20
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
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):
view = SearchFormListView.as_view()
return view(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)
self.form = self.get_form()
return super().get(request, *args, **kwargs)
def get_initial(self):
init = self.session.get("matmat_search_form", {})
if not init:
init["department"] = ""
return init
return self.request.GET
def get_form_kwargs(self):
res = super().get_form_kwargs()
if self.request.GET:
res["data"] = self.request.GET
return res
class SearchNormalFormView(SearchFormView):
search_type = SearchType.NORMAL
def get_queryset(self) -> UserQuerySet:
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))
class SearchReverseFormView(SearchFormView):
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"))
def get_context_data(self, **kwargs):
return super().get_context_data(form=self.form, **kwargs)