mirror of
https://github.com/ae-utbm/sith.git
synced 2025-12-11 07:35:59 +00:00
refactor: Matmatronch
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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 %}
|
|
||||||
|
|||||||
@@ -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"),
|
|
||||||
]
|
]
|
||||||
|
|||||||
152
matmat/views.py
152
matmat/views.py
@@ -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"))
|
|
||||||
|
|||||||
Reference in New Issue
Block a user