35 Commits

Author SHA1 Message Date
thomas girod 99d85e0361 Merge pull request #1385 from ae-utbm/localstorage
Automatic localstorage cleaning
2026-05-22 08:21:36 +02:00
thomas girod cff0dd2cf6 Merge pull request #1405 from ae-utbm/fixes
Fixes
2026-05-21 22:54:00 +02:00
imperosol 5238e2e2d6 doc-comment explaining base-bundle-index.ts 2026-05-21 22:52:11 +02:00
imperosol 4375fdf150 fix: birthdays font-size too big 2026-05-21 22:51:33 +02:00
imperosol a10773bb37 fix: ClubRole migration 2026-05-21 22:50:56 +02:00
imperosol ce0ddcd184 replace exportToHtml by Object.assign(window, { obj }) 2026-05-21 22:41:51 +02:00
imperosol 6cec2e74d0 feat: versionedLocalStorage 2026-05-21 22:28:31 +02:00
imperosol 2228a3f961 use sessionStorage to cache user pictures
Le sessionStorage est automatiquement vidé à la fermeture de la page, ce qui, dans le cas des photos, est un peu plus fiable et correspond un peu mieux à nos besoins.
2026-05-21 22:28:23 +02:00
imperosol 4b369b73a7 feat: automatic localstorage cleaning 2026-05-21 22:28:20 +02:00
imperosol 73f422db23 refactor: move nested-key.d.ts 2026-05-21 22:24:16 +02:00
imperosol 0aed36c8d9 refactor: assemble main js files into a single bundle 2026-05-21 22:24:16 +02:00
imperosol e2c17175f5 feat: use a queue in user pictures localstorage 2026-05-21 18:45:24 +02:00
klmp200 59b4c4e73c Merge pull request #1390 from ae-utbm/dependencies
Upgrade python dependencies
2026-05-20 23:03:49 +02:00
thomas girod b8623eed11 Merge pull request #1391 from ae-utbm/notifications-magic
improve `$notifications`
2026-05-20 23:03:30 +02:00
thomas girod ed791dc544 Merge pull request #1388 from ae-utbm/club-link
Club links
2026-05-20 23:03:08 +02:00
klmp200 18e8088cf3 Upgrade python dependencies 2026-05-20 21:48:51 +02:00
thomas girod 5ef7fd0294 Merge pull request #1403 from ae-utbm/update-deps
Update deps
2026-05-20 19:33:37 +02:00
imperosol afa3ea4f2c update xapian 2026-05-20 14:32:14 +02:00
imperosol c38fe7f9ae update JS deps 2026-05-20 14:21:34 +02:00
imperosol c6c8781511 update python deps 2026-05-20 14:15:37 +02:00
thomas girod 30bd4fd3ea Merge pull request #1402 from ae-utbm/fix-trans
fix: translation of UT network subscription
2026-05-20 12:41:55 +02:00
imperosol 019ab95773 fix: translation of UT network subscription 2026-05-20 12:35:32 +02:00
imperosol c83ab7c2c1 apply review comments 2026-05-20 12:26:15 +02:00
imperosol 23dc7702c4 add club links to club list page 2026-05-20 12:26:15 +02:00
imperosol def4f80ec0 add translations 2026-05-20 12:26:15 +02:00
imperosol a5a31b6a4d display club links on club main page 2026-05-20 12:25:26 +02:00
imperosol eaf90efd5c feat: club link management in club edit view 2026-05-20 12:25:26 +02:00
imperosol 4d1019c955 generate club links in populate 2026-05-20 12:25:26 +02:00
imperosol 52f112a041 feat: add links to response of GET /api/club/{club_id} 2026-05-20 12:25:26 +02:00
imperosol e89be22c34 feat: ClubLink model 2026-05-20 12:25:26 +02:00
imperosol fc37cbb454 improve main page style 2026-05-20 11:01:32 +02:00
imperosol 204e4e97bd apply review comments 2026-05-20 10:39:52 +02:00
imperosol 57a422de8c add doc comments for notifications 2026-05-20 10:25:14 +02:00
imperosol 75f3094dd5 fix: wrong notification update with fragments 2026-05-19 14:43:43 +02:00
imperosol 68aa4515f9 improve $notifications 2026-05-16 20:04:54 +02:00
53 changed files with 1669 additions and 848 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.5
rev: v0.15.13
hooks:
- id: ruff-check # just check the code, and print the errors
- id: ruff-check # actually fix the fixable errors, but print nothing
@@ -14,7 +14,7 @@ repos:
- id: biome-check
additional_dependencies: ["@biomejs/biome@2.4.6"]
- repo: https://github.com/rtts/djhtml
rev: 3.0.10
rev: 3.0.11
hooks:
- id: djhtml
name: format templates
+1 -1
View File
@@ -38,6 +38,6 @@
}
},
"javascript": {
"globals": ["Alpine", "$", "jQuery", "gettext", "interpolate"]
"globals": ["Alpine", "gettext", "interpolate"]
}
}
+16 -1
View File
@@ -16,7 +16,7 @@ from django.contrib import admin
from django.forms.models import ModelForm
from django.http import HttpRequest
from club.models import Club, ClubRole, Membership
from club.models import Club, ClubLink, ClubRole, LinkType, Membership
@admin.register(Club)
@@ -67,3 +67,18 @@ class MembershipAdmin(admin.ModelAdmin):
"club__name",
)
autocomplete_fields = ("user",)
@admin.register(LinkType)
class LinkTypeAdmin(admin.ModelAdmin):
list_display = ("name", "url_base", "icon")
search_fields = ("name",)
@admin.register(ClubLink)
class ClubLinkAdmin(admin.ModelAdmin):
list_display = ("link_type", "club", "url")
list_select_related = ("link_type", "club")
autocomplete_fields = ("link_type", "club")
search_fields = ("link_type__name", "url")
list_filter = ("link_type", ("club", admin.RelatedOnlyFieldListFilter))
+1 -1
View File
@@ -41,7 +41,7 @@ class ClubController(ControllerBase):
queryset=Membership.objects.ongoing().select_related("user", "role"),
)
return self.get_object_or_exception(
Club.objects.prefetch_related(prefetch), id=club_id
Club.objects.prefetch_related(prefetch, "links"), id=club_id
)
+42 -1
View File
@@ -28,7 +28,14 @@ from django.db.models.functions import Lower
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from club.models import Club, ClubRole, Mailing, MailingSubscription, Membership
from club.models import (
Club,
ClubLink,
ClubRole,
Mailing,
MailingSubscription,
Membership,
)
from core.models import User
from core.views.forms import SelectDateTime
from core.views.widgets.ajax_select import (
@@ -39,6 +46,26 @@ from counter.models import Counter, Selling
from counter.schemas import SaleFilterSchema
class ClubLinkForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"
class Meta:
model = ClubLink
fields = ["url", "name", "link_type"]
widgets = {
"url": forms.URLInput(
{"pattern": "https://.*", "placeholder": "https://monlien.com"}
),
"link_type": forms.HiddenInput(),
}
ClubLinkFormSet = forms.inlineformset_factory(
Club, ClubLink, ClubLinkForm, extra=0, can_delete_extra=False
)
class ClubEditForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"
@@ -48,6 +75,20 @@ class ClubEditForm(forms.ModelForm):
fields = ["address", "logo", "short_description"]
widgets = {"short_description": forms.Textarea()}
def __init__(self, *args, prefix: str | None = None, instance=None, **kwargs):
super().__init__(*args, prefix=prefix, instance=instance, **kwargs)
self.link_formset = ClubLinkFormSet(
*args, instance=self.instance, prefix="link", **kwargs
)
def is_valid(self):
return super().is_valid() and self.link_formset.is_valid()
def save(self, commit=True): # noqa: FBT002
res = super().save(commit=commit)
self.link_formset.save(commit=commit)
return res
class ClubAdminEditForm(ClubEditForm):
admin_fields = ["name", "parent", "is_active"]
@@ -33,7 +33,7 @@ def migrate_roles(apps: StateApps, schema_editor):
club_id=club_id,
order=max(SITH_CLUB_ROLES) - role,
)
updates.append(When(role=role, then=new_role.id))
updates.append(When(club_id=club_id, role=role, then=new_role.id))
# all updates must happen at the same time
# otherwise, the 10 first created ClubRole would be
# re-modified after their initial creation, and it would
+105
View File
@@ -0,0 +1,105 @@
# Generated by Django 5.2.12 on 2026-04-27 07:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("club", "0016_clubrole_alter_membership_role")]
operations = [
migrations.CreateModel(
name="LinkType",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=40, verbose_name="name")),
(
"url_base",
models.URLField(
help_text=(
"The base url that links with this type "
"must respect (e.g. `https://www.instagram.com`)"
),
unique=True,
verbose_name="url base",
),
),
(
"icon",
models.CharField(
help_text=(
"The fontawesome class to use "
"(e.g. `fa-brands fa-instagram`)"
),
max_length=40,
verbose_name="icon",
),
),
],
options={"verbose_name": "link type", "verbose_name_plural": "link types"},
),
migrations.CreateModel(
name="ClubLink",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(blank=True, max_length=40, verbose_name="name"),
),
("url", models.URLField(verbose_name="link url")),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="created at"),
),
(
"updated_at",
models.DateTimeField(auto_now=True, verbose_name="updated at"),
),
(
"club",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="links",
to="club.club",
verbose_name="club",
),
),
(
"link_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="links",
to="club.linktype",
verbose_name="link type",
),
),
],
options={
"verbose_name": "club link",
"verbose_name_plural": "club links",
"constraints": [
models.UniqueConstraint(
fields=["club", "url"],
name="club_clublink_unique_club_url",
violation_error_message="Duplicated url",
)
],
},
),
]
+78
View File
@@ -773,3 +773,81 @@ class MailingSubscription(models.Model):
def fetch_format(self):
return self.get_email + " "
class LinkType(models.Model):
"""A link type, in order to group links and give them icons.
Notes:
Among all club links, there is a special one, with an empty base url
and a default link icon.
It is use as a fallback item when no actual link type can be found.
Danger:
LinkType.icon is content that will be raw-rendered in the template.
It is NOT safe to allow users to give it.
The edition of this field must be reserved to trusted admins.
"""
name = models.CharField(_("name"), max_length=40)
url_base = models.URLField(
"url base",
unique=True,
help_text=_(
"The base url that links with this type must respect (e.g. `%(url)s`)"
)
% {"url": "https://www.instagram.com"},
)
icon = models.CharField(
_("icon"),
max_length=40,
help_text=_("The fontawesome class to use (e.g. `fa-brands fa-instagram`)"),
)
class Meta:
verbose_name = _("link type")
verbose_name_plural = _("link types")
def __str__(self):
return self.name
class ClubLink(models.Model):
link_type = models.ForeignKey(
LinkType,
verbose_name=_("link type"),
on_delete=models.CASCADE,
related_name="links",
)
name = models.CharField(_("name"), max_length=40, blank=True)
url = models.URLField(_("link url"))
club = models.ForeignKey(
Club, verbose_name=_("club"), on_delete=models.CASCADE, related_name="links"
)
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
updated_at = models.DateTimeField(_("updated at"), auto_now=True)
class Meta:
verbose_name = _("club link")
verbose_name_plural = _("club links")
constraints = [
models.UniqueConstraint(
fields=["club", "url"],
name="club_clublink_unique_club_url",
violation_error_message=_("Duplicated url"),
)
]
def __str__(self):
return self.url
def save(self, **kwargs):
if not self.name:
self.name = self.link_type.name
return super().save(**kwargs)
def clean(self):
if not self.url.startswith(self.link_type.url_base):
raise ValidationError(
_("This link doesn't match with the url base of its type.")
)
+6
View File
@@ -2,6 +2,7 @@ from typing import Annotated
from django.db.models import Q
from ninja import FilterLookup, FilterSchema, ModelSchema
from pydantic import HttpUrl
from club.models import Club, ClubRole, Membership
from core.schemas import NonEmptyStr, SimpleUserSchema
@@ -62,6 +63,11 @@ class ClubSchema(ModelSchema):
fields = ["id", "name", "logo", "is_active", "short_description", "address"]
members: list[ClubMemberSchema]
links: list[HttpUrl]
@staticmethod
def resolve_links(obj: Club):
return [link.url for link in obj.links.all()]
class UserMembershipSchema(ModelSchema):
+66
View File
@@ -0,0 +1,66 @@
#club-detail {
img.club-logo {
display: block;
max-height: 200px;
max-width: 200px;
}
#club-attributes {
ul {
list-style: none;
margin-left: 0;
display: flex;
flex-direction: column;
gap: .75rem;
li i {
margin-right: .5rem;
}
}
}
&:not(.has-links) {
#club-attributes {
float: right;
margin: 1em 0 1em 2em;
@media screen and (max-width: 650px) {
margin-left: 1em;
}
@media screen and (max-width: 400px) {
float: unset;
img.club-logo {
margin: auto;
}
}
}
}
&.has-links {
display: flex;
flex-direction: row-reverse;
gap: 2em;
@media screen and (max-width: 650px) {
flex-direction: column;
gap: 1em;
}
#club-attributes {
display: flex;
flex-direction: column;
gap: 1em;
min-width: 200px;
@media screen and (max-width: 650px) {
margin-top: 1em;
flex-direction: row-reverse;
justify-content: flex-end;
h4 {
margin: 0;
}
img.club-logo {
margin-left: auto;
}
}
}
}
}
+32 -4
View File
@@ -21,16 +21,44 @@
{% endif %}
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("club/detail.scss") }}">
{% endblock %}
{% block content %}
<div id="club_detail">
{% if club.logo %}
<div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.name }}"></div>
{% endif %}
<h3>{{ club.name }}</h3>
<div id="club-detail" {% if links %}class="has-links"{% endif %}>
<div id="club-attributes">
{% if club.logo %}
<img
class="club-logo"
src="{{ club.logo.url }}"
alt="{{ club.name }}"
width="{{ club.logo.width }}"
height="{{ club.logo.height }}"
>
{% endif %}
{% if links %}
<div id="club-links">
<h4>{% trans %}Links{% endtrans %}</h4>
<ul>
{% for link in links %}
<li>
<a href="{{ link.url }}" rel="noopener external" target="_blank">
<i class="{{ link.link_type.icon }} fa-xl"></i>{{ link.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<div id="club-page">
{% if page_revision %}
{{ page_revision|markdown }}
{% endif %}
</div>
</div>
{% endblock %}
+12
View File
@@ -69,6 +69,18 @@
{{ club.name }} {% if not club.is_active %}({% trans %}inactive{% endtrans %}){% endif %}
</h4>
</a>
{% set links = club.links.all() %}
{% if links %}
<br>
<div class="row gap-2x">
{% for link in club.links.all() %}
<a href="{{ link.url }}" rel="noopener external" target="_blank">
<i class="{{ link.link_type.icon }} fa-xl"></i>
<strong>{{ link.name }}</strong>
</a>
{% endfor %}
</div>
{% endif %}
{{ club.short_description|markdown }}
</div>
</div>
+98 -4
View File
@@ -1,9 +1,63 @@
{% extends "core/base.jinja" %}
{% block additional_js %}
<script type="module" src="{{ static("bundled/core/dynamic-formset-index.ts") }}"></script>
{% endblock %}
{% block title %}
{% trans name=object %}Edit {{ name }}{% endtrans %}
{% endblock %}
{% macro link_form(form) %}
<fieldset
{# set url in x-init rather than in x-data,
in order to trigger the $watch on initial load #}
x-data="{ url: '', linkType: { icon: '', id: 0 } }"
x-init="() => {
$watch('url', (u) => linkType = linkTypes.find((t) => u.startsWith(t.url)));
url = '{{ form.url.value() or "" }}';
}"
>
{{ form.non_field_errors() }}
<div class="form-group row gap-2x">
<div>
{{ form.url.label_tag() }}
{{ form.url.errors }}
<span>
{# we change the icon when the user change it and leave the input,
or when it is pasted from the clipboard #}
{{ form.url|add_attr("x-model.change=url,@paste.prevent=url = $event.clipboardData.getData('text')") }}
<i
:class="linkType.icon || 'fa fa-link'"
tooltip="{% trans %}This icon will change according to the given url.{% endtrans %}"
></i>
</span>
</div>
<div>{{ form.name.as_field_group() }}</div>
</div>
{%- if form.DELETE -%}
<div class="form-group row gap">
{{ form.DELETE.as_field_group() }}
</div>
{%- else -%}
<br>
<button
class="btn btn-grey"
@click.prevent="removeForm($event.target.closest('fieldset'))"
>
<i class="fa fa-minus"></i> {% trans %}Remove link{% endtrans %}
</button>
{%- endif -%}
{{ form.link_type|add_attr(":value=linkType.id") }}
{%- for field in form.hidden_fields() -%}
{%- if field != form.link_type -%}
{{ field }}
{%- endif -%}
{%- endfor -%}
</fieldset>
{% endmacro %}
{% block content %}
<h2>{% trans name=object %}Edit {{ name }}{% endtrans %}</h2>
@@ -17,7 +71,7 @@
and explicitly separate them from the non-admin ones,
with some help text.
Non-admin users will only see the regular form fields,
so they don't need thoses explanations #}
so they don't need those explanations #}
<h3>{% trans %}Club properties{% endtrans %}</h3>
<p class="helptext">
{% trans trimmed %}
@@ -25,7 +79,7 @@
Only admin users can see and edit them.
{% endtrans %}
</p>
<fieldset class="required margin-bottom">
<fieldset class="margin-bottom">
{% for field_name in form.admin_fields %}
{% set field = form[field_name] %}
<div class="form-group">
@@ -36,11 +90,13 @@
{# Remove the the admin fields from the form.
The remaining non-admin fields will be rendered
at once with a simple {{ form.as_p() }} #}
{% set _ = form.fields.pop(field_name) %}
{% do form.fields.pop(field_name) %}
{% endfor %}
</fieldset>
{% endif %}
<h3>{% trans %}Club informations{% endtrans %}</h3>
{% if form.admin_fields %}
<p class="helptext">
{% trans trimmed %}
The following form fields are linked to the basic description of a club.
@@ -48,7 +104,45 @@
{% endtrans %}
</p>
{% endif %}
<fieldset class="margin-bottom">
{{ form.as_p() }}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</fieldset>
<h3>{% trans %}Club links{% endtrans %}</h3>
<div x-data="dynamicFormSet({ prefix: '{{ form.link_formset.prefix }}' })" class="margin-bottom">
{{ form.link_formset.management_form }}
<div x-ref="formContainer">
{%- for f in form.link_formset.forms -%}
{{ link_form(f) }}
{%- endfor -%}
</div>
<template x-ref="formTemplate">
{{ link_form(form.link_formset.empty_form) }}
</template>
<p>
<i>{% trans trimmed %}
Note: if the icon of one of your links doesn't exist yet,
you can ask the info team to add it.
{% endtrans %}</i>
</p>
<br>
<button @click.prevent="addForm()" class="btn btn-grey">
<i class="fa fa-plus"></i>{% trans %}Add link{% endtrans %}
</button>
</div>
<hr>
<button type="submit" class="btn btn-blue">
<i class="fa fa-check"></i>{% trans %}Save{% endtrans %}
</button>
</form>
{% endblock content %}
{% block script %}
<script>
const linkTypes = [
{%- for t in link_types -%}
{ id: {{ t.id }}, url: '{{ t.url_base }}', icon: '{{ t.icon }}' },
{%- endfor -%}
];
</script>
{% endblock %}
+2 -2
View File
@@ -88,8 +88,8 @@ class TestFetchClub:
def test_fetch_club_nb_queries(self, client: Client, club: Club):
user = subscriber_user.make()
client.force_login(user)
with assertNumQueries(6):
with assertNumQueries(7):
# - 4 queries for authentication
# - 2 queries for the actual data
# - 3 queries for the actual data
res = client.get(reverse("api:fetch_club", kwargs={"club_id": club.id}))
assert res.status_code == 200
+7 -1
View File
@@ -21,7 +21,13 @@ def test_club_board_member_cannot_edit_club_properties(client: Client):
client.force_login(user)
res = client.post(
reverse("club:club_edit", kwargs={"club_id": club.id}),
{"name": "new name", "is_active": False, "address": "new address"},
{
"name": "new name",
"is_active": False,
"address": "new address",
"link-TOTAL_FORMS": 0,
"link-INITIAL_FORMS": 0,
},
)
# The request should success,
# but admin-only fields shouldn't be taken into account
+2 -2
View File
@@ -21,7 +21,7 @@ def test_page_display_on_club_main_page(client: Client):
assert res.status_code == 200
soup = BeautifulSoup(res.text, "lxml")
detail_html = soup.find(id="club_detail").find(class_="markdown")
detail_html = soup.find(id="club-page").find(class_="markdown")
assertHTMLEqual(detail_html.decode_contents(), markdown(content))
@@ -34,7 +34,7 @@ def test_club_main_page_without_content(client: Client):
assert res.status_code == 200
soup = BeautifulSoup(res.text, "lxml")
detail_html = soup.find(id="club_detail")
detail_html = soup.find(id="club-page")
assert detail_html.find_all("markdown") == []
+21 -9
View File
@@ -36,7 +36,8 @@ from django.contrib.auth.mixins import (
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
from django.core.paginator import InvalidPage, Paginator
from django.db.models import F, Q, Sum
from django.db.models import F, Prefetch, Q, Sum
from django.db.models.functions import Length
from django.http import Http404, StreamingHttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
@@ -47,12 +48,7 @@ from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, View
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import (
CreateView,
DeleteView,
FormMixin,
UpdateView,
)
from django.views.generic.edit import CreateView, DeleteView, FormMixin, UpdateView
from club.forms import (
ClubAddMemberForm,
@@ -66,7 +62,15 @@ from club.forms import (
MailingForm,
SellingsForm,
)
from club.models import Club, ClubRole, Mailing, MailingSubscription, Membership
from club.models import (
Club,
ClubLink,
ClubRole,
LinkType,
Mailing,
MailingSubscription,
Membership,
)
from com.models import Poster
from com.views import (
PosterCreateBaseView,
@@ -210,7 +214,9 @@ class ClubListView(AllowFragment, FormMixin, ListView):
template_name = "club/club_list.jinja"
form_class = ClubSearchForm
queryset = Club.objects.order_by("name")
queryset = Club.objects.prefetch_related(
Prefetch("links", queryset=ClubLink.objects.select_related("link_type"))
).order_by("name")
paginate_by = 20
def get_form_kwargs(self):
@@ -249,6 +255,7 @@ class ClubView(ClubTabsMixin, DetailView):
.values_list("content", flat=True)
.first()
)
kwargs["links"] = list(self.object.links.select_related("link_type").all())
return kwargs
@@ -689,6 +696,11 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
return ClubAdminEditForm
return ClubEditForm
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"link_types": list(LinkType.objects.order_by(Length("url_base").desc()))
}
class ClubCreateView(PermissionRequiredMixin, CreateView):
"""Create a club (for the Sith admin)."""
@@ -1,4 +1,3 @@
import { exportToHtml } from "#core:utils/globals.ts";
import { newsDeleteNews, newsFetchNewsDates, newsPublishNews } from "#openapi";
// This will be used in jinja templates,
@@ -13,7 +12,8 @@ const AlertState = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
DISPLAYED: 4, // When published at page generation
};
exportToHtml("AlertState", AlertState);
// biome-ignore lint/style/useNamingConvention: it's an enum, PascalCase is better
Object.assign(window, { AlertState });
document.addEventListener("alpine:init", () => {
Alpine.data("moderationAlert", (newsId: number) => ({
+9 -17
View File
@@ -16,9 +16,13 @@
#right_column {
flex: 20%;
margin: 3.2px;
display: inline-block;
vertical-align: top;
@media screen and (min-width: 800px) {
max-width: 20%;
min-width: 200px;
}
}
#left_column {
@@ -46,7 +50,7 @@
}
}
@media screen and (max-width: $small-devices) {
@media screen and (max-width: 800px) {
#left_column,
#right_column {
flex: 100%;
@@ -76,8 +80,8 @@
display: block;
width: 100%;
background: white;
font-size: 70%;
margin-bottom: 1em;
font-size: 85%;
#links_content {
overflow: auto;
@@ -96,23 +100,10 @@
li {
margin: 10px;
.fa-facebook {
color: $faceblue;
}
.fa-discord {
color: $discordblurple;
}
.fa-square-instagram::before {
background: $instagradient;
background-clip: text;
-webkit-text-fill-color: transparent;
}
i {
width: 25px;
text-align: center;
margin-right: .5rem;
}
}
}
@@ -122,6 +113,7 @@
#birthdays_content {
box-shadow: $shadow-color 1px 1px 1px;
padding: 1em;
ul.birthdays_year {
margin: 0;
list-style-type: none;
+1 -1
View File
@@ -4,7 +4,7 @@
<title>{% trans %}Slideshow{% endtrans %}</title>
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
<script type="module" src="{{ static('bundled/base-bundle-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script>
</head>
<body x-data="slideshow([
+49 -1
View File
@@ -36,7 +36,7 @@ from django.utils import timezone
from django.utils.timezone import localdate
from PIL import Image
from club.models import Club, ClubRole, Membership
from club.models import Club, ClubLink, ClubRole, LinkType, Membership
from com.ics_calendar import IcsCalendar
from com.models import News, NewsDate, Sith, Weekmail
from core.models import BanGroup, Group, Page, PageRev, SithFile, User
@@ -830,6 +830,54 @@ class Command(BaseCommand):
):
roles.append(ClubRole(club=club, order=i, name=role))
ClubRole.objects.bulk_create(roles)
insta, fb, discord, _ = LinkType.objects.bulk_create(
[
LinkType(
name="instagram",
icon="fa-brands fa-square-instagram",
url_base="https://www.instagram.com",
),
LinkType(
name="facebook",
icon="fa-brands fa-facebook",
url_base="https://www.facebook.com",
),
LinkType(
name="discord",
icon="fa-brands fa-discord",
url_base="https://discord.gg",
),
LinkType(name="generic", icon="fa fa-link", url_base=""),
]
)
ClubLink.objects.bulk_create(
[
ClubLink(
name="insta AE",
url="https://www.instagram.com/ae_utbm/",
club=ae,
link_type=insta,
),
ClubLink(
name="insta activités AE",
url="https://www.instagram.com/activites_ae/",
club=ae,
link_type=insta,
),
ClubLink(
name="facebook AE",
url="https://www.facebook.com/ae_utbm",
club=ae,
link_type=fb,
),
ClubLink(
name="Discord",
url="https://discord.gg/QvTm3XJrHR",
club=ae,
link_type=discord,
),
]
)
return PopulatedClubs(ae=ae, troll=troll, pdf=pdf, refound=refound)
def _create_groups(self) -> PopulatedGroups:
-12
View File
@@ -1,12 +0,0 @@
import sort from "@alpinejs/sort";
import Alpine from "alpinejs";
import { limitedChoices } from "#core:alpine/limited-choices.ts";
import { alpinePlugin as notificationPlugin } from "#core:utils/notifications.ts";
Alpine.plugin([sort, limitedChoices]);
Alpine.magic("notifications", notificationPlugin);
window.Alpine = Alpine;
window.addEventListener("DOMContentLoaded", () => {
Alpine.start();
});
+64
View File
@@ -0,0 +1,64 @@
/**
* File containing main functions and library re-exports
* that should be accessible throughout the whole website.
*
* The idea is to group all that shared code into a single bundle,
* for more efficient tree-shaking and gzip compression.
*/
import sort from "@alpinejs/sort";
import Alpine from "alpinejs";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import htmx from "htmx.org";
import { limitedChoices } from "#core:alpine/limited-choices";
import { expireOldStorage } from "#core:core/localstorage";
import { default as navbar } from "#core:core/navbar";
import {
type NotificationPlugin,
notificationsPlugin as notifications,
} from "#core:utils/notifications";
/**
* Alpine
*/
declare module "alpinejs" {
interface Magics<T> {
$notifications: NotificationPlugin;
}
}
Alpine.plugin([sort, limitedChoices, notifications]);
// biome-ignore lint/style/useNamingConvention: it's how it's named
Object.assign(window, { Alpine });
window.addEventListener("DOMContentLoaded", () => {
Alpine.start();
});
/**
* Polyfill for country flags (used for language choice)
*/
polyfillCountryFlagEmojis();
/**
* HTMX
*/
document.body.addEventListener("htmx:beforeRequest", (event: CustomEvent) => {
event.detail.target.ariaBusy = true;
});
document.body.addEventListener("htmx:beforeSwap", (event: CustomEvent) => {
event.detail.target.ariaBusy = null;
});
Object.assign(window, { htmx });
/**
* navbar
*/
navbar();
/**
* Script that clears the cache when the cache version changes
*/
expireOldStorage();
+72
View File
@@ -0,0 +1,72 @@
/**
* For more detailed infos on how to use this file,
* check /docs/tutorial/front/localstorage.md,
* or https://ae-utbm.github.io/sith/tutorial/front/localstorage/
*/
// increment this number when a breaking change is made with localStorage
const CURRENT_LOCALSTORAGE_VERSION = 1;
/**
* Remove keys that are no longer used from localStorage
*/
export function expireOldStorage() {
const version = Number.parseInt(localStorage.getItem("version") ?? "0", 10);
if (version === CURRENT_LOCALSTORAGE_VERSION) {
// The cache schema is up-to-date. Nothing to do.
return;
}
localStorage.removeItem("basket1");
// remove all storage items which key is in the form
// `userXXXPictures` or `userXXXPicturesNumber`
Object.keys(localStorage)
.filter(
(key) =>
key.startsWith("user") &&
(key.endsWith("Pictures") || key.endsWith("PicturesNumber")),
)
.forEach((key) => {
localStorage.removeItem(key);
});
localStorage.setItem("version", CURRENT_LOCALSTORAGE_VERSION.toString());
}
interface VersionedStorageItem<T> {
version?: number;
val: T | undefined;
}
export const versionedLocalStorage = {
...localStorage,
/**
* set this item in localStorage, alongside its version.
*
* Note: this expects an object, not a JSON string, because the parsing
* into JSON needs to be done inside the function.
*/
setItem<T>(key: string, value: T, { version }: { version: number }) {
localStorage.setItem(key, JSON.stringify({ version: version, val: value }));
},
/**
* Get the item linked with the given key and version from localStorage.
*
* Note: if the given key exists in localStorage but doesn't satisfy
* the given version, it will be cleared from cache.
*
* @return the object if found and with the good version, else null;
*/
getItem<T>(key: string, { version }: { version: number }): T | null {
const stored = localStorage.getItem(key);
if (!stored) {
// this key doesn't exist, return null;
return null;
}
const obj: VersionedStorageItem<T> = JSON.parse(stored);
if (obj.version !== version || obj.val === undefined) {
// The version is wrong, return null and remove this item from cache
localStorage.removeItem(key);
return null;
}
return obj.val;
},
};
@@ -1,12 +1,10 @@
import { exportToHtml } from "#core:utils/globals.ts";
exportToHtml("showMenu", () => {
function showMenu() {
const navbar = document.getElementById("navbar-content");
const current = navbar.getAttribute("mobile-display");
navbar.setAttribute("mobile-display", current === "hidden" ? "revealed" : "hidden");
});
}
document.addEventListener("alpine:init", () => {
function navbarInit() {
const menuItems = document.querySelectorAll(".navbar details[name='navbar'].menu");
const isDesktop = () => {
return window.innerWidth >= 500;
@@ -33,4 +31,9 @@ document.addEventListener("alpine:init", () => {
}
});
}
});
}
export default () => {
Object.assign(document, { showMenu });
document.addEventListener("alpine:init", navbarInit);
};
@@ -1,3 +0,0 @@
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
polyfillCountryFlagEmojis();
-11
View File
@@ -1,11 +0,0 @@
import htmx from "htmx.org";
document.body.addEventListener("htmx:beforeRequest", (event) => {
event.detail.target.ariaBusy = true;
});
document.body.addEventListener("htmx:beforeSwap", (event) => {
event.detail.target.ariaBusy = null;
});
Object.assign(window, { htmx });
+3 -3
View File
@@ -1,6 +1,5 @@
// biome-ignore lint/performance/noNamespaceImport: this is the recommended way from the documentation
import * as Sentry from "@sentry/browser";
import { exportToHtml } from "#core:utils/globals.ts";
interface LoggedUser {
name: string;
@@ -13,7 +12,7 @@ interface SentryOptions {
user?: LoggedUser;
}
exportToHtml("loadSentryPopup", (options: SentryOptions) => {
const loadSentryPopup = (options: SentryOptions) => {
Sentry.init({
dsn: options.dsn,
});
@@ -21,4 +20,5 @@ exportToHtml("loadSentryPopup", (options: SentryOptions) => {
eventId: options.eventId,
...(options.user ?? {}),
});
});
};
Object.assign(window, { loadSentryPopup });
+1 -1
View File
@@ -1,4 +1,4 @@
import type { NestedKeyOf } from "#core:utils/types.ts";
import type { NestedKeyOf } from "#core:types/nested-key";
interface StringifyOptions<T extends object> {
/** The columns to include in the resulting CSV. */
-14
View File
@@ -5,17 +5,3 @@ declare global {
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;
}
+51 -23
View File
@@ -1,36 +1,64 @@
import { Alpine } from "alpinejs";
export enum NotificationLevel {
Error = "error",
Warning = "warning",
Success = "success",
}
export function createNotification(message: string, level: NotificationLevel) {
const element = document.getElementById("quick-notifications");
if (element === null) {
return false;
}
return element.dispatchEvent(
new CustomEvent("quick-notification-add", {
detail: { text: message, tag: level },
}),
);
export interface NotificationPlugin {
/**
* Add an error message to the notifications.
*/
error: (message: string) => void;
/**
* Add a warning message to the notifications
*/
warning: (message: string) => void;
/**
* Add a success message to the notifications
*/
success: (message: string) => void;
/**
* Remove all notifications displayed on the page.
*/
clear: () => void;
/**
* Add multiple notifications at once.
* The added notifications can have different notification levels.
*/
addMany: (notifs: Notification[]) => void;
/**
* Return all notifications displayed on the page.
*/
getAll: () => Notification[];
}
export function deleteNotifications() {
const element = document.getElementById("quick-notifications");
if (element === null) {
return false;
}
return element.dispatchEvent(new CustomEvent("quick-notification-delete"));
export interface Notification {
tag: NotificationLevel;
text: string;
}
export function alpinePlugin() {
return {
Alpine.store("notifications", [] as Notification[]);
function createNotification(message: string, level: NotificationLevel) {
(Alpine.store("notifications") as Notification[]).push({ text: message, tag: level });
}
function createManyNotifications(notifs: Notification[]) {
for (const notif of notifs) {
createNotification(notif.text, notif.tag);
}
}
export const notifications: NotificationPlugin = {
error: (message: string) => createNotification(message, NotificationLevel.Error),
warning: (message: string) =>
createNotification(message, NotificationLevel.Warning),
success: (message: string) =>
createNotification(message, NotificationLevel.Success),
clear: () => deleteNotifications(),
warning: (message: string) => createNotification(message, NotificationLevel.Warning),
success: (message: string) => createNotification(message, NotificationLevel.Success),
clear: () => Alpine.store("notifications", []),
addMany: (notifs: Notification[]) => createManyNotifications(notifs),
getAll: () => Alpine.store("notifications") as Notification[],
};
export function notificationsPlugin(GlobalAlpine: Alpine) {
GlobalAlpine.magic("notifications", () => ({ ...notifications }));
}
+22 -13
View File
@@ -398,6 +398,28 @@ body {
}
}
/* Fontawesome icons */
.fa-brands, .fa-link {
color: black;
}
.fa-facebook {
color: $faceblue;
}
.fa-discord {
color: $discordblurple;
}
.fa-square-instagram::before, .fa-instagram::before {
background: $instagradient;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.fa-bluesky, .fa-square-bluesky {
color: #0f73ff;
}
}
@media screen and (max-width: $small-devices) {
@@ -749,16 +771,3 @@ textarea {
vertical-align: middle;
}
}
/*--------------------------------JQuery-------------------------------*/
#club_detail {
.club_logo {
float: right;
img {
display: block;
max-height: 10em;
max-width: 10em;
}
}
}
+2 -5
View File
@@ -35,12 +35,9 @@
<noscript><link rel="stylesheet" href="{{ static('bundled/fontawesome-index.css') }}"></noscript>
<script src="{{ url('javascript-catalog') }}"></script>
<script type="module" src="{{ static("bundled/core/navbar-index.ts") }}"></script>
<script type="module" src="{{ static("bundled/base-bundle-index.ts") }}"></script>
<script type="module" src="{{ static("bundled/core/components/include-index.ts") }}"></script>
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
<script type="module" src="{{ static('bundled/htmx-index.js') }}"></script>
<script type="module" src="{{ static('bundled/country-flags-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/core/tooltips-index.ts') }}"></script>
<script type="module" src="{{ static("bundled/core/tooltips-index.ts") }}"></script>
{% block additional_css %}{% endblock %}
{% block additional_js %}{% endblock %}
+5 -8
View File
@@ -1,16 +1,13 @@
<div id="quick-notifications"
x-data='{
messages: [
x-init='$notifications.addMany([
{%- for message in messages -%}
{%- if not message.extra_tags -%}
{ tag: {{ message.tags|string|tojson }}, text: {{ message|string|tojson }} },
{ tag: "{{ message.tags }}", text: {{ message|string|tojson }} },
{%- endif -%}
{%- endfor -%}
]
}'
@quick-notification-add="(e) => messages.push(e?.detail)"
@quick-notification-delete="messages = []">
<template x-for="(message, index) in messages">
])'
>
<template x-for="(message, index) in $notifications.getAll()">
<div class="alert" :class="`alert-${message.tag}`" x-transition>
<span class="alert-main" x-text="message.text"></span>
<span class="clickable" @click="messages = messages.filter((item, i) => i !== index)">
+13 -10
View File
@@ -226,7 +226,7 @@
<button type="button" onclick="checkbox_{{form_id}}(false);">{% trans %}Unselect All{% endtrans %}</button>
{% endmacro %}
{% macro update_notifications(messages, clear) %}
{% macro update_notifications(messages, clear = True) %}
{# Update notification area from new messages sent by django backend
This is useful when performing fragment swaps to keep messages up to date
Without this, the fragment would need to take control of the notification area and
@@ -236,16 +236,19 @@
messages: messages from django.contrib
clear : optional boolean that controls if notifications should be cleared first. True is the default
#}
{% set clear = clear|default(true) %}
{% if messages %}
<div x-init="() => {
{% if clear %}
$notifications.clear()
{% endif %}
{% for message in messages %}
$notifications.{{ message.tags }}('{{ message }}')
{% endfor %}
}"></div>
<div x-init='() => {
{%- if clear -%}
$notifications.clear();
{%- endif -%}
$notifications.addMany([
{%- for message in messages -%}
{%- if not message.extra_tags -%}
{ tag: "{{ message.tags }}", text: {{ message|string|tojson }} },
{%- endif -%}
{%- endfor -%}
])
}'></div>
{% endif %}
{% endmacro %}
+1 -1
View File
@@ -103,7 +103,7 @@ def add_attr(field: BoundField, attr: str):
if "=" not in d:
attrs["class"] = d
else:
key, val = d.split("=")
key, val = d.split("=", maxsplit=1)
attrs[key] = val
return field.as_widget(attrs=attrs)
+1 -1
View File
@@ -54,7 +54,7 @@ class FragmentRenderer(Protocol):
) -> SafeString: ...
class FragmentMixin(TemplateResponseMixin, ContextMixin):
class FragmentMixin(TemplateResponseMixin, AllowFragment, ContextMixin):
"""Make a view buildable as a fragment that can be embedded in a template.
Most fragments are used in two different ways :
+2 -1
View File
@@ -32,8 +32,9 @@ class Migration(migrations.Migration):
(
"result",
models.OneToOneField(
help_text="The formula product.",
help_text="The product got with the formula.",
on_delete=django.db.models.deletion.CASCADE,
related_name="formula",
to="counter.product",
verbose_name="result product",
),
@@ -1,9 +1,9 @@
import { showSaveFilePicker } from "native-file-system-adapter";
import type TomSelect from "tom-select";
import type { NestedKeyOf } from "#core:types/nested-key";
import { paginated } from "#core:utils/api";
import { csv } from "#core:utils/csv";
import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history";
import type { NestedKeyOf } from "#core:utils/types";
import {
type ProductSchema,
type ProductSearchProductsDetailedData,
+2 -2
View File
@@ -35,8 +35,8 @@ les fichiers sont à mettre dans un dossier `static/bundled` de l'application à
Pour accéder au fichier, il faut utiliser `static` comme pour le reste mais en ajouter `bundled/` comme prefix.
```jinja
{# Example pour ajouter sith/core/bundled/alpine-index.js #}
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
{# Example pour ajouter sith/core/bundled/alpine-index.ts #}
<script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/other-index.ts') }}"></script>
```
+75
View File
@@ -0,0 +1,75 @@
[Documentation du localStorage (mozilla)](https://developer.mozilla.org/fr/docs/Web/API/Window/localStorage)
## Utilité et limitations
Le `localStorage` est un cache géré directement par le navigateur.
Il permet de stocker des données directement chez le client.
Il s'agit donc d'un outil extrêmement puissant, qui permet d'éviter
beaucoup de requêtes au serveur, améliorant ainsi les temps de chargement.
Cependant, il y a deux limitations majeures à prendre en compte :
- le `localStorage` est entièrement géré par le client,
une fois le déploiement effectué, vous ne pouvez plus y toucher ;
vous devez donc être sûr de vous avant d'apporter des modifications
reposant sur le `localStorage`.
- la quantité de données stockable est limitée à 10Mo ;
une fois ce quota rempli, le navigateur lèvera une `QuotaExceededError`.
## Invalidation du `localStorage`
Pour résoudre le premier de ces deux problèmes, il y a un script permettant
d'annuler une partie du cache.
Ce dernier se trouve dans le fichier `core/static/bundled/core/cache.ts`.
Vous devrez modifier ce fichier chaque fois qu'un élément du localStorage
cessera d'être utilisé.
Les modifications à apporter sont les suivantes :
- incrémenter la version du cache
- ajouter une ligne permettant de retirer votre clef du cache
```ts hl_lines="2 11"
// increment this number when a breaking change is made with localStorage
const CURRENT_CACHE_VERSION = 2; // <-- changez cette ligne
export function cacheBuster() {
const version = Number.parseInt(localStorage.getItem("version") ?? "0", 10);
if (version === CURRENT_CACHE_VERSION) {
// The cache schema is up-to-date. Nothing to do.
return;
}
// ...
localStorage.removeItem("<clef>"); // <-- et rajoutez cette ligne
localStorage.setItem("version", CURRENT_CACHE_VERSION.toString());
}
```
## Versionnage d'une clef
Dans le cas où une paire clef-valeur du localStorage subit un changement
dans son schéma de données, utilisez `versionedLocalStorage` :
```typescript
import { versionedLocalStorage } from "#core:core/localstorage";
const foo = () => {
let obj = versionedLocalStorage.getItem("<key>", { version: 1 });
if (obj === null) {
obj = fetchMyObject();
versionedLocalStorage.setItem("<key>", obj, { version: 1 })
}
// Do something with obj...
}
```
!!!Warning
Il existe une différence d'usage entre `localStorage` et `versionedLocalStorage` :
les valeurs données à `localStorage` doivent être des strings (généralement
obtenus avec `JSON.stringify`), tandis que `versionedLocalStorage` utilise
directement des objets JS.
Cette différence résulte du fait que `versionedLocalStorage` doit légèrement
modifier les données pour y inclure la version, et gérer en interne
la conversion en JSON.
+10 -12
View File
@@ -1,4 +1,4 @@
export {};
import { versionedLocalStorage } from "#core:core/localstorage";
interface BasketItem {
priceId: number;
@@ -7,8 +7,8 @@ interface BasketItem {
unitPrice: number;
}
// increment the key number if the data schema of the cached basket changes
const BASKET_CACHE_KEY = "basket1";
const BASKET_CACHE_KEY = "basket";
const BASKET_CACHE_VERSION = 1;
document.addEventListener("alpine:init", () => {
Alpine.data("basket", (lastPurchaseTime?: number) => ({
@@ -34,18 +34,16 @@ document.addEventListener("alpine:init", () => {
},
loadBasket(): BasketItem[] {
if (localStorage.getItem(BASKET_CACHE_KEY) === null) {
return [];
}
try {
return JSON.parse(localStorage.getItem(BASKET_CACHE_KEY));
} catch (_err) {
return [];
}
const cached = versionedLocalStorage.getItem<BasketItem[]>(BASKET_CACHE_KEY, {
version: BASKET_CACHE_VERSION,
});
return cached ?? [];
},
saveBasket() {
localStorage.setItem(BASKET_CACHE_KEY, JSON.stringify(this.basket));
versionedLocalStorage.setItem(BASKET_CACHE_KEY, this.basket, {
version: BASKET_CACHE_VERSION,
});
localStorage.setItem("basketTimestamp", Date.now().toString());
},
@@ -32,5 +32,7 @@
</form>
</div>
<br>
{% if is_fragment %}
{{ update_notifications(messages) }}
{% endif %}
</div>
+82 -20
View File
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-12 09:52+0200\n"
"POT-Creation-Date: 2026-05-12 11:12+0200\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -362,6 +362,62 @@ msgstr "Cet email est déjà abonné à cette mailing"
msgid "Unregistered user"
msgstr "Utilisateur non enregistré"
#: club/models.py
#, python-format
msgid "The base url that links with this type must respect (e.g. `%(url)s`)"
msgstr ""
"L'url de base que tous les liens de ce type doivent respecter (par exemple "
"`%(url)s`)"
#: club/models.py counter/models.py
msgid "icon"
msgstr "icône"
#: club/models.py
msgid "The fontawesome class to use (e.g. `fa-brands fa-instagram`)"
msgstr ""
"La classe fontawesome à utiliser (par exemple `fa-brands fa-instagram`)"
#: club/models.py
msgid "link type"
msgstr "type de lien"
#: club/models.py
msgid "link types"
msgstr "types de lien"
#: club/models.py
msgid "link url"
msgstr "url du lien"
#: club/models.py core/models.py counter/models.py
msgid "created at"
msgstr "créé le"
#: club/models.py core/models.py counter/models.py
msgid "updated at"
msgstr "mis à jour le"
#: club/models.py
msgid "club link"
msgstr "lien de club"
#: club/models.py
msgid "club links"
msgstr "liens de club"
#: club/models.py
msgid "Duplicated url"
msgstr "Url dupliquée"
#: club/models.py
msgid "This link doesn't match with the url base of its type."
msgstr "Ce lien ne correspond pas à l'url de base de son type."
#: club/templates/club/club_detail.jinja com/templates/com/news_list.jinja
msgid "Links"
msgstr "Liens"
#: club/templates/club/club_list.jinja
msgid "The list of all clubs existing at UTBM."
msgstr "La liste de tous les clubs existants à l'UTBM"
@@ -708,6 +764,14 @@ msgstr "Comptoirs : "
msgid "Edit %(name)s"
msgstr "Éditer %(name)s"
#: club/templates/club/edit_club.jinja
msgid "This icon will change according to the given url."
msgstr "Cette icône changera en fonction de l'url fournie"
#: club/templates/club/edit_club.jinja
msgid "Remove link"
msgstr "Retirer le lien"
#: club/templates/club/edit_club.jinja
msgid "Club properties"
msgstr "Propriétés du club"
@@ -732,6 +796,22 @@ msgstr ""
"Les champs de formulaire suivants sont liées à la description basique d'un "
"club. Tous les membres du bureau du club peuvent voir et modifier ceux-ci."
#: club/templates/club/edit_club.jinja
msgid "Club links"
msgstr "Liens du club"
#: club/templates/club/edit_club.jinja
msgid ""
"Note: if the icon of one of your links doesn't exist yet, you can ask the "
"info team to add it."
msgstr ""
"Note : si l'icône d'un de vos liens n'existe pas encore, vous pouvez "
"demander au pôle info de l'ajouter."
#: club/templates/club/edit_club.jinja
msgid "Add link"
msgstr "Ajouter un lien"
#: club/templates/club/fragments/add_member.jinja
msgid "Add a new member"
msgstr "Ajouter un nouveau membre"
@@ -1251,10 +1331,6 @@ msgstr ""
msgid "All coming events"
msgstr "Tous les événements à venir"
#: com/templates/com/news_list.jinja
msgid "Links"
msgstr "Liens"
#: com/templates/com/news_list.jinja
msgid "Our services"
msgstr "Nos services"
@@ -1760,10 +1836,6 @@ msgstr "Visiteur"
msgid "ban type"
msgstr "type de ban"
#: core/models.py counter/models.py
msgid "created at"
msgstr "créé le"
#: core/models.py
msgid "expires at"
msgstr "expire le"
@@ -1853,10 +1925,6 @@ msgstr "taille"
msgid "date"
msgstr "date"
#: core/models.py counter/models.py
msgid "updated at"
msgstr "mis à jour le"
#: core/models.py
msgid "asked for removal"
msgstr "retrait demandé"
@@ -3327,10 +3395,6 @@ msgstr "prix d'achat"
msgid "Initial cost of purchasing the product"
msgstr "Coût initial d'achat du produit"
#: counter/models.py
msgid "icon"
msgstr "icône"
#: counter/models.py
msgid "limit age"
msgstr "âge limite"
@@ -5497,10 +5561,8 @@ msgid "Amicale/DOCEO member"
msgstr "Membre de l'Amicale/DOCEO"
#: sith/settings.py
#, fuzzy
#| msgid "UT network member"
msgid "UT network member (excluding UTC)"
msgstr "Cotisant du réseau UT"
msgstr "Cotisant du réseau UT (hors UTC)"
#: sith/settings.py
msgid "CROUS member"
+2
View File
@@ -66,6 +66,8 @@ nav:
- Gestion des permissions: tutorial/perms.md
- Gestion des groupes: tutorial/groups.md
- Les fragments: tutorial/fragments.md
- Frontend:
- localStorage: tutorial/front/localstorage.md
- API:
- Développement: tutorial/api/dev.md
- Connexion à l'API: tutorial/api/connect.md
+227 -198
View File
@@ -9,7 +9,7 @@
"version": "3",
"license": "GPL-3.0-only",
"dependencies": {
"@alpinejs/sort": "^3.15.11",
"@alpinejs/sort": "^3.15.12",
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
"@floating-ui/dom": "^1.7.6",
"@fortawesome/fontawesome-free": "^7.2.0",
@@ -17,45 +17,46 @@
"@fullcalendar/daygrid": "^6.1.20",
"@fullcalendar/icalendar": "^6.1.20",
"@fullcalendar/list": "^6.1.20",
"@sentry/browser": "^10.51.0",
"@sentry/browser": "^10.53.1",
"@zip.js/zip.js": "^2.8.26",
"3d-force-graph": "^1.80.0",
"alpinejs": "^3.15.11",
"alpinejs": "^3.15.12",
"chart.js": "^4.5.1",
"country-flag-emoji-polyfill": "^0.1.8",
"cytoscape": "^3.33.2",
"cytoscape": "^3.33.4",
"cytoscape-cxtmenu": "^3.5.0",
"cytoscape-klay": "^3.1.4",
"d3-force-3d": "^3.0.6",
"easymde": "^2.20.0",
"easymde": "^2.21.0",
"glob": "^13.0.6",
"html2canvas": "^1.4.1",
"htmx.org": "^2.0.10",
"js-cookie": "^3.0.5",
"lit-html": "^3.3.2",
"js-cookie": "^3.0.7",
"lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1",
"three": "^0.184.0",
"three-spritetext": "^1.10.0",
"tom-select": "^2.6.0"
"tom-select": "^2.6.1"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@biomejs/biome": "^2.4.13",
"@babel/preset-env": "^7.29.5",
"@biomejs/biome": "^2.4.15",
"@hey-api/openapi-ts": "^0.94.5",
"@types/alpinejs": "^3.13.11",
"@types/alpinejs__sort": "^3.13.0",
"@types/cytoscape-cxtmenu": "^3.4.5",
"@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6",
"rollup-plugin-visualizer": "^7.0.1",
"typescript": "^5.9.3",
"vite": "^8.0.10"
"vite": "^8.0.13"
}
},
"node_modules/@alpinejs/sort": {
"version": "3.15.11",
"resolved": "https://registry.npmjs.org/@alpinejs/sort/-/sort-3.15.11.tgz",
"integrity": "sha512-HaDZ0jP7OYjRJ8Pv1aErOd3EaILq6pQWr+g3ZY9ddHTA37o1NoLfe20tu8AI6SOs3dppqxykXG99RTn/tsyShA==",
"version": "3.15.12",
"resolved": "https://registry.npmjs.org/@alpinejs/sort/-/sort-3.15.12.tgz",
"integrity": "sha512-DNIS7SQFg4H4o5faluRgqYEPi1Q7Hf+HMDgoCcIHXbXWH0g66WPEzz9OMk1zsUYib0KxMms4xXOqk+xh2tHZzg==",
"license": "MIT",
"dependencies": {
"sortablejs": "^1.15.2"
@@ -83,9 +84,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
"integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -480,6 +481,23 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": {
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz",
"integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
@@ -999,9 +1017,9 @@
}
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz",
"integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==",
"version": "7.29.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
"integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1434,19 +1452,20 @@
}
},
"node_modules/@babel/preset-env": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz",
"integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==",
"version": "7.29.5",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz",
"integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.29.0",
"@babel/compat-data": "^7.29.3",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
"@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
"@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3",
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6",
"@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
@@ -1478,7 +1497,7 @@
"@babel/plugin-transform-member-expression-literals": "^7.27.1",
"@babel/plugin-transform-modules-amd": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-modules-systemjs": "^7.29.0",
"@babel/plugin-transform-modules-systemjs": "^7.29.4",
"@babel/plugin-transform-modules-umd": "^7.27.1",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0",
"@babel/plugin-transform-new-target": "^7.27.1",
@@ -1591,9 +1610,9 @@
}
},
"node_modules/@biomejs/biome": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.13.tgz",
"integrity": "sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA==",
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.15.tgz",
"integrity": "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
@@ -1607,20 +1626,20 @@
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.4.13",
"@biomejs/cli-darwin-x64": "2.4.13",
"@biomejs/cli-linux-arm64": "2.4.13",
"@biomejs/cli-linux-arm64-musl": "2.4.13",
"@biomejs/cli-linux-x64": "2.4.13",
"@biomejs/cli-linux-x64-musl": "2.4.13",
"@biomejs/cli-win32-arm64": "2.4.13",
"@biomejs/cli-win32-x64": "2.4.13"
"@biomejs/cli-darwin-arm64": "2.4.15",
"@biomejs/cli-darwin-x64": "2.4.15",
"@biomejs/cli-linux-arm64": "2.4.15",
"@biomejs/cli-linux-arm64-musl": "2.4.15",
"@biomejs/cli-linux-x64": "2.4.15",
"@biomejs/cli-linux-x64-musl": "2.4.15",
"@biomejs/cli-win32-arm64": "2.4.15",
"@biomejs/cli-win32-x64": "2.4.15"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.13.tgz",
"integrity": "sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg==",
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.15.tgz",
"integrity": "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==",
"cpu": [
"arm64"
],
@@ -1635,9 +1654,9 @@
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.13.tgz",
"integrity": "sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg==",
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.15.tgz",
"integrity": "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==",
"cpu": [
"x64"
],
@@ -1652,9 +1671,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.13.tgz",
"integrity": "sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==",
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.15.tgz",
"integrity": "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==",
"cpu": [
"arm64"
],
@@ -1672,9 +1691,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.13.tgz",
"integrity": "sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg==",
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.15.tgz",
"integrity": "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==",
"cpu": [
"arm64"
],
@@ -1692,9 +1711,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.13.tgz",
"integrity": "sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==",
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.15.tgz",
"integrity": "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==",
"cpu": [
"x64"
],
@@ -1712,9 +1731,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.13.tgz",
"integrity": "sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==",
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.15.tgz",
"integrity": "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==",
"cpu": [
"x64"
],
@@ -1732,9 +1751,9 @@
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.13.tgz",
"integrity": "sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==",
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.15.tgz",
"integrity": "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==",
"cpu": [
"arm64"
],
@@ -1749,9 +1768,9 @@
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.4.13",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.13.tgz",
"integrity": "sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ==",
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.15.tgz",
"integrity": "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==",
"cpu": [
"x64"
],
@@ -2091,9 +2110,9 @@
"license": "Apache-2.0"
},
"node_modules/@oxc-project/types": {
"version": "0.127.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
"version": "0.130.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
"dev": true,
"license": "MIT",
"funding": {
@@ -2101,9 +2120,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
"cpu": [
"arm64"
],
@@ -2118,9 +2137,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
"cpu": [
"arm64"
],
@@ -2135,9 +2154,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
"cpu": [
"x64"
],
@@ -2152,9 +2171,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
"cpu": [
"x64"
],
@@ -2169,9 +2188,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
"cpu": [
"arm"
],
@@ -2186,9 +2205,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
"cpu": [
"arm64"
],
@@ -2206,9 +2225,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
"cpu": [
"arm64"
],
@@ -2226,9 +2245,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
"cpu": [
"ppc64"
],
@@ -2246,9 +2265,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
"cpu": [
"s390x"
],
@@ -2266,9 +2285,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
"cpu": [
"x64"
],
@@ -2286,9 +2305,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
"cpu": [
"x64"
],
@@ -2306,9 +2325,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
"cpu": [
"arm64"
],
@@ -2323,9 +2342,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
"cpu": [
"wasm32"
],
@@ -2342,9 +2361,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
"cpu": [
"arm64"
],
@@ -2359,9 +2378,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
"cpu": [
"x64"
],
@@ -2376,82 +2395,82 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/@sentry-internal/browser-utils": {
"version": "10.51.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.51.0.tgz",
"integrity": "sha512-lNKBS4P7RUvf1niojXQWe9bU3gnBUCbST4Dj0pSiyat1N96cXVyHkeE+uGxowD0RrVWhs+kGHiVX3FcmRWF6sA==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.53.1.tgz",
"integrity": "sha512-X4d6y8sBMjmNhcDW4eMBU3ASsNIMz8dqaFkhyIMN/dkYr/yZKnbRZPaVuVUGvHKjnlficPpIH0/HK9KBjrYxPw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.51.0"
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "10.51.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.51.0.tgz",
"integrity": "sha512-bCM95bcpphx28e6aU0bwRLxOgwosYsdNzezM1sM0pVOkb0TB3hDFRamramVDK+/Hp1o8qmRxS4c5w/A7YBZGkA==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.53.1.tgz",
"integrity": "sha512-vVpTI/aEYN5d9IgZeYJWMqVaN0+iFgidSrYNAsZTh1US5sJUzF/wrl+68KdpmCtFROrN3jiAn1oPSwL5CKvEJA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.51.0"
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "10.51.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.51.0.tgz",
"integrity": "sha512-jCpI5HXSwK6ZT2HX70+mDRciAocHzSiDk4DTgvzV69Wvd+Ei5WLgE+d39eaEPsm8lUC0Ydntb5sJIB6uG9D4bw==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.53.1.tgz",
"integrity": "sha512-wZNzTBYkgGUPWMuUQv7L64+OJmoCnz7GQNiTrTFK6EVAjJXFBCSsPp/nhif0bLhbk8+0g4xz633uOhpXuQbFdw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.51.0",
"@sentry/core": "10.51.0"
"@sentry-internal/browser-utils": "10.53.1",
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "10.51.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.51.0.tgz",
"integrity": "sha512-8PW1Pp+Yl3lPwYqhBCr5SgkuhDanu9ZLzUqD2bPKL/ElqbM2eDVIWxq4z4ZzePrmZa6IcCjTv6sVQJ7Z4dLyLA==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.53.1.tgz",
"integrity": "sha512-aueLaf/2prExwA76BGU5/bOXCKWqtt6jQXWA6WJQNrmKpPEtZJB4ypnpsou0McXQCF8tur2Y8U0TEkwQP13yJQ==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "10.51.0",
"@sentry/core": "10.51.0"
"@sentry-internal/replay": "10.53.1",
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/browser": {
"version": "10.51.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.51.0.tgz",
"integrity": "sha512-Zdc0sKfenxUtW/OGhtJ7xHFN44bXR7YqxJ1zBDzlZfW0nTbeTTUZBq9z5NUw6qdS0Vs/i3V4qzAKTbRKWfqSEA==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.53.1.tgz",
"integrity": "sha512-zXF373hzUOGzUOrqd8xb1U3LQi5uYC3mwv+z5OMKUUinQlu30tTWBs7ypy6YTchtix9QlYaHWlayUF8vBZ5UjA==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.51.0",
"@sentry-internal/feedback": "10.51.0",
"@sentry-internal/replay": "10.51.0",
"@sentry-internal/replay-canvas": "10.51.0",
"@sentry/core": "10.51.0"
"@sentry-internal/browser-utils": "10.53.1",
"@sentry-internal/feedback": "10.53.1",
"@sentry-internal/replay": "10.53.1",
"@sentry-internal/replay-canvas": "10.53.1",
"@sentry/core": "10.53.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/core": {
"version": "10.51.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.51.0.tgz",
"integrity": "sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w==",
"version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.53.1.tgz",
"integrity": "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -2464,9 +2483,9 @@
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -2481,6 +2500,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/alpinejs__sort": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@types/alpinejs__sort/-/alpinejs__sort-3.13.0.tgz",
"integrity": "sha512-iR9vEy6e3yXbYAK45/hpulzlt8SSKTsvYUl/t5nuWjtbJPoGxzxUUqOm3egp83Gqtf//TyJnDCI4OTebAKDRAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/alpinejs": "*"
}
},
"node_modules/@types/codemirror": {
"version": "5.60.17",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz",
@@ -2603,9 +2632,9 @@
}
},
"node_modules/alpinejs": {
"version": "3.15.11",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.11.tgz",
"integrity": "sha512-m26gkTg/MId8O+F4jHKK3vB3SjbFxxk/JHP+qzmw1H6aQrZuPAg4CUoAefnASzzp/eNroBjrRQe7950bNeaBJw==",
"version": "3.15.12",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz",
"integrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "~3.1.1"
@@ -2728,9 +2757,9 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -2996,9 +3025,9 @@
}
},
"node_modules/cytoscape": {
"version": "3.33.2",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz",
"integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==",
"version": "3.33.4",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.4.tgz",
"integrity": "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==",
"license": "MIT",
"engines": {
"node": ">=0.10"
@@ -3295,9 +3324,9 @@
}
},
"node_modules/easymde": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz",
"integrity": "sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==",
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.21.0.tgz",
"integrity": "sha512-5uE7I/DEN8gvGRwxaqAv7h1PMEK2ykNXVX5zL0dK3nCYROGja3AMbdQz8eCEELnfvCfy7tRkTmLuvyJG8uSWjQ==",
"license": "MIT",
"dependencies": {
"@types/codemirror": "^5.60.10",
@@ -3656,12 +3685,12 @@
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz",
"integrity": "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==",
"license": "MIT",
"engines": {
"node": ">=14"
"node": ">=20"
}
},
"node_modules/js-tokens": {
@@ -3990,9 +4019,9 @@
}
},
"node_modules/lit-html": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz",
"integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz",
"integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/trusted-types": "^2.0.2"
@@ -4065,9 +4094,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
@@ -4332,9 +4361,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -4352,7 +4381,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -4498,14 +4527,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.127.0",
"@rolldown/pluginutils": "1.0.0-rc.17"
"@oxc-project/types": "=0.130.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -4514,21 +4543,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
"@rolldown/binding-android-arm64": "1.0.1",
"@rolldown/binding-darwin-arm64": "1.0.1",
"@rolldown/binding-darwin-x64": "1.0.1",
"@rolldown/binding-freebsd-x64": "1.0.1",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
"@rolldown/binding-linux-arm64-gnu": "1.0.1",
"@rolldown/binding-linux-arm64-musl": "1.0.1",
"@rolldown/binding-linux-ppc64-gnu": "1.0.1",
"@rolldown/binding-linux-s390x-gnu": "1.0.1",
"@rolldown/binding-linux-x64-gnu": "1.0.1",
"@rolldown/binding-linux-x64-musl": "1.0.1",
"@rolldown/binding-openharmony-arm64": "1.0.1",
"@rolldown/binding-wasm32-wasi": "1.0.1",
"@rolldown/binding-win32-arm64-msvc": "1.0.1",
"@rolldown/binding-win32-x64-msvc": "1.0.1"
}
},
"node_modules/rollup-plugin-visualizer": {
@@ -4785,9 +4814,9 @@
}
},
"node_modules/tom-select": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.6.0.tgz",
"integrity": "sha512-o2ToBjhUAnrrQvW/hrY9c//TpOpAKYSlfuFnf0DIwNy+ua+mmYnsF4PxN/PpzBfUIfEFkNYAngeGBfOAZWF3tw==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.6.1.tgz",
"integrity": "sha512-d/1kngVOQTGcI/2pVDfDLYjtjUgSSd3fSgkYUpi0y+yRtQQu2kzljj3aUdqMfqc45cjPvDEpfDt/hSX4awDFTg==",
"license": "Apache-2.0",
"dependencies": {
"@orchidjs/sifter": "^1.1.0",
@@ -4914,16 +4943,16 @@
}
},
"node_modules/vite": {
"version": "8.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.10",
"rolldown": "1.0.0-rc.17",
"postcss": "^8.5.14",
"rolldown": "1.0.1",
"tinyglobby": "^0.2.16"
},
"bin": {
@@ -4940,7 +4969,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"@vitejs/devtools": "^0.1.18",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
+12 -11
View File
@@ -25,19 +25,20 @@
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@biomejs/biome": "^2.4.13",
"@babel/preset-env": "^7.29.5",
"@biomejs/biome": "^2.4.15",
"@hey-api/openapi-ts": "^0.94.5",
"@types/alpinejs": "^3.13.11",
"@types/alpinejs__sort": "^3.13.0",
"@types/cytoscape-cxtmenu": "^3.4.5",
"@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6",
"rollup-plugin-visualizer": "^7.0.1",
"typescript": "^5.9.3",
"vite": "^8.0.10"
"vite": "^8.0.13"
},
"dependencies": {
"@alpinejs/sort": "^3.15.11",
"@alpinejs/sort": "^3.15.12",
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
"@floating-ui/dom": "^1.7.6",
"@fortawesome/fontawesome-free": "^7.2.0",
@@ -45,25 +46,25 @@
"@fullcalendar/daygrid": "^6.1.20",
"@fullcalendar/icalendar": "^6.1.20",
"@fullcalendar/list": "^6.1.20",
"@sentry/browser": "^10.51.0",
"@sentry/browser": "^10.53.1",
"@zip.js/zip.js": "^2.8.26",
"3d-force-graph": "^1.80.0",
"alpinejs": "^3.15.11",
"alpinejs": "^3.15.12",
"chart.js": "^4.5.1",
"country-flag-emoji-polyfill": "^0.1.8",
"cytoscape": "^3.33.2",
"cytoscape": "^3.33.4",
"cytoscape-cxtmenu": "^3.5.0",
"cytoscape-klay": "^3.1.4",
"d3-force-3d": "^3.0.6",
"easymde": "^2.20.0",
"easymde": "^2.21.0",
"glob": "^13.0.6",
"html2canvas": "^1.4.1",
"htmx.org": "^2.0.10",
"js-cookie": "^3.0.5",
"lit-html": "^3.3.2",
"js-cookie": "^3.0.7",
"lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1",
"three": "^0.184.0",
"three-spritetext": "^1.10.0",
"tom-select": "^2.6.0"
"tom-select": "^2.6.1"
}
}
+30 -30
View File
@@ -19,38 +19,38 @@ authors = [
license = { text = "GPL-3.0-only" }
requires-python = "<4.0,>=3.12"
dependencies = [
"django>=5.2.13,<6.0.0",
"django-ninja>=1.6.2,<6.0.0",
"django>=5.2.14,<6.0.0",
"django-ninja>=1.6.2,<2.0.0",
"django-ninja-extra>=0.31.4",
"Pillow>=12.2.0,<13.0.0",
"mistune>=3.2.0,<4.0.0",
"mistune>=3.2.1,<4.0.0",
"django-jinja<3.0.0,>=2.11.0",
"cryptography>=47.0.0,<48.0.0",
"cryptography>=48.0.0,<49.0.0",
"django-phonenumber-field>=8.4.0,<9.0.0",
"phonenumbers>=9.0.29,<10.0.0",
"reportlab>=4.5.0,<5.0.0",
"phonenumbers>=9.0.30,<10.0.0",
"reportlab>=4.5.1,<5.0.0",
"django-haystack>=3.3.0,<4.0.0",
"xapian-haystack>=4.0.0,<5.0.0",
"libsass<1.0.0,>=0.23.0",
"django-ordered-model<4.0.0,>=3.7.4",
"django-simple-captcha<1.0.0,>=0.6.3",
"python-dateutil<3.0.0.0,>=2.9.0.post0",
"sentry-sdk>=2.58.0,<3.0.0",
"jinja2<4.0.0,>=3.1.6",
"libsass>=0.23.0,<1.0.0",
"django-ordered-model>=3.7.4,<4.0.0",
"django-simple-captcha>=0.6.3,<1.0.0",
"python-dateutil>=2.9.0.post0,<3.0.0.0",
"sentry-sdk>=2.60.0,<3.0.0",
"jinja2>=3.1.6,<4.0.0",
"django-countries>=8.2.0,<9.0.0",
"dict2xml>=1.7.8,<2.0.0",
"Sphinx<6,>=5",
"Sphinx>=9.1.0,<10", # Used by xapian during installation
"tomli>=2.4.1,<3.0.0",
"django-honeypot>=1.3.0,<2",
"pydantic-extra-types>=2.11.1,<3.0.0",
"ical>=11.1.0,<14.0.0",
"redis[hiredis]>=6.4.0,<8.0.0",
"environs[django]>=15.0.1,<16.0.0",
"requests>=2.32.5,<3.0.0",
"ical>=12.0.0,<14.0.0",
"redis[hiredis]>=3.3.1,<8.0.0",
"environs[django]>=15.0.1,<16",
"requests>=2.34.2,<3.0.0",
"honcho>=2.0.0",
"psutil>=7.2.2,<8.0.0",
"celery[redis]>=5.6.2,<7",
"django-celery-results>=2.5.1",
"celery[redis]>=5.6.3,<8",
"django-celery-results>=2.6.0",
"django-celery-beat>=2.9.0",
]
@@ -60,43 +60,43 @@ documentation = "https://sith-ae.readthedocs.io/"
[dependency-groups]
prod = [
"psycopg[c]>=3.3.3,<4.0.0",
"psycopg[c]>=3.3.4,<4.0.0",
]
dev = [
"django-debug-toolbar>=6.3.0,<7",
"ipython>=9.13.0,<10.0.0",
"pre-commit>=4.6.0,<5.0.0",
"ruff>=0.15.12,<1.0.0",
"ruff>=0.15.13,<1.0.0",
"djhtml>=3.0.11,<4.0.0",
"faker>=40.15.0,<41.0.0",
"faker>=40.18.0,<41.0.0",
"rjsmin>=1.2.5,<2.0.0",
]
tests = [
"freezegun>=1.5.5,<2.0.0",
"pytest>=9.0.3,<10.0.0",
"pytest-cov>=7.1.0,<8.0.0",
"pytest-django<5.0.0,>=4.12.0",
"model-bakery<2.0.0,>=1.23.4",
"pytest-django>=4.12.0,<5.0.0",
"model-bakery>=1.23.4,<2.0.0",
"beautifulsoup4>=4.14.3,<5",
"lxml>=6.1.0,<7",
"lxml>=6.1.1,<7",
]
docs = [
"mkdocs<2.0.0,>=1.6.1",
"mkdocs>=1.6.1,<2.0.0",
"mkdocs-material>=9.7.6,<10.0.0",
"mkdocstrings>=1.0.4,<2.0.0",
"mkdocstrings-python>=2.0.3,<3.0.0",
"mkdocs-include-markdown-plugin>=7.2.2,<8.0.0",
"mkdocs-include-markdown-plugin>=7.3.0,<8.0.0",
]
[tool.uv]
default-groups = ["dev", "tests", "docs"]
[tool.xapian]
version = "1.4.31"
version = "2.0.0"
# Those hashes are here to protect against supply chains attacks
# See `https://ae-utbm.github.io/sith/howto/xapian/` for more information
core-sha256 = "fecf609ea2efdc8a64be369715aac733336a11f7480a6545244964ae6bc80811"
bindings-sha256 = "a38cc7ba4188cc0bd27dc7369f03906772047087a1c54f1b93355d5e9103c304"
core-sha256 = "6cea3f49952a47224439a40bdb3608f928d121ad8721b9921cc42802d548ecf8"
bindings-sha256 = "9a544b69c31355a92edbcd4102cf0f1ec4407fd0a4645f4870fb52300b736910"
[tool.ruff]
output-format = "concise" # makes ruff error logs easier to read
+19 -19
View File
@@ -22,35 +22,35 @@ document.addEventListener("alpine:init", () => {
albums: [] as Album[],
async fetchPictures(): Promise<PictureSchema[]> {
const localStorageKey = `user${config.userId}Pictures`;
const localStorageInvalidationKey = `user${config.userId}PicturesNumber`;
const lastCachedNumber = localStorage.getItem(localStorageInvalidationKey);
// Check the cache before hitting the API.
const storageKey = "userPictures";
const cacheContent: { userId: number; pictures: PictureSchema[] }[] = JSON.parse(
sessionStorage.getItem(storageKey) || "[]",
);
const userPictures = cacheContent.find((obj) => obj.userId === config.userId);
if (
lastCachedNumber !== null &&
Number.parseInt(lastCachedNumber, 10) === config.nbPictures
userPictures !== undefined &&
userPictures.pictures.length === config.nbPictures
) {
return JSON.parse(localStorage.getItem(localStorageKey));
// The cached value is considered valid
// if it contains the right amount of pictures.
// This amount is known because it is given in the template.
return userPictures.pictures;
}
const pictures = await paginated(picturesFetchPictures, {
// biome-ignore lint/style/useNamingConvention: from python api
query: { users_identified: [config.userId] },
} as PicturesFetchPicturesData);
cacheContent.push({ userId: config.userId, pictures: pictures });
try {
localStorage.setItem(localStorageInvalidationKey, config.nbPictures.toString());
localStorage.setItem(localStorageKey, JSON.stringify(pictures));
// cache only the pictures of the last 4 visited profiles
sessionStorage.setItem(storageKey, JSON.stringify(cacheContent.slice(-4)));
} catch {
// an exception is raised if the localstorage is entirely filled
// so just delete all cached user pictures.
// an exception is raised if the storage is entirely filled.
// To be as safe as possible, delete the cached pictures.
// A cache hit is not worth the page breaking.
Object.keys(localStorage)
.filter(
(key) =>
key.startsWith("user") &&
(key.endsWith("Pictures") || key.endsWith("PicturesNumber")),
)
.forEach((key) => {
localStorage.removeItem(key);
});
sessionStorage.removeItem(storageKey);
}
return pictures;
},
+5
View File
@@ -88,6 +88,11 @@ X_FRAME_OPTIONS = "SAMEORIGIN"
ALLOWED_HOSTS = ["*"]
# RemovedInDjango60Warning: It's a transitional setting helpful in early
# adoption of "https" as the new default value of forms.URLField.assume_scheme.
# Remove this after upgrading to Django 6.x
FORMS_URLFIELD_ASSUME_HTTPS = True
# Application definition
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
+1 -1
View File
@@ -32,7 +32,7 @@ class JsBundlerManifestEntry:
# because that's what the user types when importing statics and that's what django gives us
# This is really similar to what we are doing in the bundler, it uses a similar algorithm
# Example:
# core/static/bundled/alpine-index.js -> bundled/alpine-index.js
# core/static/bundled/alpine-index.ts -> bundled/alpine-index.ts
# core/static/bundled/components/include-index.ts -> core/static/bundled/components/include-index.ts
def get_relative_src_name(name: str) -> str:
original_path = Path(name)
Generated
+391 -381
View File
File diff suppressed because it is too large Load Diff