Merge pull request #898 from ae-utbm/taiste

Complete webpack migration, introduction of tom select, better SAS moderation workflow, more ruff and bugfixes
This commit is contained in:
thomas girod 2024-10-18 11:11:39 +02:00 committed by GitHub
commit e6f25fb707
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
132 changed files with 3710 additions and 2434 deletions

View File

@ -15,7 +15,16 @@
from django.contrib import admin
from accounting.models import *
from accounting.models import (
AccountingType,
BankAccount,
ClubAccount,
Company,
GeneralJournal,
Label,
Operation,
SimplifiedAccountingType,
)
admin.site.register(BankAccount)
admin.site.register(ClubAccount)

View File

@ -82,9 +82,7 @@ class Company(models.Model):
def is_owned_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return False
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
@ -127,9 +125,7 @@ class BankAccount(models.Model):
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
m = self.club.get_membership_for(user)
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
class ClubAccount(models.Model):
@ -161,29 +157,20 @@ class ClubAccount(models.Model):
"""Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return False
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
m = self.club.get_membership_for(user)
if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
return m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
def can_be_viewed_by(self, user):
"""Check if that object can be viewed by the given user."""
m = self.club.get_membership_for(user)
if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
return m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
def has_open_journal(self):
for j in self.journals.all():
if not j.closed:
return True
return False
return self.journals.filter(closed=False).exists()
def get_open_journal(self):
return self.journals.filter(closed=False).first()
@ -228,17 +215,13 @@ class GeneralJournal(models.Model):
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
if self.club_account.can_be_edited_by(user):
return True
return False
return self.club_account.can_be_edited_by(user)
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
if self.club_account.can_be_edited_by(user):
return True
return False
return self.club_account.can_be_edited_by(user)
def can_be_viewed_by(self, user):
return self.club_account.can_be_viewed_by(user)
@ -416,9 +399,7 @@ class Operation(models.Model):
if self.journal.closed:
return False
m = self.journal.club_account.club.get_membership_for(user)
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
@ -427,9 +408,7 @@ class Operation(models.Model):
if self.journal.closed:
return False
m = self.journal.club_account.club.get_membership_for(user)
if m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
return m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
class AccountingType(models.Model):
@ -472,9 +451,7 @@ class AccountingType(models.Model):
"""Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return False
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
class SimplifiedAccountingType(models.Model):

View File

@ -102,7 +102,7 @@ class TestOperation(TestCase):
code="443", label="Ce code n'existe pas", movement_type="CREDIT"
)
at.save()
l = Label.objects.create(club_account=self.journal.club_account, name="bob")
label = Label.objects.create(club_account=self.journal.club_account, name="bob")
self.client.force_login(User.objects.get(username="comptable"))
self.op1 = Operation(
journal=self.journal,
@ -111,7 +111,7 @@ class TestOperation(TestCase):
remark="Test bilan",
mode="CASH",
done=True,
label=l,
label=label,
accounting_type=at,
target_type="USER",
target_id=self.skia.id,
@ -124,7 +124,7 @@ class TestOperation(TestCase):
remark="Test bilan",
mode="CASH",
done=True,
label=l,
label=label,
accounting_type=at,
target_type="USER",
target_id=self.skia.id,

View File

@ -15,7 +15,41 @@
from django.urls import path
from accounting.views import *
from accounting.views import (
AccountingTypeCreateView,
AccountingTypeEditView,
AccountingTypeListView,
BankAccountCreateView,
BankAccountDeleteView,
BankAccountDetailView,
BankAccountEditView,
BankAccountListView,
ClubAccountCreateView,
ClubAccountDeleteView,
ClubAccountDetailView,
ClubAccountEditView,
CompanyCreateView,
CompanyEditView,
CompanyListView,
JournalAccountingStatementView,
JournalCreateView,
JournalDeleteView,
JournalDetailView,
JournalEditView,
JournalNatureStatementView,
JournalPersonStatementView,
LabelCreateView,
LabelDeleteView,
LabelEditView,
LabelListView,
OperationCreateView,
OperationEditView,
OperationPDFView,
RefoundAccountView,
SimplifiedAccountingTypeCreateView,
SimplifiedAccountingTypeEditView,
SimplifiedAccountingTypeListView,
)
urlpatterns = [
# Accounting types

View File

@ -182,7 +182,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView):
def get_initial(self):
ret = super().get_initial()
if "parent" in self.request.GET.keys():
if "parent" in self.request.GET:
obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first()
if obj is not None:
ret["bank_account"] = obj.id
@ -264,7 +264,7 @@ class JournalCreateView(CanCreateMixin, CreateView):
def get_initial(self):
ret = super().get_initial()
if "parent" in self.request.GET.keys():
if "parent" in self.request.GET:
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
if obj is not None:
ret["club_account"] = obj.id
@ -362,7 +362,7 @@ class OperationForm(forms.ModelForm):
def clean(self):
self.cleaned_data = super().clean()
if "target_type" in self.cleaned_data.keys():
if "target_type" in self.cleaned_data:
if (
self.cleaned_data.get("user") is None
and self.cleaned_data.get("club") is None
@ -633,19 +633,17 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
ret = collections.OrderedDict()
statement = collections.OrderedDict()
total_sum = 0
for sat in [None] + list(
SimplifiedAccountingType.objects.order_by("label").all()
):
for sat in [
None,
*list(SimplifiedAccountingType.objects.order_by("label")),
]:
amount = queryset.filter(
accounting_type__movement_type=movement_type, simpleaccounting_type=sat
).aggregate(amount_sum=Sum("amount"))["amount_sum"]
if sat:
sat = sat.label
else:
sat = ""
label = sat.label if sat is not None else ""
if amount:
total_sum += amount
statement[sat] = amount
statement[label] = amount
ret[movement_type] = statement
ret[movement_type + "_sum"] = total_sum
return ret
@ -668,15 +666,12 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
self.statement(self.object.operations.filter(label=None).all(), "DEBIT")
)
statement[_("No label operations")] = no_label_statement
for l in labels:
for label in labels:
l_stmt = collections.OrderedDict()
l_stmt.update(
self.statement(self.object.operations.filter(label=l).all(), "CREDIT")
)
l_stmt.update(
self.statement(self.object.operations.filter(label=l).all(), "DEBIT")
)
statement[l] = l_stmt
journals = self.object.operations.filter(label=label).all()
l_stmt.update(self.statement(journals, "CREDIT"))
l_stmt.update(self.statement(journals, "DEBIT"))
statement[label] = l_stmt
return statement
def get_context_data(self, **kwargs):
@ -798,7 +793,7 @@ class LabelCreateView(
def get_initial(self):
ret = super().get_initial()
if "parent" in self.request.GET.keys():
if "parent" in self.request.GET:
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
if obj is not None:
ret["club_account"] = obj.id

View File

@ -7,7 +7,7 @@
},
"files": {
"ignoreUnknown": false,
"ignore": ["core/static/vendored", "*.min.*", "staticfiles/generated"]
"ignore": ["*.min.*", "staticfiles/generated"]
},
"formatter": {
"enabled": true,

View File

@ -111,8 +111,8 @@ class MailingForm(forms.Form):
"""Convert given users into real users and check their validity."""
cleaned_data = super().clean()
users = []
for user in cleaned_data["subscription_users"]:
user = User.objects.filter(id=user).first()
for user_id in cleaned_data["subscription_users"]:
user = User.objects.filter(id=user_id).first()
if not user:
raise forms.ValidationError(
_("One of the selected users doesn't exist"), code="invalid"
@ -128,7 +128,7 @@ class MailingForm(forms.Form):
def clean(self):
cleaned_data = super().clean()
if not "action" in cleaned_data:
if "action" not in cleaned_data:
# If there is no action provided, we can stop here
raise forms.ValidationError(_("An action is required"), code="invalid")

View File

@ -389,9 +389,7 @@ class Membership(models.Model):
if user.is_root or user.is_board_member:
return True
membership = self.club.get_membership_for(user)
if membership is not None and membership.role >= self.role:
return True
return False
return membership is not None and membership.role >= self.role
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)

View File

@ -24,7 +24,32 @@
from django.urls import path
from club.views import *
from club.views import (
ClubCreateView,
ClubEditPropView,
ClubEditView,
ClubListView,
ClubMailingView,
ClubMembersView,
ClubOldMembersView,
ClubPageEditView,
ClubPageHistView,
ClubRevView,
ClubSellingCSVView,
ClubSellingView,
ClubStatView,
ClubToolsView,
ClubView,
MailingAutoGenerationView,
MailingDeleteView,
MailingSubscriptionDeleteView,
MembershipDeleteView,
MembershipSetOldView,
PosterCreateView,
PosterDeleteView,
PosterEditView,
PosterListView,
)
urlpatterns = [
path("", ClubListView.as_view(), name="club_list"),
@ -32,32 +57,20 @@ urlpatterns = [
path("stats/", ClubStatView.as_view(), name="club_stats"),
path("<int:club_id>/", ClubView.as_view(), name="club_view"),
path(
"<int:club_id>/rev/<int:rev_id>/",
ClubRevView.as_view(),
name="club_view_rev",
"<int:club_id>/rev/<int:rev_id>/", ClubRevView.as_view(), name="club_view_rev"
),
path("<int:club_id>/hist/", ClubPageHistView.as_view(), name="club_hist"),
path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"),
path(
"<int:club_id>/edit/page/",
ClubPageEditView.as_view(),
name="club_edit_page",
),
path("<int:club_id>/edit/page/", ClubPageEditView.as_view(), name="club_edit_page"),
path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"),
path(
"<int:club_id>/elderlies/",
ClubOldMembersView.as_view(),
name="club_old_members",
),
path("<int:club_id>/sellings/", ClubSellingView.as_view(), name="club_sellings"),
path(
"<int:club_id>/sellings/",
ClubSellingView.as_view(),
name="club_sellings",
),
path(
"<int:club_id>/sellings/csv/",
ClubSellingCSVView.as_view(),
name="sellings_csv",
"<int:club_id>/sellings/csv/", ClubSellingCSVView.as_view(), name="sellings_csv"
),
path("<int:club_id>/prop/", ClubEditPropView.as_view(), name="club_prop"),
path("<int:club_id>/tools/", ClubToolsView.as_view(), name="tools"),
@ -89,9 +102,7 @@ urlpatterns = [
),
path("<int:club_id>/poster/", PosterListView.as_view(), name="poster_list"),
path(
"<int:club_id>/poster/create/",
PosterCreateView.as_view(),
name="poster_create",
"<int:club_id>/poster/create/", PosterCreateView.as_view(), name="poster_create"
),
path(
"<int:club_id>/poster/<int:poster_id>/edit/",

View File

@ -397,7 +397,8 @@ class ClubSellingCSVView(ClubSellingView):
row.append(selling.customer.user.get_display_name())
else:
row.append("")
row = row + [
row = [
*row,
selling.label,
selling.quantity,
selling.quantity * selling.unit_price,
@ -408,7 +409,7 @@ class ClubSellingCSVView(ClubSellingView):
row.append(selling.product.purchase_price)
row.append(selling.product.selling_price - selling.product.purchase_price)
else:
row = row + ["", "", ""]
row = [*row, "", "", ""]
return row
def get(self, request, *args, **kwargs):
@ -622,9 +623,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
def remove_subscription(self, cleaned_data):
"""Remove specified users from a mailing list."""
fields = [
cleaned_data[key]
for key in cleaned_data.keys()
if key.startswith("removal_")
val for key, val in cleaned_data.items() if key.startswith("removal_")
]
for field in fields:
for sub in field:

View File

@ -15,7 +15,7 @@
from django.contrib import admin
from haystack.admin import SearchModelAdmin
from com.models import *
from com.models import News, Poster, Screen, Sith, Weekmail
@admin.register(News)

View File

@ -16,7 +16,36 @@
from django.urls import path
from club.views import MailingDeleteView
from com.views import *
from com.views import (
AlertMsgEditView,
InfoMsgEditView,
MailingListAdminView,
MailingModerateView,
NewsAdminListView,
NewsCreateView,
NewsDeleteView,
NewsDetailView,
NewsEditView,
NewsListView,
NewsModerateView,
PosterCreateView,
PosterDeleteView,
PosterEditView,
PosterListView,
PosterModerateListView,
PosterModerateView,
ScreenCreateView,
ScreenDeleteView,
ScreenEditView,
ScreenListView,
ScreenSlideshowView,
WeekmailArticleCreateView,
WeekmailArticleDeleteView,
WeekmailArticleEditView,
WeekmailDestinationEditView,
WeekmailEditView,
WeekmailPreviewView,
)
urlpatterns = [
path("sith/edit/alert/", AlertMsgEditView.as_view(), name="alert_edit"),
@ -46,15 +75,9 @@ urlpatterns = [
path("news/", NewsListView.as_view(), name="news_list"),
path("news/admin/", NewsAdminListView.as_view(), name="news_admin_list"),
path("news/create/", NewsCreateView.as_view(), name="news_new"),
path("news/<int:news_id>/delete/", NewsDeleteView.as_view(), name="news_delete"),
path(
"news/<int:news_id>/delete/",
NewsDeleteView.as_view(),
name="news_delete",
),
path(
"news/<int:news_id>/moderate/",
NewsModerateView.as_view(),
name="news_moderate",
"news/<int:news_id>/moderate/", NewsModerateView.as_view(), name="news_moderate"
),
path("news/<int:news_id>/edit/", NewsEditView.as_view(), name="news_edit"),
path("news/<int:news_id>/", NewsDetailView.as_view(), name="news_detail"),
@ -71,11 +94,7 @@ urlpatterns = [
),
path("poster/", PosterListView.as_view(), name="poster_list"),
path("poster/create/", PosterCreateView.as_view(), name="poster_create"),
path(
"poster/<int:poster_id>/edit/",
PosterEditView.as_view(),
name="poster_edit",
),
path("poster/<int:poster_id>/edit/", PosterEditView.as_view(), name="poster_edit"),
path(
"poster/<int:poster_id>/delete/",
PosterDeleteView.as_view(),
@ -98,11 +117,7 @@ urlpatterns = [
ScreenSlideshowView.as_view(),
name="screen_slideshow",
),
path(
"screen/<int:screen_id>/edit/",
ScreenEditView.as_view(),
name="screen_edit",
),
path("screen/<int:screen_id>/edit/", ScreenEditView.as_view(), name="screen_edit"),
path(
"screen/<int:screen_id>/delete/",
ScreenDeleteView.as_view(),

View File

@ -86,8 +86,7 @@ class PosterForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
if self.user:
if not self.user.is_com_admin:
if self.user and not self.user.is_com_admin:
self.fields["club"].queryset = Club.objects.filter(
id__in=self.user.clubs_with_rights
)
@ -312,7 +311,7 @@ class NewsCreateView(CanCreateMixin, CreateView):
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid() and "preview" not in request.POST.keys():
if form.is_valid() and "preview" not in request.POST:
return self.form_valid(form)
else:
self.object = form.instance
@ -354,13 +353,13 @@ class NewsModerateView(CanEditMixin, SingleObjectMixin):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if "remove" in request.GET.keys():
if "remove" in request.GET:
self.object.is_moderated = False
else:
self.object.is_moderated = True
self.object.moderator = request.user
self.object.save()
if "next" in self.request.GET.keys():
if "next" in self.request.GET:
return redirect(self.request.GET["next"])
return redirect("com:news_admin_list")
@ -424,7 +423,7 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai
try:
self.object.send() # This should fail
except SMTPRecipientsRefused as e:
users = User.objects.filter(email__in=e.recipients.keys())
users = User.objects.filter(email__in=e.recipients)
for u in users:
u.preferences.receive_weekmail = False
u.preferences.save()
@ -471,7 +470,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if "up_article" in request.GET.keys():
if "up_article" in request.GET:
art = get_object_or_404(
WeekmailArticle, id=request.GET["up_article"], weekmail=self.object
)
@ -483,7 +482,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
art.save()
prev_art.save()
self.quick_notif_list += ["qn_success"]
if "down_article" in request.GET.keys():
if "down_article" in request.GET:
art = get_object_or_404(
WeekmailArticle, id=request.GET["down_article"], weekmail=self.object
)
@ -495,7 +494,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
art.save()
next_art.save()
self.quick_notif_list += ["qn_success"]
if "add_article" in request.GET.keys():
if "add_article" in request.GET:
art = get_object_or_404(
WeekmailArticle, id=request.GET["add_article"], weekmail=None
)
@ -504,7 +503,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
art.rank += 1
art.save()
self.quick_notif_list += ["qn_success"]
if "del_article" in request.GET.keys():
if "del_article" in request.GET:
art = get_object_or_404(
WeekmailArticle, id=request.GET["del_article"], weekmail=self.object
)
@ -571,7 +570,7 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
)
),
)
if form.is_valid() and not "preview" in request.POST.keys():
if form.is_valid() and "preview" not in request.POST:
return self.form_valid(form)
else:
return self.form_invalid(form)
@ -689,19 +688,13 @@ class PosterEditBaseView(UpdateView):
template_name = "com/poster_edit.jinja"
def get_initial(self):
init = {}
try:
init["date_begin"] = self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
pass
try:
init["date_end"] = self.object.date_end.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
pass
return init
return {
"date_begin": self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S"),
"date_end": self.object.date_end.strftime("%Y-%m-%d %H:%M:%S"),
}
def dispatch(self, request, *args, **kwargs):
if "club_id" in kwargs and kwargs["club_id"]:
if kwargs.get("club_id"):
try:
self.club = Club.objects.get(pk=kwargs["club_id"])
except Club.DoesNotExist as e:
@ -737,7 +730,7 @@ class PosterDeleteBaseView(DeleteView):
template_name = "core/delete_confirm.jinja"
def dispatch(self, request, *args, **kwargs):
if "club_id" in kwargs and kwargs["club_id"]:
if kwargs.get("club_id"):
try:
self.club = Club.objects.get(pk=kwargs["club_id"])
except Club.DoesNotExist as e:

View File

@ -1,117 +0,0 @@
import re
from subprocess import PIPE, Popen, TimeoutExpired
from django.conf import settings
from django.core.management.base import BaseCommand
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
# added "v?"
# Please note that this does not match the version of the three.js library.
# Hence, you shall have to check this one by yourself
semver_regex = re.compile(
r"^v?"
r"(?P<major>\d+)"
r"\.(?P<minor>\d+)"
r"\.(?P<patch>\d+)"
r"(?:-(?P<prerelease>(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
)
class Command(BaseCommand):
help = "Checks the front dependencies are up to date."
def handle(self, *args, **options):
deps = settings.SITH_FRONT_DEP_VERSIONS
processes = {
url: create_process(url)
for url in deps.keys()
if parse_semver(deps[url]) is not None
}
for url, process in processes.items():
try:
stdout, stderr = process.communicate(timeout=15)
except TimeoutExpired:
process.kill()
self.stderr.write(self.style.WARNING("{}: timeout".format(url)))
continue
# error, notice, warning
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
if stderr != "":
self.stderr.write(self.style.WARNING(stderr.strip()))
continue
# get all tags, parse them as semvers and find the biggest
tags = list_tags(stdout)
tags = map(parse_semver, tags)
tags = filter(lambda tag: tag is not None, tags)
latest_version = max(tags)
# cannot fail as those which fail are filtered in the processes dict creation
current_version = parse_semver(deps[url])
assert current_version is not None
if latest_version == current_version:
msg = "{}: {}".format(url, semver_to_s(current_version))
self.stdout.write(self.style.SUCCESS(msg))
else:
msg = "{}: {} < {}".format(
url, semver_to_s(current_version), semver_to_s(latest_version)
)
self.stdout.write(self.style.ERROR(msg))
def create_process(url):
"""Spawn a "git ls-remote --tags" child process."""
return Popen(["git", "ls-remote", "--tags", url], stdout=PIPE, stderr=PIPE)
def list_tags(s):
"""Parses "git ls-remote --tags" output. Takes a string."""
tag_prefix = "refs/tags/"
for line in s.strip().split("\n"):
# an example line could be:
# "1f41e2293f9c3c1962d2d97afa666207b98a222a\trefs/tags/foo"
parts = line.split("\t")
# check we have a commit ID (SHA-1 hash) and a tag name
assert len(parts) == 2
assert len(parts[0]) == 40
assert parts[1].startswith(tag_prefix)
# avoid duplicates (a peeled tag will appear twice: as "name" and as "name^{}")
if not parts[1].endswith("^{}"):
yield parts[1][len(tag_prefix) :]
def parse_semver(s) -> tuple[int, int, int] | None:
"""Parse a semver string.
See https://semver.org
Returns:
A tuple, if the parsing was successful, else None.
In the latter case, it must probably be a prerelease
or include build metadata.
"""
m = semver_regex.match(s)
if (
m is None
or m.group("prerelease") is not None
or m.group("buildmetadata") is not None
):
return None
return int(m.group("major")), int(m.group("minor")), int(m.group("patch"))
def semver_to_s(t):
"""Expects a 3-tuple with ints and turns it into a string of type "1.2.3"."""
return "{}.{}.{}".format(t[0], t[1], t[2])

View File

@ -67,5 +67,6 @@ class Command(BaseCommand):
subprocess.run(
[str(Path(__file__).parent / "install_xapian.sh"), desired],
env=dict(os.environ),
).check_returncode()
check=True,
)
self.stdout.write("Installation success")

View File

@ -934,7 +934,7 @@ Welcome to the wiki page!
# Adding subscription for sli
s = Subscription(
member=User.objects.filter(pk=sli.pk).first(),
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0],
subscription_type=next(iter(settings.SITH_SUBSCRIPTIONS.keys())),
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
)
s.subscription_start = s.compute_start()
@ -947,7 +947,7 @@ Welcome to the wiki page!
# Adding subscription for Krophil
s = Subscription(
member=User.objects.filter(pk=krophil.pk).first(),
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0],
subscription_type=next(iter(settings.SITH_SUBSCRIPTIONS.keys())),
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
)
s.subscription_start = s.compute_start()

View File

@ -217,9 +217,9 @@ class Command(BaseCommand):
UV.objects.bulk_create(uvs, ignore_conflicts=True)
def create_products(self):
categories = []
for _ in range(10):
categories.append(ProductType(name=self.faker.text(max_nb_chars=30)))
categories = [
ProductType(name=self.faker.text(max_nb_chars=30)) for _ in range(10)
]
ProductType.objects.bulk_create(categories)
categories = list(
ProductType.objects.filter(name__in=[c.name for c in categories])
@ -254,15 +254,15 @@ class Command(BaseCommand):
archived=bool(random.random() > 0.7),
)
products.append(product)
for group in random.sample(groups, k=random.randint(0, 3)):
# there will be products without buying groups
# but there are also such products in the real database
buying_groups.append(
buying_groups.extend(
Product.buying_groups.through(product=product, group=group)
for group in random.sample(groups, k=random.randint(0, 3))
)
for counter in random.sample(counters, random.randint(0, 4)):
selling_places.append(
selling_places.extend(
Counter.products.through(counter=counter, product=product)
for counter in random.sample(counters, random.randint(0, 4))
)
Product.objects.bulk_create(products)
Product.buying_groups.through.objects.bulk_create(buying_groups)

View File

@ -174,7 +174,7 @@ def validate_promo(value: int) -> None:
)
def get_group(*, pk: int = None, name: str = None) -> Group | None:
def get_group(*, pk: int | None = None, name: str | None = None) -> Group | None:
"""Search for a group by its primary key or its name.
Either one of the two must be set.
@ -445,7 +445,7 @@ class User(AbstractBaseUser):
else:
return 0
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
def is_in_group(self, *, pk: int | None = None, name: str | None = None) -> bool:
"""Check if this user is in the given group.
Either a group id or a group name must be provided.
If both are passed, only the id will be considered.
@ -649,7 +649,7 @@ class User(AbstractBaseUser):
continue
links = list(User.godfathers.through.objects.filter(**{key: self.id}))
res.extend(links)
for _ in range(1, depth):
for _ in range(1, depth): # noqa: F402 we don't care about gettext here
ids = [getattr(c, reverse_key) for c in links]
links = list(
User.godfathers.through.objects.filter(
@ -703,9 +703,7 @@ class User(AbstractBaseUser):
return True
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
return True
if self.is_root:
return True
return False
return self.is_root
def can_edit(self, obj):
"""Determine if the object can be edited by the user."""
@ -717,9 +715,7 @@ class User(AbstractBaseUser):
return True
if isinstance(obj, User) and obj == self:
return True
if self.is_owner(obj):
return True
return False
return self.is_owner(obj)
def can_view(self, obj):
"""Determine if the object can be viewed by the user."""
@ -729,9 +725,7 @@ class User(AbstractBaseUser):
for pk in obj.view_groups.values_list("pk", flat=True):
if self.is_in_group(pk=pk):
return True
if self.can_edit(obj):
return True
return False
return self.can_edit(obj)
def can_be_edited_by(self, user):
return user.is_root or user.is_board_member
@ -759,23 +753,17 @@ class User(AbstractBaseUser):
@cached_property
def preferences(self):
try:
if hasattr(self, "_preferences"):
return self._preferences
except:
prefs = Preferences(user=self)
prefs.save()
return prefs
return Preferences.objects.create(user=self)
@cached_property
def forum_infos(self):
try:
if hasattr(self, "_forum_infos"):
return self._forum_infos
except:
from forum.models import ForumUserInfo
infos = ForumUserInfo(user=self)
infos.save()
return infos
return ForumUserInfo.objects.create(user=self)
@cached_property
def clubs_with_rights(self) -> list[Club]:
@ -840,7 +828,7 @@ class AnonymousUser(AuthAnonymousUser):
def favorite_topics(self):
raise PermissionDenied
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
def is_in_group(self, *, pk: int | None = None, name: str | None = None) -> bool:
"""The anonymous user is only in the public group."""
allowed_id = settings.SITH_GROUP_PUBLIC_ID
if pk is not None:
@ -867,9 +855,7 @@ class AnonymousUser(AuthAnonymousUser):
and obj.view_groups.filter(id=settings.SITH_GROUP_PUBLIC_ID).exists()
):
return True
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
return True
return False
return hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self)
def get_display_name(self):
return _("Visitor")
@ -1070,7 +1056,7 @@ class SithFile(models.Model):
]:
self.file.delete()
self.file = None
except:
except: # noqa E722 I don't know the exception that can be raised
self.file = None
self.mime_type = "inode/directory"
if self.is_file and (self.file is None or self.file == ""):
@ -1196,12 +1182,12 @@ class SithFile(models.Model):
return Album.objects.filter(id=self.id).first()
def get_parent_list(self):
l = []
p = self.parent
while p is not None:
l.append(p)
p = p.parent
return l
parents = []
current = self.parent
while current is not None:
parents.append(current)
current = current.parent
return parents
def get_parent_path(self):
return "/" + "/".join([p.name for p in self.get_parent_list()[::-1]])
@ -1359,22 +1345,18 @@ class Page(models.Model):
if hasattr(self, "club") and self.club.can_be_edited_by(user):
# Override normal behavior for clubs
return True
if self.name == settings.SITH_CLUB_ROOT_PAGE and user.is_board_member:
return True
return False
return self.name == settings.SITH_CLUB_ROOT_PAGE and user.is_board_member
def can_be_viewed_by(self, user):
if self.is_club_page:
return True
return False
return self.is_club_page
def get_parent_list(self):
l = []
p = self.parent
while p is not None:
l.append(p)
p = p.parent
return l
parents = []
current = self.parent
while current is not None:
parents.append(current)
current = current.parent
return parents
def is_locked(self):
"""Is True if the page is locked, False otherwise.
@ -1386,7 +1368,6 @@ class Page(models.Model):
if self.lock_timeout and (
timezone.now() - self.lock_timeout > timedelta(minutes=5)
):
# print("Lock timed out")
self.unset_lock()
return (
self.lock_user
@ -1401,7 +1382,6 @@ class Page(models.Model):
self.lock_user = user
self.lock_timeout = timezone.now()
super().save()
# print("Locking page")
def set_lock_recursive(self, user):
"""Locks recursively all the child pages for editing properties."""
@ -1420,7 +1400,6 @@ class Page(models.Model):
self.lock_user = None
self.lock_timeout = None
super().save()
# print("Unlocking page")
def get_lock(self):
"""Returns the page's mutex containing the time and the user in a dict."""
@ -1435,13 +1414,11 @@ class Page(models.Model):
"""
if self.parent is None:
return self.name
return "/".join([self.parent.get_full_name(), self.name])
return f"{self.parent.get_full_name()}/{self.name}"
def get_display_name(self):
try:
return self.revisions.last().title
except:
return self.name
rev = self.revisions.last()
return rev.title if rev is not None else self.name
@cached_property
def is_club_page(self):

View File

@ -1,265 +0,0 @@
/**
* Builders to use Select2 in our templates.
*
* This comes with two flavours : local data or remote data.
*
* # Local data source
*
* To use local data source, you must define an array
* in your JS code, having the fields `id` and `text`.
*
* ```js
* const data = [
* {id: 1, text: "foo"},
* {id: 2, text: "bar"},
* ];
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* dataSource: localDataSource(data)
* }));
* ```
*
* You can also define a callback that return ids to exclude :
*
* ```js
* const data = [
* {id: 1, text: "foo"},
* {id: 2, text: "bar"},
* {id: 3, text: "to exclude"},
* ];
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* dataSource: localDataSource(data, {
* excluded: () => data.filter((i) => i.text === "to exclude").map((i) => parseInt(i))
* })
* }));
* ```
*
* # Remote data source
*
* Select2 with remote data sources are similar to those with local
* data, but with some more parameters, like `resultConverter`,
* which takes a callback that must return a `Select2Object`.
*
* ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* dataSource: remoteDataSource("/api/user/search", {
* excluded: () => [1, 2], // exclude users 1 and 2 from the search
* resultConverter: (user) => Object({id: user.id, text: user.firstName})
* })
* }));
* ```
*
* # Overrides
*
* Dealing with a select2 may be complex.
* That's why, when defining a select,
* you may add an override parameter,
* in which you can declare any parameter defined in the
* Select2 documentation.
*
* ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* dataSource: remoteDataSource("/api/user/search", {
* resultConverter: (user) => Object({id: user.id, text: user.firstName}),
* overrides: {
* delay: 500
* }
* })
* }));
* ```
*
* # Caveats with exclude
*
* With local data source, select2 evaluates the data only once.
* Thus, modify the exclude after the initialisation is a no-op.
*
* With remote data source, the exclude list will be evaluated
* after each api response.
* It makes it possible to bind the data returned by the callback
* to some reactive data, thus making the exclude list dynamic.
*
* # Images
*
* Sometimes, you would like to display an image besides
* the text on the select items.
* In this case, fill the `pictureGetter` option :
*
* ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"),
* dataSource: remoteDataSource("/api/user/search", {
* resultConverter: (user) => Object({id: user.id, text: user.firstName})
* })
* pictureGetter: (user) => user.profilePict,
* }));
* ```
*
* # Binding with alpine
*
* You can declare your select2 component in an Alpine data.
*
* ```html
* <body>
* <div x-data="select2_test">
* <select x-ref="search" x-ref="select"></select>
* <p x-text="currentSelection.id"></p>
* <p x-text="currentSelection.text"></p>
* </div>
* </body>
*
* <script>
* document.addEventListener("alpine:init", () => {
* Alpine.data("select2_test", () => ({
* selector: undefined,
* currentSelect: {id: "", text: ""},
*
* init() {
* this.selector = sithSelect2({
* element: $(this.$refs.select),
* dataSource: localDataSource(
* [{id: 1, text: "foo"}, {id: 2, text: "bar"}]
* ),
* });
* this.selector.on("select2:select", (event) => {
* // select2 => Alpine signals here
* this.currentSelect = this.selector.select2("data")
* });
* this.$watch("currentSelected" (value) => {
* // Alpine => select2 signals here
* });
* },
* }));
* })
* </script>
*/
/**
* @typedef Select2Object
* @property {number} id
* @property {string} text
*/
/**
* @typedef Select2Options
* @property {Element} element
* @property {Object} dataSource
* the data source, built with `localDataSource` or `remoteDataSource`
* @property {number[]} excluded A list of ids to exclude from search
* @property {undefined | function(Object): string} pictureGetter
* A callback to get the picture field from the API response
* @property {Object | undefined} overrides
* Any other select2 parameter to apply on the config
*/
/**
* @param {Select2Options} options
*/
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function sithSelect2(options) {
const elem = $(options.element);
return elem.select2({
theme: elem[0].multiple ? "classic" : "default",
minimumInputLength: 2,
templateResult: selectItemBuilder(options.pictureGetter),
...options.dataSource,
...(options.overrides || {}),
});
}
/**
* @typedef LocalSourceOptions
* @property {undefined | function(): number[]} excluded
* A callback to the ids to exclude from the search
*/
/**
* Build a data source for a Select2 from a local array
* @param {Select2Object[]} source The array containing the data
* @param {RemoteSourceOptions} options
*/
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function localDataSource(source, options) {
if (options.excluded) {
const ids = options.excluded();
return { data: source.filter((i) => !ids.includes(i.id)) };
}
return { data: source };
}
/**
* @typedef RemoteSourceOptions
* @property {undefined | function(): number[]} excluded
* A callback to the ids to exclude from the search
* @property {undefined | function(): Select2Object} resultConverter
* A converter for a value coming from the remote api
* @property {undefined | Object} overrides
* Any other select2 parameter to apply on the config
*/
/**
* Build a data source for a Select2 from a remote url
* @param {string} source The url of the endpoint
* @param {RemoteSourceOptions} options
*/
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function remoteDataSource(source, options) {
jQuery.ajaxSettings.traditional = true;
const params = {
url: source,
dataType: "json",
cache: true,
delay: 250,
data: function (params) {
return {
search: params.term,
exclude: [
...(this.val() || []).map((i) => Number.parseInt(i)),
...(options.excluded ? options.excluded() : []),
],
};
},
};
if (options.resultConverter) {
params.processResults = (data) => ({
results: data.results.map(options.resultConverter),
});
}
if (options.overrides) {
Object.assign(params, options.overrides);
}
return { ajax: params };
}
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function itemFormatter(user) {
if (user.loading) {
return user.text;
}
}
/**
* Build a function to display the results
* @param {null | function(Object):string} pictureGetter
* @return {function(string): jQuery|HTMLElement}
*/
function selectItemBuilder(pictureGetter) {
return (item) => {
const picture = typeof pictureGetter === "function" ? pictureGetter(item) : null;
const imgHtml = picture
? `<img
src="${pictureGetter(item)}"
alt="${item.text}"
onerror="this.src = '/static/core/img/unknown.jpg'"
/>`
: "";
return $(`<div class="select-item">
${imgHtml}
<span class="select-item-text">${item.text}</span>
</div>`);
};
}

View File

@ -28,6 +28,7 @@ input[type="file"] {
font-size: 1.2em;
border-radius: 5px;
color: black;
&:hover {
background: hsl(0, 0%, 83%);
}
@ -63,6 +64,7 @@ textarea[type="text"],
border-radius: 5px;
max-width: 95%;
}
textarea {
border: none;
text-decoration: none;
@ -72,6 +74,7 @@ textarea {
border-radius: 5px;
font-family: sans-serif;
}
select {
border: none;
text-decoration: none;
@ -85,9 +88,11 @@ select {
a:not(.button) {
text-decoration: none;
color: $primary-dark-color;
&:hover {
color: $primary-light-color;
}
&:active {
color: $primary-color;
}
@ -116,7 +121,9 @@ a:not(.button) {
}
@keyframes rotate {
100% { transform: rotate(360deg); }
100% {
transform: rotate(360deg);
}
}
.ib {
@ -143,11 +150,13 @@ a:not(.button) {
.collapse-header-icon {
transition: all ease-in-out 150ms;
&.reverse {
transform: rotate(180deg);
}
}
}
.collapse-body {
padding: 10px;
}
@ -202,9 +211,11 @@ a:not(.button) {
font-size: 0.9em;
margin: 0.2em;
border-radius: 0.6em;
.markdown {
margin: 0.5em;
}
&:before {
font-family: FontAwesome;
font-size: 4em;
@ -212,15 +223,19 @@ a:not(.button) {
margin: 0.2em;
}
}
#info_box {
background: $primary-neutral-light-color;
&:before {
content: "\f05a";
color: hsl(210, 100%, 56%);
}
}
#alert_box {
background: $second-color;
&:before {
content: "\f06a";
color: $white-color;
@ -240,6 +255,7 @@ a:not(.button) {
#page {
width: 90%;
margin: 20px auto 0;
/*---------------------------------NAV---------------------------------*/
.btn {
font-size: 15px;
@ -252,9 +268,11 @@ a:not(.button) {
&.btn-blue {
background-color: $deepblue;
&:not(:disabled):hover {
background-color: darken($deepblue, 10%);
}
&:disabled {
background-color: rgba(70, 90, 126, 0.4);
}
@ -262,9 +280,11 @@ a:not(.button) {
&.btn-grey {
background-color: grey;
&:not(:disabled):hover {
background-color: darken(gray, 15%);
}
&:disabled {
background-color: lighten(gray, 15%);
}
@ -273,9 +293,11 @@ a:not(.button) {
&.btn-red {
background-color: #fc8181;
color: black;
&:not(:disabled):hover {
background-color: darken(#fc8181, 15%);
}
&:disabled {
background-color: lighten(#fc8181, 15%);
color: grey;
@ -293,6 +315,7 @@ a:not(.button) {
margin: 0 auto;
list-style-type: none;
background: $second-color;
li {
padding: 10px;
}
@ -333,14 +356,24 @@ a:not(.button) {
border: #fc8181 1px solid;
}
.alert-title {
margin-top: 0;
}
.alert-main {
flex: 2;
}
.alert-aside {
display: flex;
flex-direction: column;
}
}
.tool_bar {
overflow: auto;
padding: 4px;
.tools {
display: flex;
flex-wrap: wrap;
@ -349,6 +382,7 @@ a:not(.button) {
padding: 5px;
border-radius: 6px;
text-align: center;
a {
padding: 7px;
display: inline-block;
@ -358,11 +392,13 @@ a:not(.button) {
flex: 1;
flex-wrap: nowrap;
white-space: nowrap;
&.selected_tab {
background: $primary-color;
color: $white-color;
border-radius: 6px;
}
&:hover {
background: $primary-color;
color: $white-color;
@ -385,17 +421,21 @@ a:not(.button) {
margin: 0;
vertical-align: top;
}
#news_admin {
margin-bottom: 1em;
}
#right_column {
flex: 20%;
float: right;
margin: 0.2em;
}
#left_column {
flex: 79%;
margin: 0.2em;
h3 {
background: $second-color;
box-shadow: $shadow-color 1px 1px 1px;
@ -403,12 +443,15 @@ a:not(.button) {
margin: 0 0 0.5em 0;
text-transform: uppercase;
font-size: 1.1em;
&:not(:first-of-type) {
margin: 2em 0 1em 0;
}
}
}
@media screen and (max-width: $small-devices) {
#left_column,
#right_column {
flex: 100%;
@ -423,6 +466,7 @@ a:not(.button) {
background: white;
font-size: 70%;
margin-bottom: 1em;
#agenda_title,
#birthdays_title {
margin: 0;
@ -435,39 +479,48 @@ a:not(.button) {
text-transform: uppercase;
background: $second-color;
}
#agenda_content {
overflow: auto;
box-shadow: $shadow-color 1px 1px 1px;
height: 20em;
}
#agenda_content,
#birthdays_content {
.agenda_item {
padding: 0.5em;
margin-bottom: 0.5em;
&:nth-of-type(even) {
background: $secondary-neutral-light-color;
}
.agenda_time {
font-size: 90%;
color: grey;
}
.agenda_item_content {
p {
margin-top: 0.2em;
}
}
}
ul.birthdays_year {
margin: 0;
list-style-type: none;
font-weight: bold;
>li {
padding: 0.5em;
&:nth-child(even) {
background: $secondary-neutral-light-color;
}
}
ul {
margin: 0;
margin-left: 1em;
@ -478,6 +531,7 @@ a:not(.button) {
}
}
}
/* END AGENDA/BIRTHDAYS */
/* EVENTS TODAY AND NEXT FEW DAYS */
@ -485,6 +539,7 @@ a:not(.button) {
box-shadow: $shadow-color 1px 1px 1px;
margin-left: 1em;
margin-bottom: 0.5em;
.news_events_group_date {
display: table-cell;
padding: 0.6em;
@ -500,33 +555,42 @@ a:not(.button) {
div {
margin: 0 auto;
.day {
font-size: 1.5em;
}
}
}
.news_events_group_items {
display: table-cell;
width: 100%;
.news_event:nth-of-type(odd) {
background: white;
}
.news_event:nth-of-type(even) {
background: $primary-neutral-light-color;
}
.news_event {
display: block;
padding: 0.4em;
&:not(:last-child) {
border-bottom: 1px solid grey;
}
div {
margin: 0.2em;
}
h4 {
margin-top: 1em;
text-transform: uppercase;
}
.club_logo {
float: left;
min-width: 7em;
@ -534,6 +598,7 @@ a:not(.button) {
margin: 0;
margin-right: 1em;
margin-top: 0.8em;
img {
max-height: 6em;
max-width: 8em;
@ -541,16 +606,21 @@ a:not(.button) {
margin: 0 auto;
}
}
.news_date {
font-size: 100%;
}
.news_content {
clear: left;
.button_bar {
text-align: right;
.fb {
color: $faceblue;
}
.twitter {
color: $twitblue;
}
@ -559,6 +629,7 @@ a:not(.button) {
}
}
}
/* END EVENTS TODAY AND NEXT FEW DAYS */
/* COMING SOON */
@ -568,14 +639,17 @@ a:not(.button) {
list-style-position: inside;
margin-left: 1em;
padding-left: 0;
a {
font-weight: bold;
text-transform: uppercase;
}
.news_date {
font-size: 0.9em;
}
}
/* END COMING SOON */
/* NOTICES */
@ -586,13 +660,16 @@ a:not(.button) {
background: $secondary-neutral-light-color;
box-shadow: $shadow-color 0 0 2px;
border-radius: 18px 5px 18px 5px;
h4 {
margin: 0;
}
.news_content {
margin-left: 1em;
}
}
/* END NOTICES */
/* CALLS */
@ -603,21 +680,26 @@ a:not(.button) {
background: $secondary-neutral-light-color;
border: 1px solid grey;
box-shadow: $shadow-color 1px 1px 1px;
h4 {
margin: 0;
}
.news_date {
font-size: 0.9em;
}
.news_content {
margin-left: 1em;
}
}
/* END CALLS */
.news_empty {
margin-left: 1em;
}
.news_date {
color: grey;
}
@ -631,7 +713,7 @@ a:not(.button) {
}
.select2 {
.tomselected {
margin: 10px 0 !important;
max-width: 100%;
min-width: 100%;
@ -648,7 +730,9 @@ a:not(.button) {
color: black;
}
}
.select2-results {
.ts-dropdown {
.select-item {
display: flex;
flex-direction: row;
@ -664,16 +748,39 @@ a:not(.button) {
}
}
.ts-control {
.item {
.fa-times {
margin-left: 5px;
margin-right: 5px;
}
cursor: pointer;
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
display: inline-block;
margin-left: 5px;
margin-top: 5px;
margin-bottom: 5px;
padding-right: 10px;
}
}
#news_details {
display: inline-block;
margin-top: 20px;
padding: 0.4em;
width: 80%;
background: $white-color;
h4 {
margin-top: 1em;
text-transform: uppercase;
}
.club_logo {
display: inline-block;
text-align: center;
@ -681,6 +788,7 @@ a:not(.button) {
float: left;
min-width: 15em;
margin: 0;
img {
max-height: 15em;
max-width: 12em;
@ -689,6 +797,7 @@ a:not(.button) {
margin-bottom: 10px;
}
}
.share_button {
border: none;
color: white;
@ -700,6 +809,7 @@ a:not(.button) {
float: right;
display: block;
margin-left: 0.3em;
&:hover {
color: lightgrey;
}
@ -731,26 +841,32 @@ a:not(.button) {
#poster_edit,
#screen_edit {
position: relative;
#title {
position: relative;
padding: 10px;
margin: 10px;
border-bottom: 2px solid black;
h3 {
display: flex;
justify-content: center;
align-items: center;
}
#links {
position: absolute;
display: flex;
bottom: 5px;
&.left {
left: 0;
}
&.right {
right: 0;
}
.link {
padding: 5px;
padding-left: 20px;
@ -759,27 +875,32 @@ a:not(.button) {
border-radius: 20px;
background-color: hsl(40, 100%, 50%);
color: black;
&:hover {
color: black;
background-color: hsl(40, 58%, 50%);
}
&.delete {
background-color: hsl(0, 100%, 40%);
}
}
}
}
#posters,
#screens {
position: relative;
display: flex;
flex-wrap: wrap;
#no-posters,
#no-screens {
display: flex;
justify-content: center;
align-items: center;
}
.poster,
.screen {
min-width: 10%;
@ -791,26 +912,31 @@ a:not(.button) {
border-radius: 4px;
padding: 10px;
background-color: lightgrey;
* {
display: flex;
justify-content: center;
align-items: center;
}
.name {
padding-bottom: 5px;
margin-bottom: 5px;
border-bottom: 1px solid whitesmoke;
}
.image {
flex-grow: 1;
position: relative;
padding-bottom: 5px;
margin-bottom: 5px;
border-bottom: 1px solid whitesmoke;
img {
max-height: 20vw;
max-width: 100%;
}
&:hover {
&::before {
position: absolute;
@ -829,10 +955,12 @@ a:not(.button) {
}
}
}
.dates {
padding-bottom: 5px;
margin-bottom: 5px;
border-bottom: 1px solid whitesmoke;
* {
display: flex;
justify-content: center;
@ -841,15 +969,18 @@ a:not(.button) {
margin-left: 5px;
margin-right: 5px;
}
.begin,
.end {
width: 48%;
}
.begin {
border-right: 1px solid whitesmoke;
padding-right: 2%;
}
}
.edit,
.moderate,
.slideshow {
@ -857,15 +988,18 @@ a:not(.button) {
border-radius: 20px;
background-color: hsl(40, 100%, 50%);
color: black;
&:hover {
color: black;
background-color: hsl(40, 58%, 50%);
}
&:nth-child(2n) {
margin-top: 5px;
margin-bottom: 5px;
}
}
.tooltip {
visibility: hidden;
width: 120px;
@ -876,23 +1010,28 @@ a:not(.button) {
border-radius: 6px;
position: absolute;
z-index: 10;
ul {
margin-left: 0;
display: inline-block;
li {
display: list-item;
list-style-type: none;
}
}
}
&.not_moderated {
border: 1px solid red;
}
&:hover .tooltip {
visibility: visible;
}
}
}
#view {
position: fixed;
width: 100vw;
@ -906,9 +1045,11 @@ a:not(.button) {
visibility: hidden;
background-color: rgba(10, 10, 10, 0.9);
overflow: hidden;
&.active {
visibility: visible;
}
#placeholder {
width: 80vw;
height: 80vh;
@ -917,6 +1058,7 @@ a:not(.button) {
align-items: center;
top: 0;
left: 0;
img {
max-width: 100%;
max-height: 100%;
@ -931,14 +1073,17 @@ a:not(.button) {
tbody {
.neg-amount {
color: red;
&:before {
font-family: FontAwesome;
font-size: 1em;
content: "\f063";
}
}
.pos-amount {
color: green;
&:before {
font-family: FontAwesome;
font-size: 1em;
@ -1005,6 +1150,7 @@ dt {
.edit-bar {
display: block;
margin: 4px;
a {
display: inline-block;
margin: 4px;
@ -1044,6 +1190,7 @@ th {
vertical-align: middle;
text-align: center;
padding: 5px 10px;
>ul {
margin-top: 0;
}
@ -1055,6 +1202,7 @@ td {
vertical-align: top;
overflow: hidden;
text-overflow: ellipsis;
>ul {
margin-top: 0;
}
@ -1075,11 +1223,13 @@ tbody > tr {
&:nth-child(even):not(.highlight) {
background: $primary-neutral-light-color;
}
&.clickable:hover {
cursor: pointer;
background: $secondary-neutral-light-color;
width: 100%;
}
&.highlight {
color: $primary-dark-color;
font-style: italic;
@ -1139,9 +1289,11 @@ u,
margin: 0.2em;
height: 100%;
background: $secondary-neutral-light-color;
img {
max-width: 70%;
}
input {
background: white;
}
@ -1153,10 +1305,12 @@ u,
.user_mini_profile {
height: 100%;
width: 100%;
img {
max-width: 100%;
max-height: 100%;
}
.user_mini_profile_infos {
padding: 0.2em;
height: 20%;
@ -1164,16 +1318,20 @@ u,
flex-wrap: nowrap;
justify-content: space-around;
font-size: 0.9em;
div {
max-height: 100%;
}
.user_mini_profile_infos_text {
text-align: center;
.user_mini_profile_nick {
font-style: italic;
}
}
}
.user_mini_profile_picture {
height: 80%;
display: flex;
@ -1185,14 +1343,17 @@ u,
.mini_profile_link {
display: block;
text-decoration: none;
span {
display: inline-block;
width: 50px;
vertical-align: middle;
}
em {
vertical-align: middle;
}
img {
max-width: 40px;
max-height: 60px;
@ -1214,6 +1375,7 @@ u,
border: solid 1px red;
text-align: center;
}
img {
width: 500px;
}
@ -1223,6 +1385,7 @@ u,
.matmat_results {
display: flex;
flex-wrap: wrap;
.matmat_user {
flex-basis: 14em;
align-self: flex-start;
@ -1231,10 +1394,12 @@ u,
overflow: hidden;
border: 1px solid black;
box-shadow: $shadow-color 1px 1px 1px;
&:hover {
box-shadow: 1px 1px 5px $second-color;
}
}
.matmat_user a {
color: $primary-neutral-dark-color;
height: 100%;
@ -1274,6 +1439,7 @@ footer {
font-size: 90%;
text-align: center;
vertical-align: middle;
div {
margin: 0.6em 0;
color: $white-color;
@ -1283,11 +1449,13 @@ footer {
align-items: center;
background-color: $primary-neutral-dark-color;
box-shadow: $shadow-color 0 0 15px;
a {
padding: 0.8em;
flex: 1;
font-weight: bold;
color: $white-color !important;
&:hover {
color: $primary-dark-color;
}
@ -1326,6 +1494,7 @@ label {
* {
text-align: center;
}
img {
width: 100px;
}
@ -1342,19 +1511,23 @@ label {
padding: 2px;
display: inline-block;
font-size: 0.8em;
span {
width: 70px;
float: right;
}
img {
max-width: 50px;
max-height: 50px;
float: left;
}
strong {
font-weight: bold;
font-size: 1.2em;
}
button {
vertical-align: middle;
}
@ -1371,6 +1544,7 @@ a.ui-button:active,
background: $primary-color;
border-color: $primary-color;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-right,
@ -1382,6 +1556,7 @@ a.ui-button:active,
#club_detail {
.club_logo {
float: right;
img {
display: block;
max-height: 10em;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
!function(n,e,r,t,i,o,a,c,s){for(var u=s,f=0;f<document.scripts.length;f++)if(document.scripts[f].src.indexOf(o)>-1){u&&"no"===document.scripts[f].getAttribute("data-lazy")&&(u=!1);break}var p=[];function l(n){return"e"in n}function d(n){return"p"in n}function _(n){return"f"in n}var v=[];function y(n){u&&(l(n)||d(n)||_(n)&&n.f.indexOf("capture")>-1||_(n)&&n.f.indexOf("showReportDialog")>-1)&&m(),v.push(n)}function g(){y({e:[].slice.call(arguments)})}function h(n){y({p:n})}function E(){try{n.SENTRY_SDK_SOURCE="loader";var e=n[i],o=e.init;e.init=function(i){n.removeEventListener(r,g),n.removeEventListener(t,h);var a=c;for(var s in i)Object.prototype.hasOwnProperty.call(i,s)&&(a[s]=i[s]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&(e.browserTracingIntegration?r.push(e.browserTracingIntegration({enableInp:!0})):e.BrowserTracing&&r.push(new e.BrowserTracing));(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&(e.replayIntegration?r.push(e.replayIntegration()):e.Replay&&r.push(new e.Replay));n.integrations=r}(a,e),o(a)},setTimeout((function(){return function(e){try{"function"==typeof n.sentryOnLoad&&(n.sentryOnLoad(),n.sentryOnLoad=void 0);for(var r=0;r<p.length;r++)"function"==typeof p[r]&&p[r]();p.splice(0);for(r=0;r<v.length;r++){_(o=v[r])&&"init"===o.f&&e.init.apply(e,o.a)}L()||e.init();var t=n.onerror,i=n.onunhandledrejection;for(r=0;r<v.length;r++){var o;if(_(o=v[r])){if("init"===o.f)continue;e[o.f].apply(e,o.a)}else l(o)&&t?t.apply(n,o.e):d(o)&&i&&i.apply(n,[o.p])}}catch(n){console.error(n)}}(e)}))}catch(n){console.error(n)}}var O=!1;function m(){if(!O){O=!0;var n=e.scripts[0],r=e.createElement("script");r.src=a,r.crossOrigin="anonymous",r.addEventListener("load",E,{once:!0,passive:!0}),n.parentNode.insertBefore(r,n)}}function L(){var e=n.__SENTRY__,r=void 0!==e&&e.version;return r?!!e[r]:!(void 0===e||!e.hub||!e.hub.getClient())}n[i]=n[i]||{},n[i].onLoad=function(n){L()?n():p.push(n)},n[i].forceLoad=function(){setTimeout((function(){m()}))},["init","addBreadcrumb","captureMessage","captureException","captureEvent","configureScope","withScope","showReportDialog"].forEach((function(e){n[i][e]=function(){y({f:e,a:arguments})}})),n.addEventListener(r,g),n.addEventListener(t,h),u||setTimeout((function(){m()}))}(window,document,"error","unhandledrejection","Sentry",'ab63c6820882cab2883218a4b9deba4d','https://browser.sentry-cdn.com/8.26.0/bundle.min.js',{"dsn":"https://ab63c6820882cab2883218a4b9deba4d@o4505360748642304.ingest.us.sentry.io/4507633486266368"},true);

View File

@ -0,0 +1,93 @@
import "tom-select/dist/css/tom-select.css";
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components";
import TomSelect from "tom-select";
import type { TomItem, TomLoadCallback, TomOption } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/dist/types/utils";
import { type UserProfileSchema, userSearchUsers } from "#openapi";
@registerComponent("ajax-select")
export class AjaxSelect extends inheritHtmlElement("select") {
public widget: TomSelect;
public filter?: <T>(items: T[]) => T[];
constructor() {
super();
window.addEventListener("DOMContentLoaded", () => {
this.loadTomSelect();
});
}
loadTomSelect() {
const minCharNumberForSearch = 2;
let maxItems = 1;
if (this.node.multiple) {
maxItems = Number.parseInt(this.node.dataset.max) ?? null;
}
this.widget = new TomSelect(this.node, {
hideSelected: true,
diacritics: true,
duplicates: false,
maxItems: maxItems,
loadThrottle: Number.parseInt(this.node.dataset.delay) ?? null,
valueField: "id",
labelField: "display_name",
searchField: ["display_name", "nick_name", "first_name", "last_name"],
placeholder: this.node.dataset.placeholder ?? "",
shouldLoad: (query: string) => {
return query.length >= minCharNumberForSearch; // Avoid launching search with less than 2 characters
},
load: (query: string, callback: TomLoadCallback) => {
userSearchUsers({
query: {
search: query,
},
}).then((response) => {
if (response.data) {
if (this.filter) {
callback(this.filter(response.data.results), []);
} else {
callback(response.data.results, []);
}
return;
}
callback([], []);
});
},
render: {
option: (item: UserProfileSchema, sanitize: typeof escape_html) => {
return `<div class="select-item">
<img
src="${sanitize(item.profile_pict)}"
alt="${sanitize(item.display_name)}"
onerror="this.src = '/static/core/img/unknown.jpg'"
/>
<span class="select-item-text">${sanitize(item.display_name)}</span>
</div>`;
},
item: (item: UserProfileSchema, sanitize: typeof escape_html) => {
return `<span><i class="fa fa-times"></i>${sanitize(item.display_name)}</span>`;
},
// biome-ignore lint/style/useNamingConvention: that's how it's defined
not_loading: (data: TomOption, _sanitize: typeof escape_html) => {
return `<div class="no-results">${interpolate(gettext("You need to type %(number)s more characters"), { number: minCharNumberForSearch - data.input.length }, true)}</div>`;
},
// biome-ignore lint/style/useNamingConvention: that's how it's defined
no_results: (_data: TomOption, _sanitize: typeof escape_html) => {
return `<div class="no-results">${gettext("No results found")}</div>`;
},
},
});
// Allow removing selected items by clicking on them
this.widget.on("item_select", (item: TomItem) => {
this.widget.removeItem(item);
});
// Remove typed text once an item has been selected
this.widget.on("item_add", () => {
this.widget.setTextboxValue("");
});
}
}

View File

@ -1,53 +1,57 @@
// biome-ignore lint/correctness/noUndeclaredDependencies: shipped by easymde
import "codemirror/lib/codemirror.css";
import "easymde/src/css/easymde.css";
import easyMde from "easymde";
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components";
// biome-ignore lint/correctness/noUndeclaredDependencies: Imported by EasyMDE
import type CodeMirror from "codemirror";
// biome-ignore lint/style/useNamingConvention: This is how they called their namespace
import EasyMDE from "easymde";
import { markdownRenderMarkdown } from "#openapi";
/**
* Create a new easymde based textarea
* @param {HTMLTextAreaElement} textarea to use
**/
window.easymdeFactory = (textarea) => {
const easymde = new easyMde({
const loadEasyMde = (textarea: HTMLTextAreaElement) => {
new EasyMDE({
element: textarea,
spellChecker: false,
autoDownloadFontAwesome: false,
previewRender: Alpine.debounce(async (plainText, preview) => {
previewRender: Alpine.debounce((plainText: string, preview: MarkdownInput) => {
const func = async (plainText: string, preview: MarkdownInput): Promise<null> => {
preview.innerHTML = (
await markdownRenderMarkdown({ body: { text: plainText } })
).data;
).data as string;
return null;
};
func(plainText, preview);
return null;
}, 300),
forceSync: true, // Avoid validation error on generic create view
toolbar: [
{
name: "heading-smaller",
action: easyMde.toggleHeadingSmaller,
action: EasyMDE.toggleHeadingSmaller,
className: "fa fa-header",
title: gettext("Heading"),
},
{
name: "italic",
action: easyMde.toggleItalic,
action: EasyMDE.toggleItalic,
className: "fa fa-italic",
title: gettext("Italic"),
},
{
name: "bold",
action: easyMde.toggleBold,
action: EasyMDE.toggleBold,
className: "fa fa-bold",
title: gettext("Bold"),
},
{
name: "strikethrough",
action: easyMde.toggleStrikethrough,
action: EasyMDE.toggleStrikethrough,
className: "fa fa-strikethrough",
title: gettext("Strikethrough"),
},
{
name: "underline",
action: function customFunction(editor) {
action: function customFunction(editor: { codemirror: CodeMirror.Editor }) {
const cm = editor.codemirror;
cm.replaceSelection(`__${cm.getSelection()}__`);
},
@ -56,7 +60,7 @@ window.easymdeFactory = (textarea) => {
},
{
name: "superscript",
action: function customFunction(editor) {
action: function customFunction(editor: { codemirror: CodeMirror.Editor }) {
const cm = editor.codemirror;
cm.replaceSelection(`^${cm.getSelection()}^`);
},
@ -65,7 +69,7 @@ window.easymdeFactory = (textarea) => {
},
{
name: "subscript",
action: function customFunction(editor) {
action: function customFunction(editor: { codemirror: CodeMirror.Editor }) {
const cm = editor.codemirror;
cm.replaceSelection(`~${cm.getSelection()}~`);
},
@ -74,71 +78,71 @@ window.easymdeFactory = (textarea) => {
},
{
name: "code",
action: easyMde.toggleCodeBlock,
action: EasyMDE.toggleCodeBlock,
className: "fa fa-code",
title: gettext("Code"),
},
"|",
{
name: "quote",
action: easyMde.toggleBlockquote,
action: EasyMDE.toggleBlockquote,
className: "fa fa-quote-left",
title: gettext("Quote"),
},
{
name: "unordered-list",
action: easyMde.toggleUnorderedList,
action: EasyMDE.toggleUnorderedList,
className: "fa fa-list-ul",
title: gettext("Unordered list"),
},
{
name: "ordered-list",
action: easyMde.toggleOrderedList,
action: EasyMDE.toggleOrderedList,
className: "fa fa-list-ol",
title: gettext("Ordered list"),
},
"|",
{
name: "link",
action: easyMde.drawLink,
action: EasyMDE.drawLink,
className: "fa fa-link",
title: gettext("Insert link"),
},
{
name: "image",
action: easyMde.drawImage,
action: EasyMDE.drawImage,
className: "fa-regular fa-image",
title: gettext("Insert image"),
},
{
name: "table",
action: easyMde.drawTable,
action: EasyMDE.drawTable,
className: "fa fa-table",
title: gettext("Insert table"),
},
"|",
{
name: "clean-block",
action: easyMde.cleanBlock,
action: EasyMDE.cleanBlock,
className: "fa fa-eraser fa-clean-block",
title: gettext("Clean block"),
},
"|",
{
name: "preview",
action: easyMde.togglePreview,
action: EasyMDE.togglePreview,
className: "fa fa-eye no-disable",
title: gettext("Toggle preview"),
},
{
name: "side-by-side",
action: easyMde.toggleSideBySide,
action: EasyMDE.toggleSideBySide,
className: "fa fa-columns no-disable no-mobile",
title: gettext("Toggle side by side"),
},
{
name: "fullscreen",
action: easyMde.toggleFullScreen,
action: EasyMDE.toggleFullScreen,
className: "fa fa-expand no-mobile",
title: gettext("Toggle fullscreen"),
},
@ -152,27 +156,25 @@ window.easymdeFactory = (textarea) => {
],
});
const submits = textarea.closest("form").querySelectorAll('input[type="submit"]');
const parentDiv = textarea.parentElement;
let submitPressed = false;
const submits: HTMLInputElement[] = Array.from(
textarea.closest("form").querySelectorAll('input[type="submit"]'),
);
const parentDiv = textarea.parentElement.parentElement;
function checkMarkdownInput() {
function checkMarkdownInput(event: Event) {
// an attribute is null if it does not exist, else a string
const required = textarea.getAttribute("required") != null;
const length = textarea.value.trim().length;
if (required && length === 0) {
parentDiv.style.boxShadow = "red 0px 0px 1.5px 1px";
event.preventDefault();
} else {
parentDiv.style.boxShadow = "";
}
}
function onSubmitClick(e) {
if (!submitPressed) {
easymde.codemirror.on("change", checkMarkdownInput);
}
submitPressed = true;
function onSubmitClick(e: Event) {
checkMarkdownInput(e);
}
@ -180,3 +182,11 @@ window.easymdeFactory = (textarea) => {
submit.addEventListener("click", onSubmitClick);
}
};
@registerComponent("markdown-input")
class MarkdownInput extends inheritHtmlElement("textarea") {
constructor() {
super();
window.addEventListener("DOMContentLoaded", () => loadEasyMde(this.node));
}
}

View File

@ -0,0 +1,24 @@
import { exportToHtml } from "#core:utils/globals";
// biome-ignore lint/style/noNamespaceImport: this is the recommended way from the documentation
import * as Sentry from "@sentry/browser";
interface LoggedUser {
name: string;
email: string;
}
interface SentryOptions {
dsn: string;
eventId: string;
user?: LoggedUser;
}
exportToHtml("loadSentryPopup", (options: SentryOptions) => {
Sentry.init({
dsn: options.dsn,
});
Sentry.showReportDialog({
eventId: options.eventId,
...(options.user ?? {}),
});
});

View File

@ -0,0 +1,21 @@
import type { Alpine as AlpineType } from "alpinejs";
declare global {
const Alpine: AlpineType;
const gettext: (text: string) => string;
const interpolate: <T>(fmt: string, args: string[] | T, isNamed?: boolean) => string;
}
/**
* Helper function to export typescript functions to regular html and jinja files
* Without it, you either have to use the any keyword and suppress warnings or do a
* very painful type conversion workaround which is only here to please the linter
*
* This is only useful if you're using typescript, this is equivalent to doing
* window.yourFunction = yourFunction
**/
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
export function exportToHtml(name: string, func: any) {
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
(window as any)[name] = func;
}

View File

@ -0,0 +1,50 @@
/**
* Class decorator to register components easily
* It's a wrapper around window.customElements.define
* What's nice about it is that you don't separate the component registration
* and the class definition
**/
export function registerComponent(name: string, options?: ElementDefinitionOptions) {
return (component: CustomElementConstructor) => {
window.customElements.define(name, component, options);
};
}
/**
* Safari doesn't support inheriting from HTML tags on web components
* The technique is to:
* create a new web component
* create the desired type inside
* pass all attributes to the child component
* store is at as `node` inside the parent
*
* Since we can't use the generic type to instantiate the node, we create a generator function
*
* ```js
* class MyClass extends inheritHtmlElement("select") {
* // do whatever
* }
* ```
**/
export function inheritHtmlElement<K extends keyof HTMLElementTagNameMap>(tagName: K) {
return class Inherited extends HTMLElement {
protected node: HTMLElementTagNameMap[K];
constructor() {
super();
this.node = document.createElement(tagName);
const attributes: Attr[] = []; // We need to make a copy to delete while iterating
for (const attr of this.attributes) {
if (attr.name in this.node) {
attributes.push(attr);
}
}
for (const attr of attributes) {
this.removeAttributeNode(attr);
this.node.setAttributeNode(attr);
}
this.appendChild(this.node);
}
};
}

View File

@ -1,26 +1,26 @@
{% extends "core/base.jinja" %}
{% block head %}
{{ super() }}
<script
src="{{ static('vendored/sentry/bundle.min.js') }}"
crossorigin="anonymous"
></script>
{% endblock head %}
{% block additional_js %}
{% if settings.SENTRY_DSN %}
<script src="{{ static('webpack/sentry-popup-index.ts') }}" defer ></script>
{% endif %}
{% endblock additional_js %}
{% block content %}
<h3>{% trans %}500, Server Error{% endtrans %}</h3>
{% if settings.SENTRY_DSN %}
<script>
Sentry.init({ dsn: '{{ settings.SENTRY_DSN }}' });
Sentry.showReportDialog({
eventId: '{{ request.sentry_last_event_id() }}',
window.addEventListener("DOMContentLoaded", () => {
loadSentryPopup({
dsn: "{{ settings.SENTRY_DSN }}",
eventId: "{{ request.sentry_last_event_id() }}",
{% if user.is_authenticated %}
user: {
'name': '{{user.first_name}} {{user.last_name}}',
'email': '{{user.email}}'
name: '{{user.first_name}} {{user.last_name}}',
email: '{{user.email}}'
}
{% endif %}
})
});
});
</script>
{% endif %}
{% endblock content %}

View File

@ -5,6 +5,7 @@
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link rel="stylesheet" href="{{ static('user/user_stats.scss') }}">
<link rel="stylesheet" href="{{ static('core/base.css') }}">
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
<link rel="stylesheet" href="{{ static('core/style.scss') }}">
@ -12,7 +13,6 @@
<link rel="stylesheet" href="{{ static('core/header.scss') }}">
<link rel="stylesheet" href="{{ static('core/navbar.scss') }}">
<link rel="stylesheet" href="{{ static('core/pagination.scss') }}">
<link rel="stylesheet" href="{{ static('vendored/select2/select2.min.css') }}">
{% block jquery_css %}
{# Thile file is quite heavy (around 250kb), so declaring it in a block allows easy removal #}
@ -26,8 +26,6 @@
<script src="{{ static('webpack/jquery-index.js') }}"></script>
<!-- Put here to always have access to those functions on django widgets -->
<script src="{{ static('core/js/script.js') }}"></script>
<script defer src="{{ static('vendored/select2/select2.min.js') }}"></script>
<script defer src="{{ static('core/js/sith-select2.js') }}"></script>

View File

@ -1,13 +1,7 @@
<div>
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
<markdown-input name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.value %}{{ widget.value }}{% endif %}</markdown-input>
{# The easymde script can be included twice, it's safe in the code #}
<script src="{{ statics.js }}" defer> </script>
<link rel="stylesheet" type="text/css" href="{{ statics.css }}" defer>
<script type="text/javascript">
addEventListener("DOMContentLoaded", (event) => {
easymdeFactory(
document.getElementById("{{ widget.attrs.id }}"));
})
</script>
</div>

View File

@ -50,7 +50,7 @@ def phonenumber(
try:
parsed = phonenumbers.parse(value, country)
return phonenumbers.format_number(parsed, number_format)
except phonenumbers.NumberParseException as e:
except phonenumbers.NumberParseException:
return value

View File

@ -343,7 +343,7 @@ class TestUserTools:
response = client.get(reverse("core:user_tools"))
assertRedirects(
response,
expected_url=f"/login?next=%2Fuser%2Ftools%2F",
expected_url="/login?next=%2Fuser%2Ftools%2F",
target_status_code=301,
)

View File

@ -73,7 +73,7 @@ class TestFetchFamilyApi(TestCase):
self.client.force_login(self.main_user)
response = self.client.get(
reverse("api:family_graph", args=[self.main_user.id])
+ f"?godfathers_depth=0&godchildren_depth=0"
+ "?godfathers_depth=0&godchildren_depth=0"
)
assert response.status_code == 200
assert [u["id"] for u in response.json()["users"]] == [self.main_user.id]
@ -91,7 +91,7 @@ class TestFetchFamilyApi(TestCase):
self.client.force_login(self.main_user)
response = self.client.get(
reverse("api:family_graph", args=[self.main_user.id])
+ f"?godfathers_depth=10&godchildren_depth=10"
+ "?godfathers_depth=10&godchildren_depth=10"
)
assert response.status_code == 200
assert [u["id"] for u in response.json()["users"]] == [
@ -126,7 +126,7 @@ class TestFetchFamilyApi(TestCase):
self.client.force_login(self.main_user)
response = self.client.get(
reverse("api:family_graph", args=[self.main_user.id])
+ f"?godfathers_depth=1&godchildren_depth=1"
+ "?godfathers_depth=1&godchildren_depth=1"
)
assert response.status_code == 200
assert [u["id"] for u in response.json()["users"]] == [
@ -150,7 +150,7 @@ class TestFetchFamilyApi(TestCase):
self.client.force_login(self.main_user)
response = self.client.get(
reverse("api:family_graph", args=[self.main_user.id])
+ f"?godfathers_depth=10&godchildren_depth=0"
+ "?godfathers_depth=10&godchildren_depth=0"
)
assert response.status_code == 200
assert [u["id"] for u in response.json()["users"]] == [

View File

@ -158,10 +158,11 @@ def test_user_invoice_with_multiple_items():
item_recipe = Recipe(InvoiceItem, invoice=foreign_key(Recipe(Invoice, user=user)))
item_recipe.make(_quantity=3, quantity=1, product_unit_price=5)
item_recipe.make(_quantity=1, quantity=1, product_unit_price=5)
item_recipe.make(_quantity=2, quantity=1, product_unit_price=iter([5, 8]))
res = list(
Invoice.objects.filter(user=user)
.annotate_total()
.order_by("-total")
.values_list("total", flat=True)
)
assert res == [15, 5]
assert res == [15, 13, 5]

View File

@ -29,13 +29,67 @@ from core.converters import (
FourDigitYearConverter,
TwoDigitMonthConverter,
)
from core.views import *
from core.views import (
FileDeleteView,
FileEditPropView,
FileEditView,
FileListView,
FileModerateView,
FileModerationView,
FileView,
GiftCreateView,
GiftDeleteView,
GroupCreateView,
GroupDeleteView,
GroupEditView,
GroupListView,
GroupTemplateView,
NotificationList,
PageCreateView,
PageDeleteView,
PageEditView,
PageHistView,
PageListView,
PagePropView,
PageRevView,
PageView,
SithLoginView,
SithPasswordChangeDoneView,
SithPasswordChangeView,
SithPasswordResetCompleteView,
SithPasswordResetConfirmView,
SithPasswordResetDoneView,
SithPasswordResetView,
UserAccountDetailView,
UserAccountView,
UserClubView,
UserCreationView,
UserGodfathersTreeView,
UserGodfathersView,
UserListView,
UserMiniView,
UserPicturesView,
UserPreferencesView,
UserStatsView,
UserToolsView,
UserUpdateGroupView,
UserUpdateProfileView,
UserView,
delete_user_godfather,
index,
logout,
notification,
password_root_change,
search_json,
search_user_json,
search_view,
send_file,
)
register_converter(FourDigitYearConverter, "yyyy")
register_converter(TwoDigitMonthConverter, "mm")
register_converter(BooleanStringConverter, "bool")
urlpatterns = [
path("", index, name="index"),
path("notifications/", NotificationList.as_view(), name="notification_list"),
@ -80,27 +134,17 @@ urlpatterns = [
path("group/new/", GroupCreateView.as_view(), name="group_new"),
path("group/<int:group_id>/", GroupEditView.as_view(), name="group_edit"),
path(
"group/<int:group_id>/delete/",
GroupDeleteView.as_view(),
name="group_delete",
"group/<int:group_id>/delete/", GroupDeleteView.as_view(), name="group_delete"
),
path(
"group/<int:group_id>/detail/",
GroupTemplateView.as_view(),
name="group_detail",
"group/<int:group_id>/detail/", GroupTemplateView.as_view(), name="group_detail"
),
# User views
path("user/", UserListView.as_view(), name="user_list"),
path(
"user/<int:user_id>/mini/",
UserMiniView.as_view(),
name="user_profile_mini",
),
path("user/<int:user_id>/mini/", UserMiniView.as_view(), name="user_profile_mini"),
path("user/<int:user_id>/", UserView.as_view(), name="user_profile"),
path(
"user/<int:user_id>/pictures/",
UserPicturesView.as_view(),
name="user_pictures",
"user/<int:user_id>/pictures/", UserPicturesView.as_view(), name="user_pictures"
),
path(
"user/<int:user_id>/godfathers/",
@ -117,28 +161,14 @@ urlpatterns = [
delete_user_godfather,
name="user_godfathers_delete",
),
path(
"user/<int:user_id>/edit/",
UserUpdateProfileView.as_view(),
name="user_edit",
),
path("user/<int:user_id>/edit/", UserUpdateProfileView.as_view(), name="user_edit"),
path("user/<int:user_id>/clubs/", UserClubView.as_view(), name="user_clubs"),
path("user/<int:user_id>/prefs/", UserPreferencesView.as_view(), name="user_prefs"),
path(
"user/<int:user_id>/prefs/",
UserPreferencesView.as_view(),
name="user_prefs",
),
path(
"user/<int:user_id>/groups/",
UserUpdateGroupView.as_view(),
name="user_groups",
"user/<int:user_id>/groups/", UserUpdateGroupView.as_view(), name="user_groups"
),
path("user/tools/", UserToolsView.as_view(), name="user_tools"),
path(
"user/<int:user_id>/account/",
UserAccountView.as_view(),
name="user_account",
),
path("user/<int:user_id>/account/", UserAccountView.as_view(), name="user_account"),
path(
"user/<int:user_id>/account/<yyyy:year>/<mm:month>/",
UserAccountDetailView.as_view(),
@ -179,42 +209,18 @@ urlpatterns = [
),
path("file/moderation/", FileModerationView.as_view(), name="file_moderation"),
path(
"file/<int:file_id>/moderate/",
FileModerateView.as_view(),
name="file_moderate",
"file/<int:file_id>/moderate/", FileModerateView.as_view(), name="file_moderate"
),
path("file/<int:file_id>/download/", send_file, name="download"),
# Page views
path("page/", PageListView.as_view(), name="page_list"),
path("page/create/", PageCreateView.as_view(), name="page_new"),
path("page/<int:page_id>/delete/", PageDeleteView.as_view(), name="page_delete"),
path("page/<path:page_name>/edit/", PageEditView.as_view(), name="page_edit"),
path("page/<path:page_name>/prop/", PagePropView.as_view(), name="page_prop"),
path("page/<path:page_name>/hist/", PageHistView.as_view(), name="page_hist"),
path(
"page/<int:page_id>/delete/",
PageDeleteView.as_view(),
name="page_delete",
),
path(
"page/<path:page_name>/edit/",
PageEditView.as_view(),
name="page_edit",
),
path(
"page/<path:page_name>/prop/",
PagePropView.as_view(),
name="page_prop",
),
path(
"page/<path:page_name>/hist/",
PageHistView.as_view(),
name="page_hist",
),
path(
"page/<path:page_name>/rev/<int:rev>/",
PageRevView.as_view(),
name="page_rev",
),
path(
"page/<path:page_name>/",
PageView.as_view(),
name="page",
"page/<path:page_name>/rev/<int:rev>/", PageRevView.as_view(), name="page_rev"
),
path("page/<path:page_name>/", PageView.as_view(), name="page"),
]

View File

@ -127,7 +127,7 @@ def resize_image_explicit(
def exif_auto_rotate(image):
for orientation in ExifTags.TAGS.keys():
for orientation in ExifTags.TAGS:
if ExifTags.TAGS[orientation] == "Orientation":
break
exif = dict(image._getexif().items())

View File

@ -25,6 +25,7 @@
import types
from typing import Any
from django.conf import settings
from django.contrib.auth.mixins import AccessMixin
from django.core.exceptions import (
ImproperlyConfigured,
@ -35,6 +36,7 @@ from django.http import (
HttpResponseNotFound,
HttpResponseServerError,
)
from django.shortcuts import render
from django.utils.functional import cached_property
from django.views.generic.base import View
from django.views.generic.detail import SingleObjectMixin
@ -79,9 +81,7 @@ def can_edit_prop(obj: Any, user: User) -> bool:
raise PermissionDenied
```
"""
if obj is None or user.is_owner(obj):
return True
return False
return obj is None or user.is_owner(obj)
def can_edit(obj: Any, user: User) -> bool:
@ -232,7 +232,9 @@ class UserIsRootMixin(GenericContentPermissionMixinBuilder):
PermissionDenied: if the user isn't root
"""
permission_function = lambda obj, user: user.is_root
@staticmethod
def permission_function(obj: Any, user: User):
return user.is_root
class FormerSubscriberMixin(AccessMixin):
@ -304,10 +306,10 @@ class QuickNotifMixin:
kwargs["quick_notifs"] = []
for n in self.quick_notif_list:
kwargs["quick_notifs"].append(settings.SITH_QUICK_NOTIF[n])
for k, v in settings.SITH_QUICK_NOTIF.items():
for gk in self.request.GET.keys():
if k == gk:
kwargs["quick_notifs"].append(v)
for key, val in settings.SITH_QUICK_NOTIF.items():
for gk in self.request.GET:
if key == gk:
kwargs["quick_notifs"].append(val)
return kwargs
@ -324,8 +326,10 @@ class DetailFormView(SingleObjectMixin, FormView):
return super().get_object()
from .files import *
from .group import *
from .page import *
from .site import *
from .user import *
# F403: those star-imports would be hellish to refactor
# E402: putting those import at the top of the file would also be difficult
from .files import * # noqa: F403 E402
from .group import * # noqa: F403 E402
from .page import * # noqa: F403 E402
from .site import * # noqa: F403 E402
from .user import * # noqa: F403 E402

View File

@ -193,7 +193,7 @@ class FileEditView(CanEditMixin, UpdateView):
def get_form_class(self):
fields = ["name", "is_moderated"]
if self.object.is_file:
fields = ["file"] + fields
fields = ["file", *fields]
return modelform_factory(SithFile, fields=fields)
def get_success_url(self):
@ -283,38 +283,38 @@ class FileView(CanViewMixin, DetailView, FormMixin):
`obj` is the SithFile object you want to put in the clipboard, or
where you want to paste the clipboard
"""
if "delete" in request.POST.keys():
if "delete" in request.POST:
for f_id in request.POST.getlist("file_list"):
sf = SithFile.objects.filter(id=f_id).first()
if sf:
sf.delete()
if "clear" in request.POST.keys():
file = SithFile.objects.filter(id=f_id).first()
if file:
file.delete()
if "clear" in request.POST:
request.session["clipboard"] = []
if "cut" in request.POST.keys():
for f_id in request.POST.getlist("file_list"):
f_id = int(f_id)
if "cut" in request.POST:
for f_id_str in request.POST.getlist("file_list"):
f_id = int(f_id_str)
if (
f_id in [c.id for c in obj.children.all()]
and f_id not in request.session["clipboard"]
):
request.session["clipboard"].append(f_id)
if "paste" in request.POST.keys():
if "paste" in request.POST:
for f_id in request.session["clipboard"]:
sf = SithFile.objects.filter(id=f_id).first()
if sf:
sf.move_to(obj)
file = SithFile.objects.filter(id=f_id).first()
if file:
file.move_to(obj)
request.session["clipboard"] = []
request.session.modified = True
def get(self, request, *args, **kwargs):
self.form = self.get_form()
if "clipboard" not in request.session.keys():
if "clipboard" not in request.session:
request.session["clipboard"] = []
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if "clipboard" not in request.session.keys():
if "clipboard" not in request.session:
request.session["clipboard"] = []
if request.user.can_edit(self.object):
# XXX this call can fail!
@ -398,6 +398,6 @@ class FileModerateView(CanEditPropMixin, SingleObjectMixin):
self.object.is_moderated = True
self.object.moderator = request.user
self.object.save()
if "next" in self.request.GET.keys():
if "next" in self.request.GET:
return redirect(self.request.GET["next"])
return redirect("core:file_moderation")

View File

@ -29,6 +29,7 @@ from captcha.fields import CaptchaField
from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.exceptions import ValidationError
from django.db import transaction
from django.forms import (
@ -38,7 +39,6 @@ from django.forms import (
Textarea,
TextInput,
)
from django.templatetags.static import static
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from phonenumber_field.widgets import RegionalPhoneNumberWidget
@ -72,8 +72,8 @@ class MarkdownInput(Textarea):
context = super().get_context(name, value, attrs)
context["statics"] = {
"js": static("webpack/easymde-index.js"),
"css": static("webpack/easymde-index.css"),
"js": staticfiles_storage.url("webpack/easymde-index.ts"),
"css": staticfiles_storage.url("webpack/easymde-index.css"),
}
return context
@ -140,7 +140,7 @@ class SelectUser(TextInput):
class LoginForm(AuthenticationForm):
def __init__(self, *arg, **kwargs):
if "data" in kwargs.keys():
if "data" in kwargs:
from counter.models import Customer
data = kwargs["data"].copy()
@ -157,7 +157,7 @@ class LoginForm(AuthenticationForm):
else:
user = User.objects.filter(username=data["username"]).first()
data["username"] = user.username
except:
except: # noqa E722 I don't know what error is supposed to be raised here
pass
kwargs["data"] = data
super().__init__(*arg, **kwargs)

View File

@ -55,7 +55,7 @@ class PageView(CanViewMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if "page" not in context.keys():
if "page" not in context:
context["new_page"] = self.kwargs["page_name"]
return context
@ -92,22 +92,16 @@ class PageRevView(CanViewMixin, DetailView):
)
return res
def get_object(self):
def get_object(self, *args, **kwargs):
self.page = Page.get_page_by_full_name(self.kwargs["page_name"])
return self.page
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.page is not None:
if not self.page:
return context | {"new_page": self.kwargs["page_name"]}
context["page"] = self.page
try:
rev = self.page.revisions.get(id=self.kwargs["rev"])
context["rev"] = rev
except:
# By passing, the template will just display the normal page without taking revision into account
pass
else:
context["new_page"] = self.kwargs["page_name"]
context["rev"] = self.page.revisions.filter(id=self.kwargs["rev"]).first()
return context
@ -118,7 +112,7 @@ class PageCreateView(CanCreateMixin, CreateView):
def get_initial(self):
init = {}
if "page" in self.request.GET.keys():
if "page" in self.request.GET:
page_name = self.request.GET["page"]
parent_name = "/".join(page_name.split("/")[:-1])
parent = Page.get_page_by_full_name(parent_name)
@ -145,18 +139,8 @@ class PagePropView(CanEditPagePropMixin, UpdateView):
slug_field = "_full_name"
slug_url_kwarg = "page_name"
def get_object(self):
o = super().get_object()
# Create the page if it does not exists
# if p == None:
# parent_name = '/'.join(page_name.split('/')[:-1])
# name = page_name.split('/')[-1]
# if parent_name == "":
# p = Page(name=name)
# else:
# parent = Page.get_page_by_full_name(parent_name)
# p = Page(name=name, parent=parent)
self.page = o
def get_object(self, queryset=None):
self.page = super().get_object()
try:
self.page.set_lock_recursive(self.request.user)
except LockError as e:

View File

@ -53,11 +53,8 @@ class NotificationList(ListView):
if self.request.user.is_anonymous:
return Notification.objects.none()
# TODO: Bulk update in django 2.2
if "see_all" in self.request.GET.keys():
for n in self.request.user.notifications.filter(viewed=False):
n.viewed = True
n.save()
if "see_all" in self.request.GET:
self.request.user.notifications.filter(viewed=False).update(viewed=True)
return self.request.user.notifications.order_by("-date")[:20]

View File

@ -21,9 +21,11 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import itertools
# This file contains all the views that concern the user model
from datetime import date, timedelta
from operator import itemgetter
from smtplib import SMTPException
from django.conf import settings
@ -253,8 +255,10 @@ class UserTabsMixin(TabedViewMixin):
"name": _("Groups"),
}
)
try:
if user.customer and (
if (
hasattr(user, "customer")
and user.customer
and (
user == self.request.user
or self.request.user.is_in_group(
pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
@ -264,6 +268,7 @@ class UserTabsMixin(TabedViewMixin):
+ settings.SITH_BOARD_SUFFIX
)
or self.request.user.is_root
)
):
tab_list.append(
{
@ -274,15 +279,11 @@ class UserTabsMixin(TabedViewMixin):
)
tab_list.append(
{
"url": reverse(
"core:user_account", kwargs={"user_id": user.id}
),
"url": reverse("core:user_account", kwargs={"user_id": user.id}),
"slug": "account",
"name": _("Account") + " (%s €)" % user.customer.amount,
}
)
except:
pass
return tab_list
@ -665,9 +666,15 @@ class UserAccountView(UserAccountBase):
kwargs["refilling_month"] = self.expense_by_month(
Refilling.objects.filter(customer=self.object.customer)
)
kwargs["invoices_month"] = self.expense_by_month(
Invoice.objects.filter(user=self.object)
kwargs["invoices_month"] = [
# the django ORM removes the `group by` clause in this query,
# so a little of post-processing is needed
{"grouped_date": key, "total": sum(i["total"] for i in group)}
for key, group in itertools.groupby(
self.expense_by_month(Invoice.objects.filter(user=self.object)),
key=itemgetter("grouped_date"),
)
]
kwargs["etickets"] = self.object.customer.buyings.exclude(product__eticket=None)
return kwargs

View File

@ -15,7 +15,19 @@
from django.contrib import admin
from haystack.admin import SearchModelAdmin
from counter.models import *
from counter.models import (
AccountDump,
BillingInfo,
CashRegisterSummary,
Counter,
Customer,
Eticket,
Permanency,
Product,
ProductType,
Refilling,
Selling,
)
@admin.register(Product)

View File

@ -154,7 +154,7 @@ class Customer(models.Model):
self.save()
def get_full_url(self):
return "".join(["https://", settings.SITH_URL, self.get_absolute_url()])
return f"https://{settings.SITH_URL}{self.get_absolute_url()}"
class BillingInfo(models.Model):
@ -287,9 +287,7 @@ class ProductType(models.Model):
"""Method to see if that object can be edited by the given user."""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return False
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
class Product(models.Model):
@ -346,21 +344,19 @@ class Product(models.Model):
@property
def is_record_product(self):
return settings.SITH_ECOCUP_CONS == self.id
return self.id == settings.SITH_ECOCUP_CONS
@property
def is_unrecord_product(self):
return settings.SITH_ECOCUP_DECO == self.id
return self.id == settings.SITH_ECOCUP_DECO
def is_owned_by(self, user):
"""Method to see if that object can be edited by the given user."""
if user.is_anonymous:
return False
if user.is_in_group(
return user.is_in_group(
pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
return True
return False
) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
def can_be_sold_to(self, user: User) -> bool:
"""Check if whether the user given in parameter has the right to buy
@ -392,10 +388,7 @@ class Product(models.Model):
buying_groups = list(self.buying_groups.all())
if not buying_groups:
return True
for group in buying_groups:
if user.is_in_group(pk=group.id):
return True
return False
return any(user.is_in_group(pk=group.id) for group in buying_groups)
@property
def profit(self):
@ -887,27 +880,19 @@ class Selling(models.Model):
"You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s."
) % {
"event": event,
"url": "".join(
(
'<a href="',
self.customer.get_full_url(),
'">',
self.customer.get_full_url(),
"</a>",
)
"url": (
f'<a href="{self.customer.get_full_url()}">'
f"{self.customer.get_full_url()}</a>"
),
"eticket": "".join(
(
'<a href="',
self.get_eticket_full_url(),
'">',
self.get_eticket_full_url(),
"</a>",
)
"eticket": (
f'<a href="{self.get_eticket_full_url()}">'
f"{self.get_eticket_full_url()}</a>"
),
}
message_txt = _(
"You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s."
"You bought an eticket for the event %(event)s.\n"
"You can download it directly from this link %(eticket)s.\n"
"You can also retrieve all your e-tickets on your account page %(url)s."
) % {
"event": event,
"url": self.customer.get_full_url(),
@ -919,7 +904,7 @@ class Selling(models.Model):
def get_eticket_full_url(self):
eticket_url = reverse("counter:eticket_pdf", kwargs={"selling_id": self.id})
return "".join(["https://", settings.SITH_URL, eticket_url])
return f"https://{settings.SITH_URL}{eticket_url}"
class Permanency(models.Model):
@ -1019,15 +1004,15 @@ class CashRegisterSummary(models.Model):
elif name == "hundred_euros":
return self.items.filter(value=100, is_check=False).first()
elif name == "check_1":
return checks[0] if 0 < len(checks) else None
return checks[0] if len(checks) > 0 else None
elif name == "check_2":
return checks[1] if 1 < len(checks) else None
return checks[1] if len(checks) > 1 else None
elif name == "check_3":
return checks[2] if 2 < len(checks) else None
return checks[2] if len(checks) > 2 else None
elif name == "check_4":
return checks[3] if 3 < len(checks) else None
return checks[3] if len(checks) > 3 else None
elif name == "check_5":
return checks[4] if 4 < len(checks) else None
return checks[4] if len(checks) > 4 else None
else:
return object.__getattribute__(self, name)
@ -1035,9 +1020,7 @@ class CashRegisterSummary(models.Model):
"""Method to see if that object can be edited by the given user."""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID):
return True
return False
return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
def get_total(self):
t = 0

View File

@ -51,7 +51,7 @@ def write_log(instance, operation_type):
# Return None by default
return None
log = OperationLog(
OperationLog(
label=str(instance),
operator=get_user(),
operation_type=operation_type,

View File

@ -8,25 +8,17 @@
{% if current_tab == "products" %}
<p><a href="{{ url('counter:new_product') }}">{% trans %}New product{% endtrans %}</a></p>
{% endif %}
{% if product_list %}
<h3>{% trans %}Product list{% endtrans %}</h3>
{% for t in ProductType.objects.all().order_by('name') %}
<h4>{{ t }}</h4>
{%- for product_type, products in object_list -%}
<h4>{{ product_type or _("Uncategorized") }}</h4>
<ul>
{% for p in product_list.filter(product_type=t).all().order_by('name') %}
<li><a href="{{ url('counter:product_edit', product_id=p.id) }}">{{ p }} ({{ p.code }})</a></li>
{% endfor %}
{%- for product in products -%}
<li><a href="{{ url('counter:product_edit', product_id=product.id) }}">{{ product }} ({{ product.code }})</a></li>
{%- endfor -%}
</ul>
{% endfor %}
<h4>{% trans %}Uncategorized{% endtrans %}</h4>
<ul>
{% for p in product_list.filter(product_type=None).all().order_by('name') %}
<li><a href="{{ url('counter:product_edit', product_id=p.id) }}">{{ p }} ({{ p.code }})</a></li>
{% endfor %}
</ul>
{% else %}
{%- else -%}
{% trans %}There is no products in this website.{% endtrans %}
{% endif %}
{%- endfor -%}
{% endblock %}

View File

@ -503,7 +503,7 @@ class TestBarmanConnection(TestCase):
)
response = self.client.get(reverse("counter:activity", args=[self.counter.id]))
assert not '<li><a href="/user/1/">S&#39; Kia</a></li>' in str(response.content)
assert '<li><a href="/user/1/">S&#39; Kia</a></li>' not in str(response.content)
@pytest.mark.django_db
@ -853,7 +853,7 @@ class TestCustomerAccountId(TestCase):
number = account_id[:-1]
assert created is True
assert number == "12346"
assert 6 == len(account_id)
assert len(account_id) == 6
assert account_id[-1] in string.ascii_lowercase
assert customer.amount == 0

View File

@ -15,15 +15,44 @@
from django.urls import path
from counter.views import *
from counter.views import (
ActiveProductListView,
ArchivedProductListView,
CashSummaryEditView,
CashSummaryListView,
CounterActivityView,
CounterCashSummaryView,
CounterClick,
CounterCreateView,
CounterDeleteView,
CounterEditPropView,
CounterEditView,
CounterLastOperationsView,
CounterListView,
CounterMain,
CounterRefillingListView,
CounterStatView,
EticketCreateView,
EticketEditView,
EticketListView,
EticketPDFView,
InvoiceCallView,
ProductCreateView,
ProductEditView,
ProductTypeCreateView,
ProductTypeEditView,
ProductTypeListView,
RefillingDeleteView,
SellingDeleteView,
StudentCardDeleteView,
StudentCardFormView,
counter_login,
counter_logout,
)
urlpatterns = [
path("<int:counter_id>/", CounterMain.as_view(), name="details"),
path(
"<int:counter_id>/click/<int:user_id>/",
CounterClick.as_view(),
name="click",
),
path("<int:counter_id>/click/<int:user_id>/", CounterClick.as_view(), name="click"),
path(
"<int:counter_id>/last_ops/",
CounterLastOperationsView.as_view(),
@ -34,19 +63,11 @@ urlpatterns = [
CounterCashSummaryView.as_view(),
name="cash_summary",
),
path(
"<int:counter_id>/activity/",
CounterActivityView.as_view(),
name="activity",
),
path("<int:counter_id>/activity/", CounterActivityView.as_view(), name="activity"),
path("<int:counter_id>/stats/", CounterStatView.as_view(), name="stats"),
path("<int:counter_id>/login/", counter_login, name="login"),
path("<int:counter_id>/logout/", counter_logout, name="logout"),
path(
"eticket/<int:selling_id>/pdf/",
EticketPDFView.as_view(),
name="eticket_pdf",
),
path("eticket/<int:selling_id>/pdf/", EticketPDFView.as_view(), name="eticket_pdf"),
path(
"customer/<int:customer_id>/card/add/",
StudentCardFormView.as_view(),
@ -59,17 +80,11 @@ urlpatterns = [
),
path("admin/<int:counter_id>/", CounterEditView.as_view(), name="admin"),
path(
"admin/<int:counter_id>/prop/",
CounterEditPropView.as_view(),
name="prop_admin",
"admin/<int:counter_id>/prop/", CounterEditPropView.as_view(), name="prop_admin"
),
path("admin/", CounterListView.as_view(), name="admin_list"),
path("admin/new/", CounterCreateView.as_view(), name="new"),
path(
"admin/delete/<int:counter_id>/",
CounterDeleteView.as_view(),
name="delete",
),
path("admin/delete/<int:counter_id>/", CounterDeleteView.as_view(), name="delete"),
path("admin/invoices_call/", InvoiceCallView.as_view(), name="invoices_call"),
path(
"admin/cash_summary/list/",
@ -81,10 +96,10 @@ urlpatterns = [
CashSummaryEditView.as_view(),
name="cash_summary_edit",
),
path("admin/product/list/", ProductListView.as_view(), name="product_list"),
path("admin/product/list/", ActiveProductListView.as_view(), name="product_list"),
path(
"admin/product/list_archived/",
ProductArchivedListView.as_view(),
ArchivedProductListView.as_view(),
name="product_list_archived",
),
path("admin/product/create/", ProductCreateView.as_view(), name="new_product"),

View File

@ -12,10 +12,12 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
import itertools
import re
from datetime import datetime, timedelta
from datetime import timezone as tz
from http import HTTPStatus
from operator import attrgetter
from typing import TYPE_CHECKING
from urllib.parse import parse_qs
@ -89,16 +91,10 @@ class CounterAdminMixin(View):
edit_club = []
def _test_group(self, user):
for grp_id in self.edit_group:
if user.is_in_group(pk=grp_id):
return True
return False
return any(user.is_in_group(pk=grp_id) for grp_id in self.edit_group)
def _test_club(self, user):
for c in self.edit_club:
if c.can_be_edited_by(user):
return True
return False
return any(c.can_be_edited_by(user) for c in self.edit_club)
def dispatch(self, request, *args, **kwargs):
if not (
@ -179,7 +175,7 @@ class CounterMain(
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.type == "BAR" and not (
"counter_token" in self.request.session.keys()
"counter_token" in self.request.session
and self.request.session["counter_token"] == self.object.token
): # Check the token to avoid the bar to be stolen
return HttpResponseRedirect(
@ -217,7 +213,7 @@ class CounterMain(
kwargs["barmen"] = self.object.barmen_list
elif self.request.user.is_authenticated:
kwargs["barmen"] = [self.request.user]
if "last_basket" in self.request.session.keys():
if "last_basket" in self.request.session:
kwargs["last_basket"] = self.request.session.pop("last_basket")
kwargs["last_customer"] = self.request.session.pop("last_customer")
kwargs["last_total"] = self.request.session.pop("last_total")
@ -292,7 +288,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
def get(self, request, *args, **kwargs):
"""Simple get view."""
if "basket" not in request.session.keys(): # Init the basket session entry
if "basket" not in request.session: # Init the basket session entry
request.session["basket"] = {}
request.session["basket_total"] = 0
request.session["not_enough"] = False # Reset every variable
@ -316,7 +312,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
): # Check that at least one barman is logged in
return self.cancel(request)
if self.object.type == "BAR" and not (
"counter_token" in self.request.session.keys()
"counter_token" in self.request.session
and self.request.session["counter_token"] == self.object.token
): # Also check the token to avoid the bar to be stolen
return HttpResponseRedirect(
@ -327,7 +323,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
)
+ "?bad_location"
)
if "basket" not in request.session.keys():
if "basket" not in request.session:
request.session["basket"] = {}
request.session["basket_total"] = 0
request.session["not_enough"] = False # Reset every variable
@ -384,13 +380,12 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
def get_total_quantity_for_pid(self, request, pid):
pid = str(pid)
try:
if pid not in request.session["basket"]:
return 0
return (
request.session["basket"][pid]["qty"]
+ request.session["basket"][pid]["bonus_qty"]
)
except:
return 0
def compute_record_product(self, request, product=None):
recorded = 0
@ -804,25 +799,41 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
current_tab = "products"
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
model = Product
queryset = Product.objects.annotate(type_name=F("product_type__name"))
template_name = "counter/product_list.jinja"
ordering = [
F("product_type__priority").desc(nulls_last=True),
"product_type",
"name",
]
def get_context_data(self, **kwargs):
res = super().get_context_data(**kwargs)
res["object_list"] = itertools.groupby(
res["object_list"], key=attrgetter("type_name")
)
return res
class ArchivedProductListView(ProductListView):
"""A list view for the admins."""
model = Product
template_name = "counter/product_list.jinja"
queryset = Product.objects.filter(archived=True)
ordering = ["name"]
current_tab = "archive"
def get_queryset(self):
return super().get_queryset().filter(archived=True)
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
class ActiveProductListView(ProductListView):
"""A list view for the admins."""
model = Product
template_name = "counter/product_list.jinja"
queryset = Product.objects.filter(archived=False)
ordering = ["name"]
current_tab = "products"
def get_queryset(self):
return super().get_queryset().filter(archived=False)
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
"""A create view for the admins."""

View File

@ -24,6 +24,12 @@ Si le mot apparaît dans le template Jinja :
{% trans %}Hello{% endtrans %}
```
Si on est dans un fichier javascript ou typescript :
```js
gettext("Hello");
```
## Générer le fichier django.po
La traduction se fait en trois étapes.
@ -32,7 +38,7 @@ l'éditer et enfin le compiler au format binaire pour qu'il soit lu par le serve
```bash
./manage.py makemessages --locale=fr -e py,jinja --ignore=node_modules # Pour le backend
./manage.py makemessages --locale=fr -d djangojs --ignore=node_modules # Pour le frontend
./manage.py makemessages --locale=fr -d djangojs -e js,ts --ignore=node_modules # Pour le frontend
```
## Éditer le fichier django.po

View File

@ -190,6 +190,10 @@ que sont VsCode et Sublime Text.
"[javascript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome"
}
}
```

View File

@ -246,15 +246,3 @@ pytest core/tests/tests_core.py::TestUserRegistration
tous les tests avant de push un commit.
## Vérifier les dépendances Javascript
Une commande a été écrite pour vérifier les éventuelles mises
à jour à faire sur les librairies Javascript utilisées.
N'oubliez pas de mettre à jour à la fois le fichier
de la librairie, mais également sa version dans `sith/settings.py`.
```bash
# Vérifier les mises à jour
python manage.py check_front
```

View File

@ -13,8 +13,9 @@
#
#
from django.contrib import admin
from django.db.models import F, Sum
from eboutic.models import *
from eboutic.models import Basket, BasketItem, Invoice, InvoiceItem
@admin.register(Basket)

View File

@ -117,9 +117,7 @@ class BasketForm:
"""
if not self.error_messages and not self.correct_items:
self.clean()
if self.error_messages:
return False
return True
return not self.error_messages
@cached_property
def errors(self) -> list[str]:

View File

@ -173,9 +173,8 @@ class InvoiceQueryset(models.QuerySet):
return self.annotate(
total=Subquery(
InvoiceItem.objects.filter(invoice_id=OuterRef("pk"))
.annotate(item_amount=F("product_unit_price") * F("quantity"))
.values("item_amount")
.annotate(total=Sum("item_amount"))
.values("invoice_id")
.annotate(total=Sum(F("product_unit_price") * F("quantity")))
.values("total")
)
)

View File

@ -2,8 +2,6 @@ from typing import Annotated
from ninja import ModelSchema, Schema
from pydantic import Field, NonNegativeInt, PositiveInt, TypeAdapter
# from phonenumber_field.phonenumber import PhoneNumber
from pydantic_extra_types.phone_numbers import PhoneNumber, PhoneNumberValidator
from counter.models import BillingInfo

View File

@ -7,11 +7,11 @@
import base64
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
from cryptography.hazmat.primitives.hashes import SHA1
from cryptography.hazmat.primitives.serialization import (
load_pem_private_key,
@ -19,6 +19,12 @@ from cryptography.hazmat.primitives.serialization import (
)
from django.conf import settings
if TYPE_CHECKING:
from cryptography.hazmat.primitives.asymmetric.rsa import (
RSAPrivateKey,
RSAPublicKey,
)
def test_signature_valid():
"""Test that data sent to the bank is correctly signed."""

View File

@ -24,9 +24,9 @@
import base64
import json
import urllib
from typing import TYPE_CHECKING
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.hashes import SHA1
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from django.conf import settings
@ -38,6 +38,9 @@ from core.models import User
from counter.models import Counter, Customer, Product, Selling
from eboutic.models import Basket, BasketItem
if TYPE_CHECKING:
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
class TestEboutic(TestCase):
@classmethod

View File

@ -25,7 +25,14 @@
from django.urls import path, register_converter
from eboutic.converters import PaymentResultConverter
from eboutic.views import *
from eboutic.views import (
EbouticCommand,
EtransactionAutoAnswer,
e_transaction_data,
eboutic_main,
pay_with_sith,
payment_result,
)
register_converter(PaymentResultConverter, "res")

View File

@ -17,11 +17,11 @@ import base64
import json
from datetime import datetime
from enum import Enum
from typing import TYPE_CHECKING
import sentry_sdk
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from cryptography.hazmat.primitives.hashes import SHA1
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from django.conf import settings
@ -47,6 +47,9 @@ from eboutic.models import (
)
from eboutic.schemas import PurchaseItemList, PurchaseItemSchema
if TYPE_CHECKING:
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
@login_required
@require_GET
@ -221,7 +224,7 @@ class EtransactionAutoAnswer(View):
# Payment authorized:
# * 'Error' is '00000'
# * 'Auto' is in the request
if request.GET["Error"] == "00000" and "Auto" in request.GET.keys():
if request.GET["Error"] == "00000" and "Auto" in request.GET:
try:
with transaction.atomic():
b = (

View File

@ -1,6 +1,22 @@
from django.urls import path
from election.views import *
from election.views import (
CandidatureCreateView,
CandidatureDeleteView,
CandidatureUpdateView,
ElectionCreateView,
ElectionDeleteView,
ElectionDetailView,
ElectionListArchivedView,
ElectionListCreateView,
ElectionListDeleteView,
ElectionsListView,
ElectionUpdateView,
RoleCreateView,
RoleDeleteView,
RoleUpdateView,
VoteFormView,
)
urlpatterns = [
path("", ElectionsListView.as_view(), name="list"),
@ -19,16 +35,10 @@ urlpatterns = [
name="delete_list",
),
path(
"<int:election_id>/role/create/",
RoleCreateView.as_view(),
name="create_role",
"<int:election_id>/role/create/", RoleCreateView.as_view(), name="create_role"
),
path("<int:role_id>/role/edit/", RoleUpdateView.as_view(), name="update_role"),
path(
"<int:role_id>/role/delete/",
RoleDeleteView.as_view(),
name="delete_role",
),
path("<int:role_id>/role/delete/", RoleDeleteView.as_view(), name="delete_role"),
path(
"<int:election_id>/candidate/add/",
CandidatureCreateView.as_view(),

View File

@ -1,3 +1,5 @@
from typing import TYPE_CHECKING
from ajax_select import make_ajax_field
from ajax_select.fields import AutoCompleteSelectField
from django import forms
@ -10,11 +12,14 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from core.models import User
from core.views import CanCreateMixin, CanEditMixin, CanViewMixin
from core.views.forms import MarkdownInput, SelectDateTime
from election.models import Candidature, Election, ElectionList, Role, Vote
if TYPE_CHECKING:
from core.models import User
# Custom form field
@ -23,7 +28,6 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
def __init__(self, queryset, max_choice, **kwargs):
self.max_choice = max_choice
widget = forms.CheckboxSelectMultiple()
super().__init__(queryset, **kwargs)
def clean(self, value):
@ -251,7 +255,7 @@ class VoteFormView(CanCreateMixin, FormView):
def vote(self, election_data):
with transaction.atomic():
for role_title in election_data.keys():
for role_title in election_data:
# If we have a multiple choice field
if isinstance(election_data[role_title], QuerySet):
if election_data[role_title].count() > 0:
@ -444,28 +448,16 @@ class ElectionUpdateView(CanEditMixin, UpdateView):
pk_url_kwarg = "election_id"
def get_initial(self):
init = {}
try:
init["start_date"] = self.object.start_date.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
pass
try:
init["end_date"] = self.object.end_date.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
pass
try:
init["start_candidature"] = self.object.start_candidature.strftime(
return {
"start_date": self.object.start_date.strftime("%Y-%m-%d %H:%M:%S"),
"end_date": self.object.end_date.strftime("%Y-%m-%d %H:%M:%S"),
"start_candidature": self.object.start_candidature.strftime(
"%Y-%m-%d %H:%M:%S"
)
except Exception:
pass
try:
init["end_candidature"] = self.object.end_candidature.strftime(
),
"end_candidature": self.object.end_candidature.strftime(
"%Y-%m-%d %H:%M:%S"
)
except Exception:
pass
return init
),
}
def get_success_url(self, **kwargs):
return reverse_lazy("election:detail", kwargs={"election_id": self.object.id})

View File

@ -16,7 +16,7 @@
from django.contrib import admin
from haystack.admin import SearchModelAdmin
from forum.models import *
from forum.models import Forum, ForumMessage, ForumTopic
@admin.register(Forum)

View File

@ -25,6 +25,7 @@ from __future__ import annotations
from datetime import datetime
from datetime import timezone as tz
from itertools import chain
from typing import Self
from django.conf import settings
from django.core.exceptions import ValidationError
@ -207,12 +208,12 @@ class Forum(models.Model):
return self.get_parent_list()
def get_parent_list(self):
l = []
p = self.parent
while p is not None:
l.append(p)
p = p.parent
return l
parents = []
current = self.parent
while current is not None:
parents.append(current)
current = current.parent
return parents
@property
def topic_number(self):
@ -228,12 +229,12 @@ class Forum(models.Model):
def last_message(self):
return self._last_message
def get_children_list(self):
l = [self.id]
def get_children_list(self) -> list[Self]:
children = [self.id]
for c in self.children.all():
l.append(c.id)
l += c.get_children_list()
return l
children.append(c.id)
children.extend(c.get_children_list())
return children
class ForumTopic(models.Model):

View File

@ -23,7 +23,26 @@
from django.urls import path
from forum.views import *
from forum.views import (
ForumCreateView,
ForumDeleteView,
ForumDetailView,
ForumEditView,
ForumFavoriteTopics,
ForumLastUnread,
ForumMainView,
ForumMarkAllAsRead,
ForumMessageCreateView,
ForumMessageDeleteView,
ForumMessageEditView,
ForumMessageUndeleteView,
ForumMessageView,
ForumSearchView,
ForumTopicCreateView,
ForumTopicDetailView,
ForumTopicEditView,
ForumTopicSubscribeView,
)
urlpatterns = [
path("", ForumMainView.as_view(), name="main"),
@ -35,21 +54,9 @@ urlpatterns = [
path("<int:forum_id>/", ForumDetailView.as_view(), name="view_forum"),
path("<int:forum_id>/edit/", ForumEditView.as_view(), name="edit_forum"),
path("<int:forum_id>/delete/", ForumDeleteView.as_view(), name="delete_forum"),
path(
"<int:forum_id>/new_topic/",
ForumTopicCreateView.as_view(),
name="new_topic",
),
path(
"topic/<int:topic_id>/",
ForumTopicDetailView.as_view(),
name="view_topic",
),
path(
"topic/<int:topic_id>/edit/",
ForumTopicEditView.as_view(),
name="edit_topic",
),
path("<int:forum_id>/new_topic/", ForumTopicCreateView.as_view(), name="new_topic"),
path("topic/<int:topic_id>/", ForumTopicDetailView.as_view(), name="view_topic"),
path("topic/<int:topic_id>/edit/", ForumTopicEditView.as_view(), name="edit_topic"),
path(
"topic/<int:topic_id>/new_message/",
ForumMessageCreateView.as_view(),
@ -60,11 +67,7 @@ urlpatterns = [
ForumTopicSubscribeView.as_view(),
name="toggle_subscribe_topic",
),
path(
"message/<int:message_id>/",
ForumMessageView.as_view(),
name="view_message",
),
path("message/<int:message_id>/", ForumMessageView.as_view(), name="view_message"),
path(
"message/<int:message_id>/edit/",
ForumMessageEditView.as_view(),

View File

@ -71,7 +71,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
self.logger = logging.getLogger("main")
if options["verbosity"] < 0 or 2 < options["verbosity"]:
if not 0 <= options["verbosity"] <= 2:
warnings.warn(
"verbosity level should be between 0 and 2 included", stacklevel=2
)

View File

@ -40,7 +40,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
logger = logging.getLogger("main")
if options["verbosity"] < 0 or 2 < options["verbosity"]:
if not 0 <= options["verbosity"] <= 2:
warnings.warn(
"verbosity level should be between 0 and 2 included", stacklevel=2
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,138 @@
import { default as ForceGraph3D } from "3d-force-graph";
import { forceX, forceY, forceZ } from "d3-force-3d";
// biome-ignore lint/style/noNamespaceImport: This is how it should be imported
import * as Three from "three";
import SpriteText from "three-spritetext";
/**
* @typedef GalaxyConfig
* @property {number} nodeId id of the current user node
* @property {string} dataUrl url to fetch the galaxy data from
**/
/**
* Load the galaxy of an user
* @param {GalaxyConfig} config
**/
window.loadGalaxy = (config) => {
window.getNodeFromId = (id) => {
return Graph.graphData().nodes.find((n) => n.id === id);
};
window.getLinksFromNodeId = (id) => {
return Graph.graphData().links.filter(
(l) => l.source.id === id || l.target.id === id,
);
};
window.focusNode = (node) => {
highlightNodes.clear();
highlightLinks.clear();
hoverNode = node || null;
if (node) {
// collect neighbors and links for highlighting
for (const link of window.getLinksFromNodeId(node.id)) {
highlightLinks.add(link);
highlightNodes.add(link.source);
highlightNodes.add(link.target);
}
}
// refresh node and link display
Graph.nodeThreeObject(Graph.nodeThreeObject())
.linkWidth(Graph.linkWidth())
.linkDirectionalParticles(Graph.linkDirectionalParticles());
// Aim at node from outside it
const distance = 42;
const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
const newPos =
node.x || node.y || node.z
? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }
: { x: 0, y: 0, z: distance }; // special case if node is in (0,0,0)
Graph.cameraPosition(
newPos, // new position
node, // lookAt ({ x, y, z })
3000, // ms transition duration
);
};
const highlightNodes = new Set();
const highlightLinks = new Set();
let hoverNode = null;
const grpahDiv = document.getElementById("3d-graph");
const Graph = ForceGraph3D();
Graph(grpahDiv);
Graph.jsonUrl(config.dataUrl)
.width(
grpahDiv.parentElement.clientWidth > 1200
? 1200
: grpahDiv.parentElement.clientWidth,
) // Not perfect at all. JS-fu master from the future, please fix this :-)
.height(1000)
.enableNodeDrag(false) // allow easier navigation
.onNodeClick((node) => {
const camera = Graph.cameraPosition();
const distance = Math.sqrt(
(node.x - camera.x) ** 2 + (node.y - camera.y) ** 2 + (node.z - camera.z) ** 2,
);
if (distance < 120 || highlightNodes.has(node)) {
window.focusNode(node);
}
})
.linkWidth((link) => (highlightLinks.has(link) ? 0.4 : 0.0))
.linkColor((link) =>
highlightLinks.has(link) ? "rgba(255,160,0,1)" : "rgba(128,255,255,0.6)",
)
.linkVisibility((link) => highlightLinks.has(link))
.nodeVisibility((node) => highlightNodes.has(node) || node.mass > 4)
// .linkDirectionalParticles(link => highlightLinks.has(link) ? 3 : 1) // kinda buggy for now, and slows this a bit, but would be great to help visualize lanes
.linkDirectionalParticleWidth(0.2)
.linkDirectionalParticleSpeed(-0.006)
.nodeThreeObject((node) => {
const sprite = new SpriteText(node.name);
sprite.material.depthWrite = false; // make sprite background transparent
sprite.color = highlightNodes.has(node)
? node === hoverNode
? "rgba(200,0,0,1)"
: "rgba(255,160,0,0.8)"
: "rgba(0,255,255,0.2)";
sprite.textHeight = 2;
sprite.center = new Three.Vector2(1.2, 0.5);
return sprite;
})
.onEngineStop(() => {
window.focusNode(window.getNodeFromId(config.nodeId));
Graph.onEngineStop(() => {
/* nope */
}); // don't call ourselves in a loop while moving the focus
});
// Set distance between stars
Graph.d3Force("link").distance((link) => link.value);
// Set high masses nearer the center of the galaxy
// TODO: quick and dirty strength computation, this will need tuning.
Graph.d3Force(
"positionX",
forceX().strength((node) => {
return 1 - 1 / node.mass;
}),
);
Graph.d3Force(
"positionY",
forceY().strength((node) => {
return 1 - 1 / node.mass;
}),
);
Graph.d3Force(
"positionZ",
forceZ().strength((node) => {
return 1 - 1 / node.mass;
}),
);
};

View File

@ -4,13 +4,18 @@
{% trans user_name=object.get_display_name() %}{{ user_name }}'s Galaxy{% endtrans %}
{% endblock %}
{% block additional_js %}
<script src="{{ static('webpack/galaxy/galaxy-index.js') }}" defer></script>
{% endblock %}
{% block content %}
{% if object.current_star %}
<div style="display: flex; flex-wrap: wrap;">
<div id="3d-graph"></div>
<div style="margin: 1em;">
<p><a onclick="focus_node(get_node_from_id({{ object.id }}))">Reset on {{ object.get_display_name() }}</a></p>
<p><a onclick="window.focusNode(window.getNodeFromId({{ object.id }}))">Reset on {{ object.get_display_name() }}</a></p>
<p>Self score: {{ object.current_star.mass }}</p>
<table style="width: initial;">
<tr>
@ -24,7 +29,7 @@
</tr>
{% for lane in lanes %}
<tr>
<td><a onclick="focus_node(get_node_from_id({{ lane.other_star_id }}))">Locate</a></td>
<td><a onclick="window.focusNode(window.getNodeFromId({{ lane.other_star_id }}))">Locate</a></td>
<td><a href="{{ url("galaxy:user", user_id=lane.other_star_id) }}">{{ lane.other_star_name }}</a></td>
<td>{{ lane.other_star_mass }}</td>
<td>{{ lane.distance }}</td>
@ -45,106 +50,13 @@
{% block script %}
{{ super() }}
<script src="{{ static('galaxy/js/three.min.js') }}" defer></script>
<script src="{{ static('galaxy/js/three-spritetext.min.js') }}" defer></script>
<script src="{{ static('galaxy/js/3d-force-graph.min.js') }}" defer></script>
<script src="{{ static('galaxy/js/d3-force-3d.min.js') }}" defer></script>
<script>
var Graph;
function get_node_from_id(id) {
return Graph.graphData().nodes.find(n => n.id === id);
}
function get_links_from_node_id(id) {
return Graph.graphData().links.filter(l => l.source.id === id || l.target.id === id);
}
function focus_node(node) {
highlightNodes.clear();
highlightLinks.clear();
hoverNode = node || null;
if (node) { // collect neighbors and links for highlighting
get_links_from_node_id(node.id).forEach(link => {
highlightLinks.add(link);
highlightNodes.add(link.source);
highlightNodes.add(link.target);
});
}
// refresh node and link display
Graph
.nodeThreeObject(Graph.nodeThreeObject())
.linkWidth(Graph.linkWidth())
.linkDirectionalParticles(Graph.linkDirectionalParticles());
// Aim at node from outside it
const distance = 42;
const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
const newPos = node.x || node.y || node.z
? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }
: { x: 0, y: 0, z: distance }; // special case if node is in (0,0,0)
Graph.cameraPosition(
newPos, // new position
node, // lookAt ({ x, y, z })
3000 // ms transition duration
);
}
const highlightNodes = new Set();
const highlightLinks = new Set();
let hoverNode = null;
document.addEventListener("DOMContentLoaded", () => {
var graph_div = document.getElementById('3d-graph');
Graph = ForceGraph3D();
Graph(graph_div);
Graph
.jsonUrl('{{ url("galaxy:data") }}')
.width(graph_div.parentElement.clientWidth > 1200 ? 1200 : graph_div.parentElement.clientWidth) // Not perfect at all. JS-fu master from the future, please fix this :-)
.height(1000)
.enableNodeDrag(false) // allow easier navigation
.onNodeClick(node => {
camera = Graph.cameraPosition();
var distance = Math.sqrt(Math.pow(node.x - camera.x, 2) + Math.pow(node.y - camera.y, 2) + Math.pow(node.z - camera.z, 2))
if (distance < 120 || highlightNodes.has(node)) {
focus_node(node);
}
})
.linkWidth(link => highlightLinks.has(link) ? 0.4 : 0.0)
.linkColor(link => highlightLinks.has(link) ? 'rgba(255,160,0,1)' : 'rgba(128,255,255,0.6)')
.linkVisibility(link => highlightLinks.has(link))
.nodeVisibility(node => highlightNodes.has(node) || node.mass > 4)
// .linkDirectionalParticles(link => highlightLinks.has(link) ? 3 : 1) // kinda buggy for now, and slows this a bit, but would be great to help visualize lanes
.linkDirectionalParticleWidth(0.2)
.linkDirectionalParticleSpeed(-0.006)
.nodeThreeObject(node => {
const sprite = new SpriteText(node.name);
sprite.material.depthWrite = false; // make sprite background transparent
sprite.color = highlightNodes.has(node) ? node === hoverNode ? 'rgba(200,0,0,1)' : 'rgba(255,160,0,0.8)' : 'rgba(0,255,255,0.2)';
sprite.textHeight = 2;
sprite.center = new THREE.Vector2(1.2, 0.5);
return sprite;
})
.onEngineStop( () => {
focus_node(get_node_from_id({{ object.id }}));
Graph.onEngineStop(() => {}); // don't call ourselves in a loop while moving the focus
window.loadGalaxy({
nodeId: {{ object.id }},
dataUrl: '{{ url("galaxy:data") }}',
});
});
// Set distance between stars
Graph.d3Force('link').distance(link => link.value);
// Set high masses nearer the center of the galaxy
// TODO: quick and dirty strength computation, this will need tuning.
Graph.d3Force('positionX', d3.forceX().strength(node => { return 1 - (1 / node.mass); }));
Graph.d3Force('positionY', d3.forceY().strength(node => { return 1 - (1 / node.mass); }));
Graph.d3Force('positionZ', d3.forceZ().strength(node => { return 1 - (1 / node.mass); }));
})
</script>
{% endblock %}

View File

@ -160,7 +160,7 @@ class TestGalaxyView(TestCase):
response = self.client.get(reverse("galaxy:user", args=[user.id]))
self.assertContains(
response,
f'<a onclick="focus_node(get_node_from_id({user.id}))">Reset on {user}</a>',
f'<a onclick="window.focusNode(window.getNodeFromId({user.id}))">Reset on {user}</a>',
status_code=200,
)

View File

@ -23,17 +23,9 @@
from django.urls import path
from galaxy.views import *
from galaxy.views import GalaxyDataView, GalaxyUserView
urlpatterns = [
path(
"<int:user_id>/",
GalaxyUserView.as_view(),
name="user",
),
path(
"data.json",
GalaxyDataView.as_view(),
name="data",
),
path("<int:user_id>/", GalaxyUserView.as_view(), name="user"),
path("data.json", GalaxyDataView.as_view(), name="data"),
]

View File

@ -14,7 +14,7 @@
#
from django.contrib import admin
from launderette.models import *
from launderette.models import Launderette, Machine, Slot, Token
@admin.register(Launderette)

View File

@ -51,18 +51,14 @@ class Launderette(models.Model):
unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"]
).first()
m = launderette_club.get_membership_for(user)
if m and m.role >= 9:
return True
return False
return bool(m and m.role >= 9)
def can_be_edited_by(self, user):
launderette_club = Club.objects.filter(
unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"]
).first()
m = launderette_club.get_membership_for(user)
if m and m.role >= 2:
return True
return False
return bool(m and m.role >= 2)
def can_be_viewed_by(self, user):
return user.is_subscribed
@ -113,9 +109,7 @@ class Machine(models.Model):
unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"]
).first()
m = launderette_club.get_membership_for(user)
if m and m.role >= 9:
return True
return False
return bool(m and m.role >= 9)
class Token(models.Model):
@ -164,15 +158,7 @@ class Token(models.Model):
unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"]
).first()
m = launderette_club.get_membership_for(user)
if m and m.role >= 9:
return True
return False
def is_avaliable(self):
if not self.borrow_date and not self.user:
return True
else:
return False
return bool(m and m.role >= 9)
class Slot(models.Model):

View File

@ -15,22 +15,28 @@
from django.urls import path
from launderette.views import *
from launderette.views import (
LaunderetteAdminView,
LaunderetteBookMainView,
LaunderetteBookView,
LaunderetteClickView,
LaunderetteCreateView,
LaunderetteEditView,
LaunderetteListView,
LaunderetteMainClickView,
LaunderetteMainView,
MachineCreateView,
MachineDeleteView,
MachineEditView,
SlotDeleteView,
)
urlpatterns = [
# views
path("", LaunderetteMainView.as_view(), name="launderette_main"),
path(
"slot/<int:slot_id>/delete/",
SlotDeleteView.as_view(),
name="delete_slot",
),
path("slot/<int:slot_id>/delete/", SlotDeleteView.as_view(), name="delete_slot"),
path("book/", LaunderetteBookMainView.as_view(), name="book_main"),
path(
"book/<int:launderette_id>/",
LaunderetteBookView.as_view(),
name="book_slot",
),
path("book/<int:launderette_id>/", LaunderetteBookView.as_view(), name="book_slot"),
path(
"<int:launderette_id>/click/",
LaunderetteMainClickView.as_view(),

View File

@ -19,7 +19,7 @@ from datetime import timezone as tz
from django import forms
from django.conf import settings
from django.db import DataError, transaction
from django.db import transaction
from django.template import defaultfilters
from django.urls import reverse_lazy
from django.utils import dateparse, timezone
@ -73,15 +73,15 @@ class LaunderetteBookView(CanViewMixin, DetailView):
self.machines = {}
with transaction.atomic():
self.object = self.get_object()
if "slot_type" in request.POST.keys():
if "slot_type" in request.POST:
self.slot_type = request.POST["slot_type"]
if "slot" in request.POST.keys() and request.user.is_authenticated:
if "slot" in request.POST and request.user.is_authenticated:
self.subscriber = request.user
if self.subscriber.is_subscribed:
self.date = dateparse.parse_datetime(request.POST["slot"]).replace(
tzinfo=tz.utc
)
if self.slot_type == "WASHING":
if self.slot_type in ["WASHING", "DRYING"]:
if self.check_slot(self.slot_type):
Slot(
user=self.subscriber,
@ -89,16 +89,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
machine=self.machines[self.slot_type],
type=self.slot_type,
).save()
elif self.slot_type == "DRYING":
if self.check_slot(self.slot_type):
Slot(
user=self.subscriber,
start_date=self.date,
machine=self.machines[self.slot_type],
type=self.slot_type,
).save()
else:
if self.check_slot("WASHING") and self.check_slot(
elif self.check_slot("WASHING") and self.check_slot(
"DRYING", self.date + timedelta(hours=1)
):
Slot(
@ -149,15 +140,17 @@ class LaunderetteBookView(CanViewMixin, DetailView):
):
free = False
if (
(
self.slot_type == "BOTH"
and self.check_slot("WASHING", h)
and self.check_slot("DRYING", h + timedelta(hours=1))
)
or self.slot_type == "WASHING"
and self.check_slot("WASHING", h)
or self.slot_type == "DRYING"
and self.check_slot("DRYING", h)
):
free = True
elif self.slot_type == "WASHING" and self.check_slot("WASHING", h):
free = True
elif self.slot_type == "DRYING" and self.check_slot("DRYING", h):
free = True
if free and datetime.now().replace(tzinfo=tz.utc) < h:
kwargs["planning"][date].append(h)
else:
@ -236,42 +229,39 @@ class ManageTokenForm(forms.Form):
token_list = cleaned_data["tokens"].strip(" \n\r").split(" ")
token_type = cleaned_data["token_type"]
self.data = {}
if cleaned_data["action"] == "BACK":
for t in token_list:
try:
tok = Token.objects.filter(
launderette=launderette, type=token_type, name=t
).first()
tok.borrow_date = None
tok.user = None
tok.save()
except:
if cleaned_data["action"] not in ["BACK", "ADD", "DEL"]:
return
tokens = list(
Token.objects.filter(
launderette=launderette, type=token_type, name__in=token_list
)
)
existing_names = {t.name for t in tokens}
if cleaned_data["action"] in ["BACK", "DEL"]:
for t in set(token_list) - existing_names:
self.add_error(
None,
_("Token %(token_name)s does not exists") % {"token_name": t},
)
elif cleaned_data["action"] == "ADD":
for t in token_list:
try:
Token(launderette=launderette, type=token_type, name=t).save()
except DataError as e:
self.add_error(None, e)
except:
self.add_error(
None,
_("Token %(token_name)s already exists") % {"token_name": t},
if cleaned_data["action"] == "BACK":
Token.objects.filter(id__in=[t.id for t in tokens]).update(
borrow_date=None, user=None
)
elif cleaned_data["action"] == "DEL":
for t in token_list:
try:
Token.objects.filter(
launderette=launderette, type=token_type, name=t
).delete()
except:
Token.objects.filter(id__in=[t.id for t in tokens]).delete()
elif cleaned_data["action"] == "ADD":
for name in existing_names:
self.add_error(
None,
_("Token %(token_name)s does not exists") % {"token_name": t},
_("Token %(token_name)s already exists") % {"token_name": name},
)
for t in token_list:
if t == "":
self.add_error(None, _("Token name can not be blank"))
else:
Token(launderette=launderette, type=token_type, name=t).save()
class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
@ -288,13 +278,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
return super().post(request, *args, **kwargs)
form.launderette = self.object
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""We handle here the redirection, passing the user id of the asked customer."""
@ -353,7 +337,7 @@ class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView):
kwargs["counter"] = self.object.counter
kwargs["form"] = self.get_form()
kwargs["barmen"] = [self.request.user]
if "last_basket" in self.request.session.keys():
if "last_basket" in self.request.session:
kwargs["last_basket"] = self.request.session.pop("last_basket", None)
kwargs["last_customer"] = self.request.session.pop("last_customer", None)
kwargs["last_total"] = self.request.session.pop("last_total", None)
@ -479,7 +463,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
def get_context_data(self, **kwargs):
"""We handle here the login form for the barman."""
kwargs = super().get_context_data(**kwargs)
if "form" not in kwargs.keys():
if "form" not in kwargs:
kwargs["form"] = self.get_form()
kwargs["counter"] = self.object.counter
kwargs["customer"] = self.customer
@ -519,7 +503,7 @@ class MachineCreateView(CanCreateMixin, CreateView):
def get_initial(self):
ret = super().get_initial()
if "launderette" in self.request.GET.keys():
if "launderette" in self.request.GET:
obj = Launderette.objects.filter(
id=int(self.request.GET["launderette"])
).first()

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-09 11:50+0200\n"
"POT-Creation-Date: 2024-10-16 02:19+0200\n"
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@ -22,87 +22,95 @@ msgstr ""
msgid "captured.%s"
msgstr "capture.%s"
#: core/static/webpack/easymde-index.js:32
#: core/static/webpack/ajax-select-index.ts:73
msgid "You need to type %(number)s more characters"
msgstr "Vous devez taper %(number)s caractères de plus"
#: core/static/webpack/ajax-select-index.ts:76
msgid "No results found"
msgstr "Aucun résultat trouvé"
#: core/static/webpack/easymde-index.ts:31
msgid "Heading"
msgstr "Titre"
#: core/static/webpack/easymde-index.js:38
#: core/static/webpack/easymde-index.ts:37
msgid "Italic"
msgstr "Italique"
#: core/static/webpack/easymde-index.js:44
#: core/static/webpack/easymde-index.ts:43
msgid "Bold"
msgstr "Gras"
#: core/static/webpack/easymde-index.js:50
#: core/static/webpack/easymde-index.ts:49
msgid "Strikethrough"
msgstr "Barré"
#: core/static/webpack/easymde-index.js:59
#: core/static/webpack/easymde-index.ts:58
msgid "Underline"
msgstr "Souligné"
#: core/static/webpack/easymde-index.js:68
#: core/static/webpack/easymde-index.ts:67
msgid "Superscript"
msgstr "Exposant"
#: core/static/webpack/easymde-index.js:77
#: core/static/webpack/easymde-index.ts:76
msgid "Subscript"
msgstr "Indice"
#: core/static/webpack/easymde-index.js:83
#: core/static/webpack/easymde-index.ts:82
msgid "Code"
msgstr "Code"
#: core/static/webpack/easymde-index.js:90
#: core/static/webpack/easymde-index.ts:89
msgid "Quote"
msgstr "Citation"
#: core/static/webpack/easymde-index.js:96
#: core/static/webpack/easymde-index.ts:95
msgid "Unordered list"
msgstr "Liste non ordonnée"
#: core/static/webpack/easymde-index.js:102
#: core/static/webpack/easymde-index.ts:101
msgid "Ordered list"
msgstr "Liste ordonnée"
#: core/static/webpack/easymde-index.js:109
#: core/static/webpack/easymde-index.ts:108
msgid "Insert link"
msgstr "Insérer lien"
#: core/static/webpack/easymde-index.js:115
#: core/static/webpack/easymde-index.ts:114
msgid "Insert image"
msgstr "Insérer image"
#: core/static/webpack/easymde-index.js:121
#: core/static/webpack/easymde-index.ts:120
msgid "Insert table"
msgstr "Insérer tableau"
#: core/static/webpack/easymde-index.js:128
#: core/static/webpack/easymde-index.ts:127
msgid "Clean block"
msgstr "Nettoyer bloc"
#: core/static/webpack/easymde-index.js:135
#: core/static/webpack/easymde-index.ts:134
msgid "Toggle preview"
msgstr "Activer la prévisualisation"
#: core/static/webpack/easymde-index.js:141
#: core/static/webpack/easymde-index.ts:140
msgid "Toggle side by side"
msgstr "Activer la vue côte à côte"
#: core/static/webpack/easymde-index.js:147
#: core/static/webpack/easymde-index.ts:146
msgid "Toggle fullscreen"
msgstr "Activer le plein écran"
#: core/static/webpack/easymde-index.js:154
#: core/static/webpack/easymde-index.ts:153
msgid "Markdown guide"
msgstr "Guide markdown"
#: core/static/webpack/user/family-graph-index.js:222
#: core/static/webpack/user/family-graph-index.js:233
msgid "family_tree.%(extension)s"
msgstr "arbre_genealogique.%(extension)s"
#: core/static/webpack/user/pictures-index.js:67
#: core/static/webpack/user/pictures-index.js:76
msgid "pictures.%(extension)s"
msgstr "photos.%(extension)s"
@ -110,10 +118,10 @@ msgstr "photos.%(extension)s"
msgid "Incorrect value"
msgstr "Valeur incorrecte"
#: sas/static/sas/js/viewer.js:205
#: sas/static/webpack/sas/viewer-index.ts:271
msgid "Couldn't moderate picture"
msgstr "Il n'a pas été possible de modérer l'image"
#: sas/static/sas/js/viewer.js:217
#: sas/static/webpack/sas/viewer-index.ts:284
msgid "Couldn't delete picture"
msgstr "Il n'a pas été possible de supprimer l'image"

View File

@ -23,7 +23,12 @@
from django.urls import path
from matmat.views import *
from matmat.views import (
SearchClearFormView,
SearchNormalFormView,
SearchQuickFormView,
SearchReverseFormView,
)
urlpatterns = [
path("", SearchNormalFormView.as_view(), name="search"),

View File

@ -71,15 +71,15 @@ class SearchForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for key in self.fields.keys():
for key in self.fields:
self.fields[key].required = False
@property
def cleaned_data_json(self):
data = self.cleaned_data
for key in data.keys():
if key in ("date_of_birth", "phone") and data[key] is not None:
data[key] = str(data[key])
for key, val in data.items():
if key in ("date_of_birth", "phone") and val is not None:
data[key] = str(val)
return data
@ -98,10 +98,7 @@ class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
self.session = request.session
self.last_search = self.session.get("matmat_search_result", str([]))
self.last_search = literal_eval(self.last_search)
if "valid_form" in kwargs.keys():
self.valid_form = kwargs["valid_form"]
else:
self.valid_form = None
self.valid_form = kwargs.get("valid_form")
self.init_query = self.model.objects
self.can_see_hidden = True
@ -202,8 +199,8 @@ class SearchClearFormView(FormerSubscriberMixin, View):
def dispatch(self, request, *args, **kwargs):
super().dispatch(request, *args, **kwargs)
if "matmat_search_form" in request.session.keys():
if "matmat_search_form" in request.session:
request.session.pop("matmat_search_form")
if "matmat_search_result" in request.session.keys():
if "matmat_search_result" in request.session:
request.session.pop("matmat_search_result")
return HttpResponseRedirect(reverse("matmat:search"))

656
package-lock.json generated
View File

@ -11,23 +11,32 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0",
"@hey-api/client-fetch": "^0.4.0",
"@sentry/browser": "^8.34.0",
"@zip.js/zip.js": "^2.7.52",
"3d-force-graph": "^1.73.4",
"alpinejs": "^3.14.1",
"chart.js": "^4.4.4",
"cytoscape": "^3.30.2",
"cytoscape-cxtmenu": "^3.5.0",
"cytoscape-klay": "^3.1.4",
"d3-force-3d": "^3.0.5",
"easymde": "^2.18.0",
"glob": "^11.0.0",
"jquery": "^3.7.1",
"jquery-ui": "^1.14.0",
"jquery.shorten": "^1.0.0",
"native-file-system-adapter": "^3.0.1"
"native-file-system-adapter": "^3.0.1",
"three": "^0.169.0",
"three-spritetext": "^1.9.0",
"tom-select": "^2.3.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@biomejs/biome": "1.9.3",
"@hey-api/openapi-ts": "^0.53.8",
"@types/alpinejs": "^3.13.10",
"@types/jquery": "^3.5.31",
"babel-loader": "^9.2.1",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
@ -38,6 +47,7 @@
"ts-loader": "^9.5.1",
"typescript": "^5.6.3",
"webpack": "^5.94.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4"
}
},
@ -1790,7 +1800,6 @@
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
"integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@ -2160,6 +2169,24 @@
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true
},
"node_modules/@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
"node_modules/@orchidjs/sifter": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@orchidjs/sifter/-/sifter-1.0.3.tgz",
"integrity": "sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g==",
"dependencies": {
"@orchidjs/unicode-variants": "^1.0.4"
}
},
"node_modules/@orchidjs/unicode-variants": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz",
"integrity": "sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ=="
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -2169,6 +2196,114 @@
"node": ">=14"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.28",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
"dev": true
},
"node_modules/@sentry-internal/browser-utils": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.34.0.tgz",
"integrity": "sha512-4AcYOzPzD1tL5eSRQ/GpKv5enquZf4dMVUez99/Bh3va8qiJrNP55AcM7UzZ7WZLTqKygIYruJTU5Zu2SpEAPQ==",
"dependencies": {
"@sentry/core": "8.34.0",
"@sentry/types": "8.34.0",
"@sentry/utils": "8.34.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.34.0.tgz",
"integrity": "sha512-aYSM2KPUs0FLPxxbJCFSwCYG70VMzlT04xepD1Y/tTlPPOja/02tSv2tyOdZbv8Uw7xslZs3/8Lhj74oYcTBxw==",
"dependencies": {
"@sentry/core": "8.34.0",
"@sentry/types": "8.34.0",
"@sentry/utils": "8.34.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.34.0.tgz",
"integrity": "sha512-EoMh9NYljNewZK1quY23YILgtNdGgrkzJ9TPsj6jXUG0LZ0Q7N7eFWd0xOEDBvFxrmI3cSXF1i4d1sBb+eyKRw==",
"dependencies": {
"@sentry-internal/browser-utils": "8.34.0",
"@sentry/core": "8.34.0",
"@sentry/types": "8.34.0",
"@sentry/utils": "8.34.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.34.0.tgz",
"integrity": "sha512-x8KhZcCDpbKHqFOykYXiamX6x0LRxv6N1OJHoH+XCrMtiDBZr4Yo30d/MaS6rjmKGMtSRij30v+Uq+YWIgxUrg==",
"dependencies": {
"@sentry-internal/replay": "8.34.0",
"@sentry/core": "8.34.0",
"@sentry/types": "8.34.0",
"@sentry/utils": "8.34.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/browser": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.34.0.tgz",
"integrity": "sha512-3HHG2NXxzHq1lVmDy2uRjYjGNf9NsJsTPlOC70vbQdOb+S49EdH/XMPy+J3ruIoyv6Cu0LwvA6bMOM6rHZOgNQ==",
"dependencies": {
"@sentry-internal/browser-utils": "8.34.0",
"@sentry-internal/feedback": "8.34.0",
"@sentry-internal/replay": "8.34.0",
"@sentry-internal/replay-canvas": "8.34.0",
"@sentry/core": "8.34.0",
"@sentry/types": "8.34.0",
"@sentry/utils": "8.34.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/core": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.34.0.tgz",
"integrity": "sha512-adrXCTK/zsg5pJ67lgtZqdqHvyx6etMjQW3P82NgWdj83c8fb+zH+K79Z47pD4zQjX0ou2Ws5nwwi4wJbz4bfA==",
"dependencies": {
"@sentry/types": "8.34.0",
"@sentry/utils": "8.34.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/types": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.34.0.tgz",
"integrity": "sha512-zLRc60CzohGCo6zNsNeQ9JF3SiEeRE4aDCP9fDDdIVCOKovS+mn1rtSip0qd0Vp2fidOu0+2yY0ALCz1A3PJSQ==",
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/utils": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.34.0.tgz",
"integrity": "sha512-W1KoRlFUjprlh3t86DZPFxLfM6mzjRzshVfMY7vRlJFymBelJsnJ3A1lPeBZM9nCraOSiw6GtOWu6k5BAkiGIg==",
"dependencies": {
"@sentry/types": "8.34.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -2184,6 +2319,17 @@
"node": ">=10.13.0"
}
},
"node_modules/@tweenjs/tween.js": {
"version": "25.0.0",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A=="
},
"node_modules/@types/alpinejs": {
"version": "3.13.10",
"resolved": "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.10.tgz",
"integrity": "sha512-ah53tF6mWuuwerpDE7EHwbZErNDJQlsLISPqJhYj2RZ9nuTYbRknSkqebUd3igkhLIZKkPa7IiXjSn9qsU9O2w==",
"dev": true
},
"node_modules/@types/codemirror": {
"version": "5.60.15",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz",
@ -2221,6 +2367,15 @@
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/jquery": {
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz",
"integrity": "sha512-rf/iB+cPJ/YZfMwr+FVuQbm7IaWC4y3FVYfVDxRGqmUCFjjPII0HWaP0vTPJGp6m4o13AXySCcMbWfrWtBFAKw==",
"dev": true,
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -2241,6 +2396,12 @@
"undici-types": "~6.19.2"
}
},
"node_modules/@types/sizzle": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
"integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
"dev": true
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
@ -2489,6 +2650,29 @@
"node": ">=16.5.0"
}
},
"node_modules/3d-force-graph": {
"version": "1.73.4",
"resolved": "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.73.4.tgz",
"integrity": "sha512-eMHZ1LVzh9APLv+An0AXz2dVPwasJlqAnJ61ABlb1qaO6DYuqIUTTErh0DN/24nIWJu1jCim2WiVujzz7slnWQ==",
"dependencies": {
"accessor-fn": "1",
"kapsule": "1",
"three": ">=0.118 <1",
"three-forcegraph": "1",
"three-render-objects": "^1.29"
},
"engines": {
"node": ">=12"
}
},
"node_modules/accessor-fn": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.1.tgz",
"integrity": "sha512-zZpFYBqIL1Aqg+f2qmYHJ8+yIZF7/tP6PUGx2/QM0uGPSO5UegpinmkNwDohxWtOj586BpMPVRUjce2HI6xB3A==",
"engines": {
"node": ">=12"
}
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@ -2510,6 +2694,18 @@
"acorn": "^8"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -2900,6 +3096,17 @@
"node": ">=8"
}
},
"node_modules/chart.js": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz",
"integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
@ -3419,6 +3626,159 @@
"cytoscape": "^3.2.0"
}
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-binarytree": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw=="
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-force-3d": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.5.tgz",
"integrity": "sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==",
"dependencies": {
"d3-binarytree": "1",
"d3-dispatch": "1 - 3",
"d3-octree": "1",
"d3-quadtree": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-octree": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.0.2.tgz",
"integrity": "sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA=="
},
"node_modules/d3-quadtree": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale-chromatic": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
"dependencies": {
"d3-color": "1 - 3",
"d3-interpolate": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"engines": {
"node": ">=12"
}
},
"node_modules/data-joint": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/data-joint/-/data-joint-1.3.1.tgz",
"integrity": "sha512-tMK0m4OVGqiA3zkn8JmO6YAqD8UwJqIAx4AAwFl1SKTtKAqcXePuT+n2aayiX9uITtlN3DFtKKTOxJRUc2+HvQ==",
"dependencies": {
"index-array-by": "^1.4.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
"dev": true
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@ -3515,6 +3875,12 @@
"url": "https://dotenvx.com"
}
},
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
"dev": true
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -4026,6 +4392,21 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/gzip-size": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
"integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
"dev": true,
"dependencies": {
"duplexer": "^0.1.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
@ -4068,6 +4449,12 @@
"node": ">= 0.4"
}
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
@ -4120,6 +4507,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/index-array-by": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
"integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==",
"engines": {
"node": ">=12"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/interpret": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
@ -4328,6 +4731,17 @@
"node": ">=6"
}
},
"node_modules/kapsule": {
"version": "1.14.6",
"resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.14.6.tgz",
"integrity": "sha512-wSi6tHNOfXrIK2Pvv6BhZ9ukzhbp+XZlOOPWSVGUbqfFsnnli4Eq8FN6TaWJv2e17sY5+fKYVxa4DP2oPGlKhg==",
"dependencies": {
"lodash-es": "4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@ -4375,6 +4789,11 @@
"node": ">=8"
}
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -4629,6 +5048,15 @@
"ufo": "^1.5.4"
}
},
"node_modules/mrmime": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
"integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -4680,6 +5108,39 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"node_modules/ngraph.events": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz",
"integrity": "sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ=="
},
"node_modules/ngraph.forcelayout": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz",
"integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==",
"dependencies": {
"ngraph.events": "^1.0.0",
"ngraph.merge": "^1.0.0",
"ngraph.random": "^1.0.0"
}
},
"node_modules/ngraph.graph": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.0.1.tgz",
"integrity": "sha512-VFsQ+EMkT+7lcJO1QP8Ik3w64WbHJl27Q53EO9hiFU9CRyxJ8HfcXtfWz/U8okuoYKDctbciL6pX3vG5dt1rYA==",
"dependencies": {
"ngraph.events": "^1.2.1"
}
},
"node_modules/ngraph.merge": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz",
"integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg=="
},
"node_modules/ngraph.random": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.1.0.tgz",
"integrity": "sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw=="
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@ -4791,6 +5252,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/opener": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"dev": true,
"bin": {
"opener": "bin/opener-bin.js"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@ -4923,6 +5393,17 @@
"pathe": "^1.1.2"
}
},
"node_modules/polished": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz",
"integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==",
"dependencies": {
"@babel/runtime": "^7.17.8"
},
"engines": {
"node": ">=10"
}
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
@ -5514,8 +5995,7 @@
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regenerator-transform": {
"version": "0.15.2",
@ -5718,6 +6198,20 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
"integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
"dev": true,
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
"totalist": "^3.0.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -6036,6 +6530,67 @@
}
}
},
"node_modules/three": {
"version": "0.169.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.169.0.tgz",
"integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w=="
},
"node_modules/three-forcegraph": {
"version": "1.41.15",
"resolved": "https://registry.npmjs.org/three-forcegraph/-/three-forcegraph-1.41.15.tgz",
"integrity": "sha512-E1j6bKt7lWg9t/ERdEiuxYfPbAioTCd9RG2bgqyC0yM3rwkBqn5VZN3fvb7umaOuTB1Tqpq6m07iVfJSfzTnCQ==",
"dependencies": {
"accessor-fn": "1",
"d3-array": "1 - 3",
"d3-force-3d": "2 - 3",
"d3-scale": "1 - 4",
"d3-scale-chromatic": "1 - 3",
"data-joint": "1",
"kapsule": "1",
"ngraph.forcelayout": "3",
"ngraph.graph": "20",
"tinycolor2": "1"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"three": ">=0.118.3"
}
},
"node_modules/three-render-objects": {
"version": "1.29.5",
"resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.29.5.tgz",
"integrity": "sha512-OLtETrjF184NuaaI/vpRlIP9FxVNAgBBCgWYXhGFUDnPdl/2iX8rialUPGA1gEXvOTiKyepArVgm1LUkJw15rQ==",
"dependencies": {
"@tweenjs/tween.js": "18 - 25",
"accessor-fn": "1",
"kapsule": "1",
"polished": "4"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"three": "*"
}
},
"node_modules/three-spritetext": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/three-spritetext/-/three-spritetext-1.9.0.tgz",
"integrity": "sha512-+dMrxBsxTu5OviykIg5jTMry5TQ8u5yuS9zKH0mWElyldoFGdegEkIm71kDk34bxBp/NQhRLW+iom1b/GMTioA==",
"engines": {
"node": ">=12"
},
"peerDependencies": {
"three": ">=0.86.0"
}
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@ -6057,6 +6612,31 @@
"node": ">=8.0"
}
},
"node_modules/tom-select": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.3.1.tgz",
"integrity": "sha512-QS4vnOcB6StNGqX4sGboGXL2fkhBF2gIBB+8Hwv30FZXYPn0CyYO8kkdATRvwfCTThxiR4WcXwKJZ3cOmtI9eg==",
"dependencies": {
"@orchidjs/sifter": "^1.0.3",
"@orchidjs/unicode-variants": "^1.0.4"
},
"engines": {
"node": "*"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tom-select"
}
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/ts-loader": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
@ -6282,6 +6862,53 @@
}
}
},
"node_modules/webpack-bundle-analyzer": {
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz",
"integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==",
"dev": true,
"dependencies": {
"@discoveryjs/json-ext": "0.5.7",
"acorn": "^8.0.4",
"acorn-walk": "^8.0.0",
"commander": "^7.2.0",
"debounce": "^1.2.1",
"escape-string-regexp": "^4.0.0",
"gzip-size": "^6.0.0",
"html-escaper": "^2.0.2",
"opener": "^1.5.2",
"picocolors": "^1.0.0",
"sirv": "^2.0.3",
"ws": "^7.3.1"
},
"bin": {
"webpack-bundle-analyzer": "lib/bin/analyzer.js"
},
"engines": {
"node": ">= 10.13.0"
}
},
"node_modules/webpack-bundle-analyzer/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/webpack-cli": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
@ -6469,6 +7096,27 @@
"node": ">=8"
}
},
"node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true,
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -7,6 +7,8 @@
"compile": "webpack --mode production",
"compile-dev": "webpack --mode development",
"serve": "webpack --mode development --watch",
"analyse-dev": "webpack --config webpack.analyze.config.js --mode development",
"analyse-prod": "webpack --config webpack.analyze.config.js --mode production",
"check": "biome check --write"
},
"keywords": [],
@ -23,6 +25,8 @@
"@babel/preset-env": "^7.25.4",
"@biomejs/biome": "1.9.3",
"@hey-api/openapi-ts": "^0.53.8",
"@types/alpinejs": "^3.13.10",
"@types/jquery": "^3.5.31",
"babel-loader": "^9.2.1",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
@ -33,21 +37,29 @@
"ts-loader": "^9.5.1",
"typescript": "^5.6.3",
"webpack": "^5.94.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0",
"@hey-api/client-fetch": "^0.4.0",
"@sentry/browser": "^8.34.0",
"@zip.js/zip.js": "^2.7.52",
"3d-force-graph": "^1.73.4",
"alpinejs": "^3.14.1",
"chart.js": "^4.4.4",
"cytoscape": "^3.30.2",
"cytoscape-cxtmenu": "^3.5.0",
"cytoscape-klay": "^3.1.4",
"d3-force-3d": "^3.0.5",
"easymde": "^2.18.0",
"glob": "^11.0.0",
"jquery": "^3.7.1",
"jquery-ui": "^1.14.0",
"jquery.shorten": "^1.0.0",
"native-file-system-adapter": "^3.0.1"
"native-file-system-adapter": "^3.0.1",
"three": "^0.169.0",
"three-spritetext": "^1.9.0",
"tom-select": "^2.3.1"
}
}

View File

@ -32,7 +32,10 @@ class Migration(migrations.Migration):
unique=True,
validators=[
django.core.validators.RegexValidator(
message="The code of an UV must only contains uppercase characters without accent and numbers",
message=(
"The code of an UV must only contains "
"uppercase characters without accent and numbers"
),
regex="([A-Z0-9]+)",
)
],

View File

@ -45,7 +45,8 @@ class UV(models.Model):
validators.RegexValidator(
regex="([A-Z0-9]+)",
message=_(
"The code of an UV must only contains uppercase characters without accent and numbers"
"The code of an UV must only contains "
"uppercase characters without accent and numbers"
),
)
],

View File

@ -27,7 +27,10 @@ class TestUVSearch(TestCase):
semester="AUTUMN",
department="GI",
manager="francky",
title="Programmation Orientée Objet: Concepts fondamentaux et mise en pratique avec le langage C++",
title=(
"Programmation Orientée Objet: "
"Concepts fondamentaux et mise en pratique avec le langage C++"
),
),
uv_recipe.prepare(
code="MT01",

View File

@ -381,7 +381,9 @@ class TestUVCommentCreationAndDisplay(TestCase):
self.assertContains(
response,
_(
"You already posted a comment on this UV. If you want to comment again, please modify or delete your previous comment."
"You already posted a comment on this UV. "
"If you want to comment again, "
"please modify or delete your previous comment."
),
)

View File

@ -23,7 +23,17 @@
from django.urls import path
from pedagogy.views import *
from pedagogy.views import (
UVCommentDeleteView,
UVCommentReportCreateView,
UVCommentUpdateView,
UVCreateView,
UVDeleteView,
UVDetailFormView,
UVGuideView,
UVModerationFormView,
UVUpdateView,
)
urlpatterns = [
# Urls displaying the actual application for visitors

View File

@ -23,7 +23,7 @@
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.views.generic import (
@ -193,18 +193,12 @@ class UVModerationFormView(FormView):
def form_valid(self, form):
form_clean = form.clean()
for report in form_clean.get("accepted_reports", []):
try:
report.comment.delete() # Delete the related comment
except ObjectDoesNotExist:
# To avoid errors when two reports points the same comment
pass
for report in form_clean.get("denied_reports", []):
try:
report.delete() # Delete the report itself
except ObjectDoesNotExist:
# To avoid errors when two reports points the same comment
pass
accepted = form_clean.get("accepted_reports", [])
if len(accepted) > 0: # delete the reported comments
UVComment.objects.filter(reports__in=accepted).delete()
denied = form_clean.get("denied_reports", [])
if len(denied) > 0: # delete the comments themselves
UVCommentReport.objects.filter(id__in={d.id for d in denied}).delete()
return super().form_valid(form)
def get_success_url(self):

284
poetry.lock generated
View File

@ -343,73 +343,73 @@ files = [
[[package]]
name = "coverage"
version = "7.6.2"
version = "7.6.3"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.9"
files = [
{file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"},
{file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"},
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"},
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"},
{file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"},
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"},
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"},
{file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"},
{file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"},
{file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"},
{file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"},
{file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"},
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"},
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"},
{file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"},
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"},
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"},
{file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"},
{file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"},
{file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"},
{file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"},
{file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"},
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"},
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"},
{file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"},
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"},
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"},
{file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"},
{file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"},
{file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"},
{file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"},
{file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"},
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"},
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"},
{file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"},
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"},
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"},
{file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"},
{file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"},
{file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"},
{file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"},
{file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"},
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"},
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"},
{file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"},
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"},
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"},
{file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"},
{file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"},
{file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"},
{file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"},
{file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"},
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"},
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"},
{file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"},
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"},
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"},
{file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"},
{file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"},
{file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"},
{file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"},
{file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"},
{file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"},
{file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"},
{file = "coverage-7.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d"},
{file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c"},
{file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a"},
{file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e"},
{file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc"},
{file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e"},
{file = "coverage-7.6.3-cp310-cp310-win32.whl", hash = "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007"},
{file = "coverage-7.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd"},
{file = "coverage-7.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b"},
{file = "coverage-7.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba"},
{file = "coverage-7.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38"},
{file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549"},
{file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2"},
{file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175"},
{file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b"},
{file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f"},
{file = "coverage-7.6.3-cp311-cp311-win32.whl", hash = "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97"},
{file = "coverage-7.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6"},
{file = "coverage-7.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6"},
{file = "coverage-7.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f"},
{file = "coverage-7.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234"},
{file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f"},
{file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4"},
{file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3"},
{file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83"},
{file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167"},
{file = "coverage-7.6.3-cp312-cp312-win32.whl", hash = "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd"},
{file = "coverage-7.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6"},
{file = "coverage-7.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6"},
{file = "coverage-7.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929"},
{file = "coverage-7.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990"},
{file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4"},
{file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39"},
{file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21"},
{file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b"},
{file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4"},
{file = "coverage-7.6.3-cp313-cp313-win32.whl", hash = "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f"},
{file = "coverage-7.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce"},
{file = "coverage-7.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3"},
{file = "coverage-7.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3"},
{file = "coverage-7.6.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d"},
{file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38"},
{file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd"},
{file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92"},
{file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5"},
{file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91"},
{file = "coverage-7.6.3-cp313-cp313t-win32.whl", hash = "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43"},
{file = "coverage-7.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0"},
{file = "coverage-7.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2"},
{file = "coverage-7.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba"},
{file = "coverage-7.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c"},
{file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40"},
{file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e"},
{file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6"},
{file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb"},
{file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13"},
{file = "coverage-7.6.3-cp39-cp39-win32.whl", hash = "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3"},
{file = "coverage-7.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d"},
{file = "coverage-7.6.3-pp39.pp310-none-any.whl", hash = "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181"},
{file = "coverage-7.6.3.tar.gz", hash = "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054"},
]
[package.extras]
@ -491,13 +491,13 @@ tests = ["noseofyeti[black] (==2.4.9)", "pytest (==8.3.2)"]
[[package]]
name = "distlib"
version = "0.3.8"
version = "0.3.9"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
]
[[package]]
@ -814,13 +814,13 @@ dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
name = "griffe"
version = "1.3.2"
version = "1.4.1"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "griffe-1.3.2-py3-none-any.whl", hash = "sha256:2e34b5e46507d615915c8e6288bb1a2234bd35dee44d01e40a2bc2f25bd4d10c"},
{file = "griffe-1.3.2.tar.gz", hash = "sha256:1ec50335aa507ed2445f2dd45a15c9fa3a45f52c9527e880571dfc61912fd60c"},
{file = "griffe-1.4.1-py3-none-any.whl", hash = "sha256:84295ee0b27743bd880aea75632830ef02ded65d16124025e4c263bb826ab645"},
{file = "griffe-1.4.1.tar.gz", hash = "sha256:911a201b01dc92e08c0e84c38a301e9da5ec067f00e7d9f2e39bc24dbfa3c176"},
]
[package.dependencies]
@ -1283,13 +1283,13 @@ cache = ["platformdirs"]
[[package]]
name = "mkdocs-material"
version = "9.5.39"
version = "9.5.40"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material-9.5.39-py3-none-any.whl", hash = "sha256:0f2f68c8db89523cb4a59705cd01b4acd62b2f71218ccb67e1e004e560410d2b"},
{file = "mkdocs_material-9.5.39.tar.gz", hash = "sha256:25faa06142afa38549d2b781d475a86fb61de93189f532b88e69bf11e5e5c3be"},
{file = "mkdocs_material-9.5.40-py3-none-any.whl", hash = "sha256:8e7a16ada34e79a7b6459ff2602584222f522c738b6a023d1bea853d5049da6f"},
{file = "mkdocs_material-9.5.40.tar.gz", hash = "sha256:b69d70e667ec51fc41f65e006a3184dd00d95b2439d982cb1586e4c018943156"},
]
[package.dependencies]
@ -1323,13 +1323,13 @@ files = [
[[package]]
name = "mkdocstrings"
version = "0.26.1"
version = "0.26.2"
description = "Automatic documentation from sources, for MkDocs."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf"},
{file = "mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33"},
{file = "mkdocstrings-0.26.2-py3-none-any.whl", hash = "sha256:1248f3228464f3b8d1a15bd91249ce1701fe3104ac517a5f167a0e01ca850ba5"},
{file = "mkdocstrings-0.26.2.tar.gz", hash = "sha256:34a8b50f1e6cfd29546c6c09fbe02154adfb0b361bb758834bf56aa284ba876e"},
]
[package.dependencies]
@ -1349,13 +1349,13 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
[[package]]
name = "mkdocstrings-python"
version = "1.11.1"
version = "1.12.1"
description = "A Python handler for mkdocstrings."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af"},
{file = "mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322"},
{file = "mkdocstrings_python-1.12.1-py3-none-any.whl", hash = "sha256:205244488199c9aa2a39787ad6a0c862d39b74078ea9aa2be817bc972399563f"},
{file = "mkdocstrings_python-1.12.1.tar.gz", hash = "sha256:60d6a5ca912c9af4ad431db6d0111ce9f79c6c48d33377dde6a05a8f5f48d792"},
]
[package.dependencies]
@ -1365,13 +1365,13 @@ mkdocstrings = ">=0.26"
[[package]]
name = "model-bakery"
version = "1.19.5"
version = "1.20.0"
description = "Smart object creation facility for Django."
optional = false
python-versions = ">=3.8"
files = [
{file = "model_bakery-1.19.5-py3-none-any.whl", hash = "sha256:09ecbbf124d32614339581b642c82ac4a73147442f598c7bad23eece24187e5c"},
{file = "model_bakery-1.19.5.tar.gz", hash = "sha256:37cece544a33f8899ed8f0488cd6a9d2b0b6925e7b478a4ff2786dece8c63745"},
{file = "model_bakery-1.20.0-py3-none-any.whl", hash = "sha256:875326466f5982ee8f0281abdfa774d78893d5473562575dfd5a9304ac7c5b8c"},
{file = "model_bakery-1.20.0.tar.gz", hash = "sha256:ec9dc846b9a00b20f92df38fac310263323ab61b59b6eeebf77a4aefb0412724"},
]
[package.dependencies]
@ -1599,13 +1599,13 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.8.0"
version = "4.0.1"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
files = [
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
{file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"},
{file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"},
]
[package.dependencies]
@ -2192,58 +2192,70 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rjsmin"
version = "1.2.2"
version = "1.2.3"
description = "Javascript Minifier"
optional = false
python-versions = "*"
files = [
{file = "rjsmin-1.2.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4420107304ba7a00b5b9b56cdcd166b9876b34e626829fc4552c85d8fdc3737a"},
{file = "rjsmin-1.2.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:155a2f3312c1f8c6cec7b5080581cafc761dc0e41d64bfb5d46a772c5230ded8"},
{file = "rjsmin-1.2.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:88fcb58d65f88cbfa752d51c1ebe5845553f9706def6d9671e98283411575e3e"},
{file = "rjsmin-1.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6eae13608b88f4ce32e0557c8fdef58e69bb4d293182202a03e800f0d33b5268"},
{file = "rjsmin-1.2.2-cp310-cp310-manylinux1_i686.whl", hash = "sha256:81f92fb855fb613ebd04a6d6d46483e71fe3c4f22042dc30dcc938fbd748e59c"},
{file = "rjsmin-1.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:897db9bf25538047e9388951d532dc291a629b5d041180a8a1a8c102e9d44b90"},
{file = "rjsmin-1.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:5938af8c46734f92f74fdc4d0b6324137c0e09f0a8c3825c83e4cfca1b532e40"},
{file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0424a7b9096fa2b0ab577c4dc7acd683e6cfb5c718ad39a9fb293cb6cbaba95b"},
{file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1714ed93c2bd40c5f970905d2eeda4a6844e09087ae11277d4d43b3e68c32a47"},
{file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:35596fa6d2d44a5471715c464657123995da78aa6f79bccfbb4b8d6ff7d0a4b4"},
{file = "rjsmin-1.2.2-cp311-cp311-manylinux1_i686.whl", hash = "sha256:3968667158948355b9a62e9641497aac7ac069c076a595e93199d0fe3a40217a"},
{file = "rjsmin-1.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:d07d14354694f6a47f572f2aa2a1ad74b76723e62a0d2b6df796138b71888247"},
{file = "rjsmin-1.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:a78dfa6009235b902454ac53264252b7b94f1e43e3a9e97c4cadae88e409b882"},
{file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9b7a45001e58243a455d11d2de925cadb8c2a0dc737001de646a0f4d90cf0034"},
{file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:86c5e657b74b6c9482bb96f18a79d61750f4e8204759cce179f7eb17d395c683"},
{file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c2c30b86c7232443a4a726e1bbee34f800556e581e95fc07194ecbf8e02d1d2"},
{file = "rjsmin-1.2.2-cp312-cp312-manylinux1_i686.whl", hash = "sha256:8982c3ef27fac26dd6b7d0c55ae98fa550fee72da2db010b87211e4b5dd78a67"},
{file = "rjsmin-1.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:3fc27ae4ece99e2c994cd79df2f0d3f7ac650249f632d19aa8ce85118e33bf0f"},
{file = "rjsmin-1.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:41113d8d6cae7f7406b30143cc49cc045bbb3fadc2f28df398cea30e1daa60b1"},
{file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3aa09a89b2b7aa2b9251329fe0c3e36c2dc2f10f78b8811e5be92a072596348b"},
{file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5abb8d1241f4ea97950b872fa97a422ba8413fe02358f64128ff0cf745017f07"},
{file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5abc686a9ef7eaf208f9ad1fb5fb949556ecb7cc1fee27290eb7f194e01d97bd"},
{file = "rjsmin-1.2.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:076adcf04c34f712c9427fd9ba6a75bbf7aab975650dfc78cbdd0fbdbe49ca63"},
{file = "rjsmin-1.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8cb8947ddd250fce58261b0357846cd5d55419419c0f7dfb131dc4b733579a26"},
{file = "rjsmin-1.2.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9069c48b6508b9c5b05435e2c6042c2a0e2f97b35d7b9c27ceaea5fd377ffdc5"},
{file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:02b61cf9b6bc518fdac667f3ca3dab051cb8bd1bf4cba28b6d29153ec27990ad"},
{file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:09eca8581797244587916e5e07e36c4c86d54a4b7e5c7697484a95b75803515d"},
{file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c52b9dd45c837f1c5c2e8d40776f9e63257f8dbd5f79b85f648cc70da6c1e4e9"},
{file = "rjsmin-1.2.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4fe4ce990412c053a6bcd47d55133927e22fd3d100233d73355f60f9053054c5"},
{file = "rjsmin-1.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:aa883b9363b5134239066060879d5eb422a0d4ccf24ccf871f65a5b34c64926f"},
{file = "rjsmin-1.2.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6f4e95c5ac95b4cbb519917b3aa1d3d92fc6939c371637674c4a42b67b2b3f44"},
{file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ae3cd64e18e62aa330b24dd6f7b9809ce0a694afd1f01fe99c21f9acd1cb0ea6"},
{file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7999d797fcf805844d2d91598651785497249f592f31674da0964e794b3be019"},
{file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e733fea039a7b5ad7c06cc8bf215ee7afac81d462e273b3ab55c1ccc906cf127"},
{file = "rjsmin-1.2.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ccca74461bd53a99ff3304fcf299ea861df89846be3207329cb82d717ce47ea6"},
{file = "rjsmin-1.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:88f59ad24f91bf9c25d5c2ca3c84a72eed0028f57a98e3b85a915ece5c25be1e"},
{file = "rjsmin-1.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7a8b56fbd64adcc4402637f0e07b90b441e9981d720a10eb6265118018b42682"},
{file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2c24686cfdf86e55692183f7867e72c9e982add479c244eda7b8390f96db2c6c"},
{file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6c0d9f9ea8d9cd48cbcdc74a1c2e85d4d588af12bb8f0b672070ae7c9b6e6306"},
{file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:27abd32c9f5b6e0c0a3bcad43e8e24108c6d6c13a4e6c50c97497ea2b4614bb4"},
{file = "rjsmin-1.2.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:e0e009f6f8460901f5144b34ac2948f94af2f9b8c9b5425da705dbc8152c36c2"},
{file = "rjsmin-1.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:41e6013cb37a5b3563c19aa35f8e659fa536aa4197a0e3b6a57a381638294a15"},
{file = "rjsmin-1.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:62cbd38c9f5090f0a6378a45c415b4f96ae871216cedab0dfa21965620c0be4c"},
{file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fd5254d36f10a17564b63e8bf9ac579c7b5f211364e11e9753ff5b562843c67"},
{file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6cf0309d001a0d45d731dbaab1afd0c23d135c9e029fe56c935c1798094686fc"},
{file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfbe333dab8d23f0a71da90e2d8e8b762a739cbd55a6f948b2dfda089b6d5853"},
{file = "rjsmin-1.2.2.tar.gz", hash = "sha256:8c1bcd821143fecf23242012b55e13610840a839cd467b358f16359010d62dae"},
{file = "rjsmin-1.2.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:86e4257875d89b0f7968af9e7c0292e72454f6c75031d1818997782b2e8425a8"},
{file = "rjsmin-1.2.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2ab72093591127e627b13c1243d4fef40c10593c733517999682f7f2ebf47ee"},
{file = "rjsmin-1.2.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:448b9eb9fd7b6a70beb5c728a41bc23561dd011f0b8fcf7ed9855b6be198c9a2"},
{file = "rjsmin-1.2.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ebd3948f9bc912525bab93f61c694b11410296b5fd0806e988d42378ef302b8e"},
{file = "rjsmin-1.2.3-cp310-cp310-manylinux1_i686.whl", hash = "sha256:823f856b40681328157e5dffc0a588dddefb4b6ce49f79de994dfca6084617be"},
{file = "rjsmin-1.2.3-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:15e3019f0823a003741ddb93e0c70c5d22567acd0757a7edacc40face1517029"},
{file = "rjsmin-1.2.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:dece04e309e29879c12dca8af166ea5d77c497ec932cf82e4a1eb24d1489c398"},
{file = "rjsmin-1.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dd4a1e527568c3a9711ff1d5251763645c14df02d52a45aec089836600b664ea"},
{file = "rjsmin-1.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:78aaa3b79a244a4e21164ce355ce22a5a0d7f2d7841a10343009406a3d34d9bb"},
{file = "rjsmin-1.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ea4617618cbf78d98756878a292309f6f54fb4ea1b1ea406f79e88eda4d5d50"},
{file = "rjsmin-1.2.3-cp311-cp311-manylinux1_i686.whl", hash = "sha256:85957171184ef2dee1957cef5e4adb93a7e2702c12c30bd74420ebace1756e89"},
{file = "rjsmin-1.2.3-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:b6485014e9cbec9a41fb4a7b96ce511ab45a5db8c54ca57ad610f53747e7bab1"},
{file = "rjsmin-1.2.3-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:64ac6ef8753c56179a53e237ea4d2b3ccdef88b8b51141618311d48e31013207"},
{file = "rjsmin-1.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dbd5f653b5ebcd4920793009ffa210ad5523c523e39e45ee1a0770e4323126dc"},
{file = "rjsmin-1.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b0174d7786dcebab808485d1c27f049c74b97590cddcd62f6ed54796a2c6503b"},
{file = "rjsmin-1.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6cf24720ea214cbffa0ed96ba0dc24a5cf3ff3cbf59d44a1018292424b48452a"},
{file = "rjsmin-1.2.3-cp312-cp312-manylinux1_i686.whl", hash = "sha256:ac911d1a12a6d7879ba52e08c56b0ad1a74377bae52610ea74f0f9d936d41785"},
{file = "rjsmin-1.2.3-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:57a0b2f13402623e4ec44eb7ad8846387b2d5605aa8732a05ebefb2289c24b96"},
{file = "rjsmin-1.2.3-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:e28610cca3ab03e43113eadad4f7dd9ea235ddc29a8dc5462bb161a80e5d251f"},
{file = "rjsmin-1.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d4afb4fc3624dc44a7fbae4e41c0b5dc5d861a7f5de865ad463041ec1b5d835c"},
{file = "rjsmin-1.2.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ca26b80c7e63cf0788b41571a4bd08d175df7719364e0dd9a3cf7b6cb1ab834c"},
{file = "rjsmin-1.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fcc22001588b84d34bbf2c77afa519894244150c4b0754a6e573298ffac24666"},
{file = "rjsmin-1.2.3-cp313-cp313-manylinux1_i686.whl", hash = "sha256:624d1a0a35122f3f8955d160a39305cf6f786a5b346ee34c516b391cb153a106"},
{file = "rjsmin-1.2.3-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:72bd04b7db6190339d8214a5fd289ca31fc1ed30a240f8b0ca13acb9ce3a88af"},
{file = "rjsmin-1.2.3-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:7559f59f4749519b92d72bb68e33b68463f479a82a2a739f1b28a853067aa0e7"},
{file = "rjsmin-1.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:aa8bdecf278f754d1a133ab51119a63a4d38500557912bb0930ae0fd61437ec6"},
{file = "rjsmin-1.2.3-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:2078acc2d02a005ef122eb330e941462c8c3102cf798ad49f1c5ec18ac714240"},
{file = "rjsmin-1.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fa40584fddb4f1d2236119505f6c2fe2b57a1ebaf6eaee2bb2eaac33d2a4ca73"},
{file = "rjsmin-1.2.3-cp313-cp313t-manylinux1_i686.whl", hash = "sha256:bbe5d8340878b38dd4f7b879ed7728f6fc3d7524ad81a5cfbe4eb8ae63951407"},
{file = "rjsmin-1.2.3-cp313-cp313t-manylinux1_x86_64.whl", hash = "sha256:c298c93f5633cf894325907cf49fc7fb010c0f75dc9cda90b0fc1684ad19e5a3"},
{file = "rjsmin-1.2.3-cp313-cp313t-manylinux2014_aarch64.whl", hash = "sha256:35f18cffe3f1bf6d96bcfd977199378ebfd641d823b08e235d1e0bb0fbaa5532"},
{file = "rjsmin-1.2.3-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9aeadf4dd5f941bebf110fe83960a4bafdac176647537819bb7662f5e9a37aaa"},
{file = "rjsmin-1.2.3-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c3219e6e22897b31c8598cb412ed56bc12a722c1d4f88a71710c16efe8c07d0c"},
{file = "rjsmin-1.2.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:bceccb06b118be890fe735fc09ee256851f4993708cb3647f6c71dd0151cce89"},
{file = "rjsmin-1.2.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f3620271f00b8ba3c7c5134ca1d99cde5fd1bf1e84aa96aa65c177ee634122f7"},
{file = "rjsmin-1.2.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f3d86f70fcca5f68b65eabbce365d07d80404ecd6aa9c55ba9e9f1042a3514c7"},
{file = "rjsmin-1.2.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1dae9230eae6d7eb2820a511cb640ca6f2e5b91ff78805d71332e8a65a898ea1"},
{file = "rjsmin-1.2.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b788c3ec9d68d8fda2240eb7831bdfb2cc0c88d5fb38c9ed6e0fd090eb5d1490"},
{file = "rjsmin-1.2.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:4763efbfad7fbf3240a33f08f64991bf0db07453caf283eea51ade84053e9bb7"},
{file = "rjsmin-1.2.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e1379e448da75e2426205c756e79d7b9ba1b7ed616fe97122d72c3fe054e8cac"},
{file = "rjsmin-1.2.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:27e134f4d91a5986cba6dced5cb539947a3ec61544ab5ef31b74b384ddc03931"},
{file = "rjsmin-1.2.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2674fcad70d0fab4c1c71e4ac1d4d67935f67e6ecc3924de0dd1264c80a9f9a2"},
{file = "rjsmin-1.2.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b1f2540bd0ce7eda326df7b3bfa360f6edd526bfcb959b5d136afdbccddf0765"},
{file = "rjsmin-1.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:23f3b4adde995a0d0b7835558840dd4673adf99d2473b6d40474d30801d6c57b"},
{file = "rjsmin-1.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c5fb0574eb541d374a2751e9c0ae019fdd86c9e3eb2e7cf893756886e7b3923f"},
{file = "rjsmin-1.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18d6a3229d1ed511a0b0a9a7271ef58ff3b02ba408b92b426857b33b137e7f15"},
{file = "rjsmin-1.2.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7fe1181452fca7713f377cb6c43cd139638a9edc8c8c29c67119626df164b317"},
{file = "rjsmin-1.2.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:57708a4e637aac571c578424a7092d3ec64afb1eabbb73e0c71659457eac9ee4"},
{file = "rjsmin-1.2.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:4c1b5a888d43063a22e2e2c2b4db4d6139dfa6e0d2903ae9bb050ed63a340f40"},
{file = "rjsmin-1.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4ac3f85df88d636a9680432fbbf5d3fe1f171821688106a6710738f06575fc2"},
{file = "rjsmin-1.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9dff6b14f92ca7a9f6fbf13548358715e47c5e69576aa5dd8b0ad5048fdc967f"},
{file = "rjsmin-1.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07c4f1efbbbcd16a645ada1f012595f3eb3e5d5933395effb6104d3731de2d96"},
{file = "rjsmin-1.2.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:37a73f6ff49dd8c662399575a249a2a028d098c1fa940c6e88aa9082beb44eca"},
{file = "rjsmin-1.2.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:73357ec91465cf69173d637ccde7b46ed3a8001161c9650325fa305a486e89a3"},
{file = "rjsmin-1.2.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:236c792fbe18c3b18d4e0ad5ff1b1145f1fbe02126aee9f21bca757b00b63b7e"},
{file = "rjsmin-1.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a630a3131a4e63e10665a0ea7cfe0784a3e1e1c854edf79a8ac0654e3756648"},
{file = "rjsmin-1.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a1c98f60ca57adbae023cf989eec91d052f0601df63ddc52a0a48303b21a7f9e"},
{file = "rjsmin-1.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:32a0174efac83ac72a681edcb9acf5e1c87c5b6aae65ed3424468b5945a90f9d"},
{file = "rjsmin-1.2.3.tar.gz", hash = "sha256:1388b52493a4c04fbc970a2d757c301fa05a3c37640314c2ce9dfc8d8a730cc6"},
]
[[package]]
@ -2679,4 +2691,4 @@ filelock = ">=3.4"
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "730b020b335ea67342069c40591f6959f1e5f01eef40806c95777def2f39eb37"
content-hash = "cb47f6409e629d8369a19d82f44a57dbe9414c79e6e72bd88a6bcb34d78f0bc0"

View File

@ -65,7 +65,7 @@ optional = true
# deps used for development purposes, but unneeded in prod
django-debug-toolbar = "^4.4.6"
ipython = "^8.26.0"
pre-commit = "^3.8.0"
pre-commit = "^4.0.1"
ruff = "^0.6.9" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml
djhtml = "^3.0.6"
faker = "^30.3.0"
@ -77,14 +77,14 @@ freezegun = "^1.5.1" # used to test time-dependent code
pytest = "^8.3.2"
pytest-cov = "^5.0.0"
pytest-django = "^4.9.0"
model-bakery = "^1.19.5"
model-bakery = "^1.20.0"
[tool.poetry.group.docs.dependencies]
# deps used to work on the documentation
mkdocs = "^1.6.1"
mkdocs-material = "^9.5.39"
mkdocstrings = "^0.26.1"
mkdocstrings-python = "^1.11.1"
mkdocs-material = "^9.5.40"
mkdocstrings = "^0.26.2"
mkdocstrings-python = "^1.12.0"
mkdocs-include-markdown-plugin = "^6.2.2"
[tool.poetry.group.docs]
@ -101,17 +101,30 @@ select = [
"A", # shadowing of Python builtins
"B",
"C4", # use comprehensions when possible
"I", # isort
"DJ", # django-specific rules,
"F401", # unused import
"E", # pycodestyle (https://docs.astral.sh/ruff/rules/#pycodestyle-e-w)
"ERA", # commented code
"F", # pyflakes (https://docs.astral.sh/ruff/rules/#pyflakes-f)
"FBT", # boolean trap
"FLY", # f-string instead of str.join
"FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb
"I", # isort
"INT", # gettext
"PERF", # performance
"PLW", # pylint warnings (https://docs.astral.sh/ruff/rules/#pylint-pl)
"RUF", # Ruff specific rules
"SIM", # simplify (https://docs.astral.sh/ruff/rules/#flake8-simplify-sim)
"T100", # breakpoint()
"T2", # print statements
"TCH", # type-checking block
"UP008", # Use super() instead of super(__class__, self)
"UP009", # utf-8 encoding declaration is unnecessary
"T2", # print statements
]
ignore = [
"DJ001", # null=True in CharField/TextField. this one would require a migration
"E501", # line too long. The rule is too harsh, and the formatter deals with it in most cases
"RUF012" # mutable class attributes. This rule doesn't integrate well with django
]
[tool.ruff.lint.pydocstyle]

View File

@ -44,8 +44,8 @@ class Command(BaseCommand):
exit(1)
confirm = input(
"User selected: %s\nDo you really want to delete all message from this user ? [y/N] "
% (user,)
"User selected: %s\nDo you really want "
"to delete all message from this user ? [y/N] " % (user,)
)
if not confirm.lower().startswith("y"):

View File

@ -66,11 +66,11 @@ class TestMergeUser(TestCase):
self.to_keep = User.objects.get(pk=self.to_keep.pk)
# fields of to_delete should be assigned to to_keep
# if they were not set beforehand
assert "Biggus" == self.to_keep.first_name
assert "Dickus" == self.to_keep.last_name
assert "B'ian" == self.to_keep.nick_name
assert "Jerusalem" == self.to_keep.address
assert "Rome" == self.to_keep.parent_address
assert self.to_keep.first_name == "Biggus"
assert self.to_keep.last_name == "Dickus"
assert self.to_keep.nick_name == "B'ian"
assert self.to_keep.address == "Jerusalem"
assert self.to_keep.parent_address == "Rome"
assert self.to_keep.groups.count() == 3
groups = sorted(self.to_keep.groups.all(), key=lambda i: i.id)
expected = sorted([subscribers, mde_admin, sas_admin], key=lambda i: i.id)

View File

@ -24,7 +24,11 @@
from django.urls import path
from rootplace.views import *
from rootplace.views import (
DeleteAllForumUserMessagesView,
MergeUsersView,
OperationLogListView,
)
urlpatterns = [
path("merge/", MergeUsersView.as_view(), name="merge"),

View File

@ -48,7 +48,8 @@ def __merge_subscriptions(u1: User, u2: User):
Some examples :
- if u1 is not subscribed, his subscription end date become the one of u2
- if u1 is subscribed but not u2, nothing happen
- if u1 is subscribed for, let's say, 2 remaining months and u2 is subscribed for 3 remaining months,
- if u1 is subscribed for, let's say,
2 remaining months and u2 is subscribed for 3 remaining months,
he shall then be subscribed for 5 months
"""
last_subscription = (

View File

@ -15,7 +15,7 @@
from django.contrib import admin
from sas.models import Album, PeoplePictureRelation, Picture
from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest
@admin.register(Picture)
@ -31,4 +31,15 @@ class PeoplePictureRelationAdmin(admin.ModelAdmin):
autocomplete_fields = ("picture", "user")
admin.site.register(Album)
@admin.register(Album)
class AlbumAdmin(admin.ModelAdmin):
list_display = ("name", "parent", "date", "owner", "is_moderated")
search_fields = ("name",)
autocomplete_fields = ("owner", "parent", "edit_groups", "view_groups")
@admin.register(PictureModerationRequest)
class PictureModerationRequestAdmin(admin.ModelAdmin):
list_display = ("author", "picture", "created_at")
search_fields = ("author", "picture")
autocomplete_fields = ("author", "picture")

Some files were not shown because too many files have changed in this diff Show More