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
58 changed files with 1669 additions and 1011 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.15.5 rev: v0.15.13
hooks: hooks:
- id: ruff-check # just check the code, and print the errors - id: ruff-check # just check the code, and print the errors
- id: ruff-check # actually fix the fixable errors, but print nothing - id: ruff-check # actually fix the fixable errors, but print nothing
@@ -14,7 +14,7 @@ repos:
- id: biome-check - id: biome-check
additional_dependencies: ["@biomejs/biome@2.4.6"] additional_dependencies: ["@biomejs/biome@2.4.6"]
- repo: https://github.com/rtts/djhtml - repo: https://github.com/rtts/djhtml
rev: 3.0.10 rev: 3.0.11
hooks: hooks:
- id: djhtml - id: djhtml
name: format templates name: format templates
+1 -1
View File
@@ -38,6 +38,6 @@
} }
}, },
"javascript": { "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.forms.models import ModelForm
from django.http import HttpRequest from django.http import HttpRequest
from club.models import Club, ClubRole, Membership from club.models import Club, ClubLink, ClubRole, LinkType, Membership
@admin.register(Club) @admin.register(Club)
@@ -67,3 +67,18 @@ class MembershipAdmin(admin.ModelAdmin):
"club__name", "club__name",
) )
autocomplete_fields = ("user",) 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"), queryset=Membership.objects.ongoing().select_related("user", "role"),
) )
return self.get_object_or_exception( 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.functional import cached_property
from django.utils.translation import gettext_lazy as _ 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.models import User
from core.views.forms import SelectDateTime from core.views.forms import SelectDateTime
from core.views.widgets.ajax_select import ( from core.views.widgets.ajax_select import (
@@ -39,6 +46,26 @@ from counter.models import Counter, Selling
from counter.schemas import SaleFilterSchema 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): class ClubEditForm(forms.ModelForm):
error_css_class = "error" error_css_class = "error"
required_css_class = "required" required_css_class = "required"
@@ -48,6 +75,20 @@ class ClubEditForm(forms.ModelForm):
fields = ["address", "logo", "short_description"] fields = ["address", "logo", "short_description"]
widgets = {"short_description": forms.Textarea()} 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): class ClubAdminEditForm(ClubEditForm):
admin_fields = ["name", "parent", "is_active"] admin_fields = ["name", "parent", "is_active"]
@@ -33,7 +33,7 @@ def migrate_roles(apps: StateApps, schema_editor):
club_id=club_id, club_id=club_id,
order=max(SITH_CLUB_ROLES) - role, 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 # all updates must happen at the same time
# otherwise, the 10 first created ClubRole would be # otherwise, the 10 first created ClubRole would be
# re-modified after their initial creation, and it would # 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): def fetch_format(self):
return self.get_email + " " 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 django.db.models import Q
from ninja import FilterLookup, FilterSchema, ModelSchema from ninja import FilterLookup, FilterSchema, ModelSchema
from pydantic import HttpUrl
from club.models import Club, ClubRole, Membership from club.models import Club, ClubRole, Membership
from core.schemas import NonEmptyStr, SimpleUserSchema from core.schemas import NonEmptyStr, SimpleUserSchema
@@ -62,6 +63,11 @@ class ClubSchema(ModelSchema):
fields = ["id", "name", "logo", "is_active", "short_description", "address"] fields = ["id", "name", "logo", "is_active", "short_description", "address"]
members: list[ClubMemberSchema] members: list[ClubMemberSchema]
links: list[HttpUrl]
@staticmethod
def resolve_links(obj: Club):
return [link.url for link in obj.links.all()]
class UserMembershipSchema(ModelSchema): 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;
}
}
}
}
}
+36 -8
View File
@@ -21,15 +21,43 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("club/detail.scss") }}">
{% endblock %}
{% block content %} {% block content %}
<div id="club_detail"> <h3>{{ club.name }}</h3>
{% if club.logo %} <div id="club-detail" {% if links %}class="has-links"{% endif %}>
<div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.name }}"></div> <div id="club-attributes">
{% endif %} {% if club.logo %}
<h3>{{ club.name }}</h3> <img
{% if page_revision %} class="club-logo"
{{ page_revision|markdown }} src="{{ club.logo.url }}"
{% endif %} 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> </div>
{% endblock %} {% endblock %}
+12
View File
@@ -69,6 +69,18 @@
{{ club.name }} {% if not club.is_active %}({% trans %}inactive{% endtrans %}){% endif %} {{ club.name }} {% if not club.is_active %}({% trans %}inactive{% endtrans %}){% endif %}
</h4> </h4>
</a> </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 }} {{ club.short_description|markdown }}
</div> </div>
</div> </div>
+100 -6
View File
@@ -1,9 +1,63 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block additional_js %}
<script type="module" src="{{ static("bundled/core/dynamic-formset-index.ts") }}"></script>
{% endblock %}
{% block title %} {% block title %}
{% trans name=object %}Edit {{ name }}{% endtrans %} {% trans name=object %}Edit {{ name }}{% endtrans %}
{% endblock %} {% 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 %} {% block content %}
<h2>{% trans name=object %}Edit {{ name }}{% endtrans %}</h2> <h2>{% trans name=object %}Edit {{ name }}{% endtrans %}</h2>
@@ -17,7 +71,7 @@
and explicitly separate them from the non-admin ones, and explicitly separate them from the non-admin ones,
with some help text. with some help text.
Non-admin users will only see the regular form fields, 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> <h3>{% trans %}Club properties{% endtrans %}</h3>
<p class="helptext"> <p class="helptext">
{% trans trimmed %} {% trans trimmed %}
@@ -25,7 +79,7 @@
Only admin users can see and edit them. Only admin users can see and edit them.
{% endtrans %} {% endtrans %}
</p> </p>
<fieldset class="required margin-bottom"> <fieldset class="margin-bottom">
{% for field_name in form.admin_fields %} {% for field_name in form.admin_fields %}
{% set field = form[field_name] %} {% set field = form[field_name] %}
<div class="form-group"> <div class="form-group">
@@ -36,11 +90,13 @@
{# Remove the the admin fields from the form. {# Remove the the admin fields from the form.
The remaining non-admin fields will be rendered The remaining non-admin fields will be rendered
at once with a simple {{ form.as_p() }} #} at once with a simple {{ form.as_p() }} #}
{% set _ = form.fields.pop(field_name) %} {% do form.fields.pop(field_name) %}
{% endfor %} {% endfor %}
</fieldset> </fieldset>
{% endif %}
<h3>{% trans %}Club informations{% endtrans %}</h3> <h3>{% trans %}Club informations{% endtrans %}</h3>
{% if form.admin_fields %}
<p class="helptext"> <p class="helptext">
{% trans trimmed %} {% trans trimmed %}
The following form fields are linked to the basic description of a club. The following form fields are linked to the basic description of a club.
@@ -48,7 +104,45 @@
{% endtrans %} {% endtrans %}
</p> </p>
{% endif %} {% endif %}
{{ form.as_p() }} <fieldset class="margin-bottom">
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p> {{ form.as_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> </form>
{% endblock content %} {% 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): def test_fetch_club_nb_queries(self, client: Client, club: Club):
user = subscriber_user.make() user = subscriber_user.make()
client.force_login(user) client.force_login(user)
with assertNumQueries(6): with assertNumQueries(7):
# - 4 queries for authentication # - 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})) res = client.get(reverse("api:fetch_club", kwargs={"club_id": club.id}))
assert res.status_code == 200 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) client.force_login(user)
res = client.post( res = client.post(
reverse("club:club_edit", kwargs={"club_id": club.id}), 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, # The request should success,
# but admin-only fields shouldn't be taken into account # 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 assert res.status_code == 200
soup = BeautifulSoup(res.text, "lxml") 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)) 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 assert res.status_code == 200
soup = BeautifulSoup(res.text, "lxml") 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") == [] 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.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
from django.core.paginator import InvalidPage, Paginator 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.http import Http404, StreamingHttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy 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.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, View from django.views.generic import DetailView, ListView, View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import ( from django.views.generic.edit import CreateView, DeleteView, FormMixin, UpdateView
CreateView,
DeleteView,
FormMixin,
UpdateView,
)
from club.forms import ( from club.forms import (
ClubAddMemberForm, ClubAddMemberForm,
@@ -66,7 +62,15 @@ from club.forms import (
MailingForm, MailingForm,
SellingsForm, 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.models import Poster
from com.views import ( from com.views import (
PosterCreateBaseView, PosterCreateBaseView,
@@ -210,7 +214,9 @@ class ClubListView(AllowFragment, FormMixin, ListView):
template_name = "club/club_list.jinja" template_name = "club/club_list.jinja"
form_class = ClubSearchForm 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 paginate_by = 20
def get_form_kwargs(self): def get_form_kwargs(self):
@@ -249,6 +255,7 @@ class ClubView(ClubTabsMixin, DetailView):
.values_list("content", flat=True) .values_list("content", flat=True)
.first() .first()
) )
kwargs["links"] = list(self.object.links.select_related("link_type").all())
return kwargs return kwargs
@@ -689,6 +696,11 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
return ClubAdminEditForm return ClubAdminEditForm
return ClubEditForm 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): class ClubCreateView(PermissionRequiredMixin, CreateView):
"""Create a club (for the Sith admin).""" """Create a club (for the Sith admin)."""
@@ -1,4 +1,3 @@
import { exportToHtml } from "#core:utils/globals.ts";
import { newsDeleteNews, newsFetchNewsDates, newsPublishNews } from "#openapi"; import { newsDeleteNews, newsFetchNewsDates, newsPublishNews } from "#openapi";
// This will be used in jinja templates, // This will be used in jinja templates,
@@ -13,7 +12,8 @@ const AlertState = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum // biome-ignore lint/style/useNamingConvention: this feels more like an enum
DISPLAYED: 4, // When published at page generation 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", () => { document.addEventListener("alpine:init", () => {
Alpine.data("moderationAlert", (newsId: number) => ({ Alpine.data("moderationAlert", (newsId: number) => ({
+10 -18
View File
@@ -16,9 +16,13 @@
#right_column { #right_column {
flex: 20%; flex: 20%;
margin: 3.2px; margin: 3.2px;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
@media screen and (min-width: 800px) {
max-width: 20%;
min-width: 200px;
}
} }
#left_column { #left_column {
@@ -46,7 +50,7 @@
} }
} }
@media screen and (max-width: $small-devices) { @media screen and (max-width: 800px) {
#left_column, #left_column,
#right_column { #right_column {
flex: 100%; flex: 100%;
@@ -76,8 +80,8 @@
display: block; display: block;
width: 100%; width: 100%;
background: white; background: white;
font-size: 70%;
margin-bottom: 1em; margin-bottom: 1em;
font-size: 85%;
#links_content { #links_content {
overflow: auto; overflow: auto;
@@ -96,23 +100,10 @@
li { li {
margin: 10px; 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 { i {
width: 25px; width: 25px;
text-align: center; text-align: center;
margin-right: .5rem;
} }
} }
} }
@@ -122,12 +113,13 @@
#birthdays_content { #birthdays_content {
box-shadow: $shadow-color 1px 1px 1px; box-shadow: $shadow-color 1px 1px 1px;
padding: 1em; padding: 1em;
ul.birthdays_year { ul.birthdays_year {
margin: 0; margin: 0;
list-style-type: none; list-style-type: none;
font-weight: bold; font-weight: bold;
>li { > li {
padding: 0.5em; padding: 0.5em;
&:nth-child(even) { &:nth-child(even) {
+1 -1
View File
@@ -4,7 +4,7 @@
<title>{% trans %}Slideshow{% endtrans %}</title> <title>{% trans %}Slideshow{% endtrans %}</title>
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}"> <link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" /> <link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
<script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script> <script type="module" src="{{ static('bundled/base-bundle-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script> <script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script>
</head> </head>
<body x-data="slideshow([ <body x-data="slideshow([
+49 -1
View File
@@ -36,7 +36,7 @@ from django.utils import timezone
from django.utils.timezone import localdate from django.utils.timezone import localdate
from PIL import Image 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.ics_calendar import IcsCalendar
from com.models import News, NewsDate, Sith, Weekmail from com.models import News, NewsDate, Sith, Weekmail
from core.models import BanGroup, Group, Page, PageRev, SithFile, User 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)) roles.append(ClubRole(club=club, order=i, name=role))
ClubRole.objects.bulk_create(roles) 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) return PopulatedClubs(ae=ae, troll=troll, pdf=pdf, refound=refound)
def _create_groups(self) -> PopulatedGroups: def _create_groups(self) -> PopulatedGroups:
-21
View File
@@ -1,21 +0,0 @@
import sort from "@alpinejs/sort";
import { Alpine } from "alpinejs";
import { limitedChoices } from "#core:alpine/limited-choices";
import {
type NotificationPlugin,
alpinePlugin as notificationPlugin,
} from "#core:utils/notifications";
declare module "alpinejs" {
interface Magics<T> {
$notifications: NotificationPlugin;
}
}
Alpine.plugin([sort, limitedChoices]);
Alpine.magic("notifications", notificationPlugin);
// biome-ignore lint/style/useNamingConvention: it's how it's named
Object.assign(window, { 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"; function showMenu() {
exportToHtml("showMenu", () => {
const navbar = document.getElementById("navbar-content"); const navbar = document.getElementById("navbar-content");
const current = navbar.getAttribute("mobile-display"); const current = navbar.getAttribute("mobile-display");
navbar.setAttribute("mobile-display", current === "hidden" ? "revealed" : "hidden"); navbar.setAttribute("mobile-display", current === "hidden" ? "revealed" : "hidden");
}); }
document.addEventListener("alpine:init", () => { function navbarInit() {
const menuItems = document.querySelectorAll(".navbar details[name='navbar'].menu"); const menuItems = document.querySelectorAll(".navbar details[name='navbar'].menu");
const isDesktop = () => { const isDesktop = () => {
return window.innerWidth >= 500; 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 // biome-ignore lint/performance/noNamespaceImport: this is the recommended way from the documentation
import * as Sentry from "@sentry/browser"; import * as Sentry from "@sentry/browser";
import { exportToHtml } from "#core:utils/globals.ts";
interface LoggedUser { interface LoggedUser {
name: string; name: string;
@@ -13,7 +12,7 @@ interface SentryOptions {
user?: LoggedUser; user?: LoggedUser;
} }
exportToHtml("loadSentryPopup", (options: SentryOptions) => { const loadSentryPopup = (options: SentryOptions) => {
Sentry.init({ Sentry.init({
dsn: options.dsn, dsn: options.dsn,
}); });
@@ -21,4 +20,5 @@ exportToHtml("loadSentryPopup", (options: SentryOptions) => {
eventId: options.eventId, eventId: options.eventId,
...(options.user ?? {}), ...(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> { interface StringifyOptions<T extends object> {
/** The columns to include in the resulting CSV. */ /** The columns to include in the resulting CSV. */
-14
View File
@@ -5,17 +5,3 @@ declare global {
const gettext: (text: string) => string; const gettext: (text: string) => string;
const interpolate: <T>(fmt: string, args: string[] | T, isNamed?: boolean) => 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;
}
+33 -20
View File
@@ -7,11 +7,30 @@ export enum NotificationLevel {
} }
export interface NotificationPlugin { export interface NotificationPlugin {
/**
* Add an error message to the notifications.
*/
error: (message: string) => void; error: (message: string) => void;
/**
* Add a warning message to the notifications
*/
warning: (message: string) => void; warning: (message: string) => void;
/**
* Add a success message to the notifications
*/
success: (message: string) => void; success: (message: string) => void;
/**
* Remove all notifications displayed on the page.
*/
clear: () => void; clear: () => void;
/**
* Add multiple notifications at once.
* The added notifications can have different notification levels.
*/
addMany: (notifs: Notification[]) => void; addMany: (notifs: Notification[]) => void;
/**
* Return all notifications displayed on the page.
*/
getAll: () => Notification[]; getAll: () => Notification[];
} }
@@ -25,27 +44,21 @@ Alpine.store("notifications", [] as Notification[]);
function createNotification(message: string, level: NotificationLevel) { function createNotification(message: string, level: NotificationLevel) {
(Alpine.store("notifications") as Notification[]).push({ text: message, tag: level }); (Alpine.store("notifications") as Notification[]).push({ text: message, tag: level });
} }
function createManyNotifications(notifs: Notification[]) {
function deleteNotifications() { for (const notif of notifs) {
Alpine.store("notifications", []); createNotification(notif.text, notif.tag);
}
} }
function getNotifications() { export const notifications: NotificationPlugin = {
return Alpine.store("notifications") as Notification[]; error: (message: string) => createNotification(message, NotificationLevel.Error),
} 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 alpinePlugin(): NotificationPlugin { export function notificationsPlugin(GlobalAlpine: Alpine) {
return { GlobalAlpine.magic("notifications", () => ({ ...notifications }));
error: (message: string) => createNotification(message, NotificationLevel.Error),
warning: (message: string) =>
createNotification(message, NotificationLevel.Warning),
success: (message: string) =>
createNotification(message, NotificationLevel.Success),
clear: () => deleteNotifications(),
addMany: (notifs: Notification[]) =>
notifs.forEach((n) => {
createNotification(n.text, n.tag);
}),
getAll: () => getNotifications(),
};
} }
+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) { @media screen and (max-width: $small-devices) {
@@ -749,16 +771,3 @@ textarea {
vertical-align: middle; 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> <noscript><link rel="stylesheet" href="{{ static('bundled/fontawesome-index.css') }}"></noscript>
<script src="{{ url('javascript-catalog') }}"></script> <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/core/components/include-index.ts") }}"></script>
<script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script> <script type="module" src="{{ static("bundled/core/tooltips-index.ts") }}"></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>
{% block additional_css %}{% endblock %} {% block additional_css %}{% endblock %}
{% block additional_js %}{% endblock %} {% block additional_js %}{% endblock %}
+2 -3
View File
@@ -1,14 +1,13 @@
<div id="quick-notifications" <div id="quick-notifications"
x-data='{ messages: $notifications.getAll() }'
x-init='$notifications.addMany([ x-init='$notifications.addMany([
{%- for message in messages -%} {%- for message in messages -%}
{%- if not message.extra_tags -%} {%- if not message.extra_tags -%}
{ tag: {{ message.tags|string|tojson }}, text: {{ message|string|tojson }} }, { tag: "{{ message.tags }}", text: {{ message|string|tojson }} },
{%- endif -%} {%- endif -%}
{%- endfor -%} {%- endfor -%}
])' ])'
> >
<template x-for="(message, index) in messages"> <template x-for="(message, index) in $notifications.getAll()">
<div class="alert" :class="`alert-${message.tag}`" x-transition> <div class="alert" :class="`alert-${message.tag}`" x-transition>
<span class="alert-main" x-text="message.text"></span> <span class="alert-main" x-text="message.text"></span>
<span class="clickable" @click="messages = messages.filter((item, i) => i !== index)"> <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> <button type="button" onclick="checkbox_{{form_id}}(false);">{% trans %}Unselect All{% endtrans %}</button>
{% endmacro %} {% endmacro %}
{% macro update_notifications(messages, clear) %} {% macro update_notifications(messages, clear = True) %}
{# Update notification area from new messages sent by django backend {# Update notification area from new messages sent by django backend
This is useful when performing fragment swaps to keep messages up to date 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 Without this, the fragment would need to take control of the notification area and
@@ -236,16 +236,19 @@
messages: messages from django.contrib messages: messages from django.contrib
clear : optional boolean that controls if notifications should be cleared first. True is the default clear : optional boolean that controls if notifications should be cleared first. True is the default
#} #}
{% set clear = clear|default(true) %}
{% if messages %} {% if messages %}
<div x-init="() => { <div x-init='() => {
{% if clear %} {%- if clear -%}
$notifications.clear() $notifications.clear();
{% endif %} {%- endif -%}
{% for message in messages %} $notifications.addMany([
$notifications.{{ message.tags }}('{{ message }}') {%- for message in messages -%}
{% endfor %} {%- if not message.extra_tags -%}
}"></div> { tag: "{{ message.tags }}", text: {{ message|string|tojson }} },
{%- endif -%}
{%- endfor -%}
])
}'></div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
+1 -1
View File
@@ -103,7 +103,7 @@ def add_attr(field: BoundField, attr: str):
if "=" not in d: if "=" not in d:
attrs["class"] = d attrs["class"] = d
else: else:
key, val = d.split("=") key, val = d.split("=", maxsplit=1)
attrs[key] = val attrs[key] = val
return field.as_widget(attrs=attrs) return field.as_widget(attrs=attrs)
+1 -1
View File
@@ -54,7 +54,7 @@ class FragmentRenderer(Protocol):
) -> SafeString: ... ) -> SafeString: ...
class FragmentMixin(TemplateResponseMixin, ContextMixin): class FragmentMixin(TemplateResponseMixin, AllowFragment, ContextMixin):
"""Make a view buildable as a fragment that can be embedded in a template. """Make a view buildable as a fragment that can be embedded in a template.
Most fragments are used in two different ways : Most fragments are used in two different ways :
+2 -1
View File
@@ -32,8 +32,9 @@ class Migration(migrations.Migration):
( (
"result", "result",
models.OneToOneField( models.OneToOneField(
help_text="The formula product.", help_text="The product got with the formula.",
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
related_name="formula",
to="counter.product", to="counter.product",
verbose_name="result product", verbose_name="result product",
), ),
@@ -1,9 +1,9 @@
import { showSaveFilePicker } from "native-file-system-adapter"; import { showSaveFilePicker } from "native-file-system-adapter";
import type TomSelect from "tom-select"; import type TomSelect from "tom-select";
import type { NestedKeyOf } from "#core:types/nested-key";
import { paginated } from "#core:utils/api"; import { paginated } from "#core:utils/api";
import { csv } from "#core:utils/csv"; import { csv } from "#core:utils/csv";
import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history"; import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history";
import type { NestedKeyOf } from "#core:utils/types";
import { import {
type ProductSchema, type ProductSchema,
type ProductSearchProductsDetailedData, type ProductSearchProductsDetailedData,
+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.
+1 -10
View File
@@ -1,6 +1,3 @@
from typing import Any
from ninja import Status
from ninja_extra import ControllerBase, api_controller, route from ninja_extra import ControllerBase, api_controller, route
from ninja_extra.exceptions import NotFound from ninja_extra.exceptions import NotFound
@@ -11,19 +8,13 @@ from eboutic.models import Basket
@api_controller("/etransaction", permissions=[CanView]) @api_controller("/etransaction", permissions=[CanView])
class EtransactionInfoController(ControllerBase): class EtransactionInfoController(ControllerBase):
@route.get( @route.get("/data/{basket_id}", url_name="etransaction_data")
"/data/{basket_id}",
url_name="etransaction_data",
response={200: dict[str, Any], 410: str},
)
def fetch_etransaction_data(self, basket_id: int): def fetch_etransaction_data(self, basket_id: int):
"""Generate the data to pay an eboutic command with paybox. """Generate the data to pay an eboutic command with paybox.
The data is generated with the basket that is used by the current session. The data is generated with the basket that is used by the current session.
""" """
basket: Basket = self.get_object_or_exception(Basket, pk=basket_id) basket: Basket = self.get_object_or_exception(Basket, pk=basket_id)
if basket.is_expired:
return Status(410, "This basket is expired.")
try: try:
return dict(basket.get_e_transaction_data()) return dict(basket.get_e_transaction_data())
except BillingInfo.DoesNotExist as e: except BillingInfo.DoesNotExist as e:
-20
View File
@@ -24,7 +24,6 @@ from django.conf import settings
from django.db import DataError, models from django.db import DataError, models
from django.db.models import F, OuterRef, Subquery, Sum from django.db.models import F, OuterRef, Subquery, Sum
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import User from core.models import User
@@ -96,10 +95,6 @@ class Basket(models.Model):
] ]
) )
@property
def is_expired(self) -> bool:
return (self.date + settings.SITH_EBOUTIC_BASKET_TIMEOUT) <= now()
def generate_sales( def generate_sales(
self, counter, seller: User, payment_method: Selling.PaymentMethod self, counter, seller: User, payment_method: Selling.PaymentMethod
): ):
@@ -138,20 +133,9 @@ class Basket(models.Model):
] ]
def get_e_transaction_data(self) -> list[tuple[str, str]]: def get_e_transaction_data(self) -> list[tuple[str, str]]:
"""Get data for etransaction payment.
Raises:
Customer.DoesNotExist: if the user linked to this basket
has no customer account
BillingInfo.DoesNotExist: if the user linked to this basket has no
billing infos, or incorrect billing infos.
ValueError: if this is called on a basket which payment delay is expired.
"""
user = self.user user = self.user
if not hasattr(user, "customer"): if not hasattr(user, "customer"):
raise Customer.DoesNotExist raise Customer.DoesNotExist
if self.is_expired:
raise ValueError("This method cannot be called on an expired basket.")
customer = user.customer customer = user.customer
if ( if (
not hasattr(user.customer, "billing_infos") not hasattr(user.customer, "billing_infos")
@@ -171,10 +155,6 @@ class Basket(models.Model):
("PBX_IDENTIFIANT", settings.SITH_EBOUTIC_PBX_IDENTIFIANT), ("PBX_IDENTIFIANT", settings.SITH_EBOUTIC_PBX_IDENTIFIANT),
("PBX_TOTAL", str(int(self.total * 100))), ("PBX_TOTAL", str(int(self.total * 100))),
("PBX_DEVISE", "978"), # This is Euro ("PBX_DEVISE", "978"), # This is Euro
(
"PBX_DISPLAY",
str(int(settings.SITH_EBOUTIC_ETRANSACTION_TIMEOUT.total_seconds())),
),
("PBX_CMD", str(self.id)), ("PBX_CMD", str(self.id)),
("PBX_PORTEUR", user.email), ("PBX_PORTEUR", user.email),
("PBX_RETOUR", "Amount:M;BasketID:R;Auto:A;Error:E;Sig:K"), ("PBX_RETOUR", "Amount:M;BasketID:R;Auto:A;Error:E;Sig:K"),
@@ -1,71 +1,21 @@
import { type Notification, NotificationLevel } from "#core:utils/notifications";
import { etransactioninfoFetchEtransactionData } from "#openapi"; import { etransactioninfoFetchEtransactionData } from "#openapi";
interface Basket {
id: number;
timeout: Date;
}
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("etransaction", (initialData, basket: Basket) => ({ Alpine.data("etransaction", (initialData, basketId: number) => ({
data: initialData, data: initialData,
isCbAvailable: Object.keys(initialData).length > 0, isCbAvailable: Object.keys(initialData).length > 0,
isSithAvailable: true,
init() {
const now = new Date();
const timeout = basket.timeout.getTime() - now.getTime();
if (timeout <= 0) {
// basket was already outdated at initial page load
this.timeoutBasket();
} else {
setTimeout(() => this.timeoutBasket(), timeout);
}
},
/**
* Make this basket into a timeout state.
* All submission inputs are disabled, and an error message is displayed.
*/
timeoutBasket() {
this.isCbAvailable = false;
this.isSithAvailable = false;
const message = gettext("Basket expired");
const existingNotif: Notification | undefined = this.$notifications
.getAll()
.find(
(n: Notification) =>
n.tag === NotificationLevel.Error && n.message === message,
);
if (existingNotif === undefined) {
this.$notifications.error(message);
}
},
/**
* Refresh the data used for etransaction.
*
* Note: if this is called while the basket is expired, it will be a no-op
*/
async fill() { async fill() {
if (new Date() > basket.timeout) {
// refresh etransaction data only if the basket is still valid.
this.timeoutBasket();
return;
}
this.isCbAvailable = false; this.isCbAvailable = false;
const res = await etransactioninfoFetchEtransactionData({ const res = await etransactioninfoFetchEtransactionData({
// biome-ignore lint/style/useNamingConvention: api is in snake_case path: {
path: { basket_id: basket.id }, // biome-ignore lint/style/useNamingConvention: api is in snake_case
basket_id: basketId,
},
}); });
if (res.response.ok) { if (res.response.ok) {
this.data = res.data; this.data = res.data;
this.isCbAvailable = true; this.isCbAvailable = true;
} else if (res.response.status === 410) {
// The basket is expired, so no payment method should be available at all.
// This shouldn't happen, because we don't send the request
// when the timeout is passed, but we are better safe than sorry
this.timeoutBasket();
} }
}, },
})); }));
+10 -12
View File
@@ -1,4 +1,4 @@
export {}; import { versionedLocalStorage } from "#core:core/localstorage";
interface BasketItem { interface BasketItem {
priceId: number; priceId: number;
@@ -7,8 +7,8 @@ interface BasketItem {
unitPrice: number; unitPrice: number;
} }
// increment the key number if the data schema of the cached basket changes const BASKET_CACHE_KEY = "basket";
const BASKET_CACHE_KEY = "basket1"; const BASKET_CACHE_VERSION = 1;
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("basket", (lastPurchaseTime?: number) => ({ Alpine.data("basket", (lastPurchaseTime?: number) => ({
@@ -34,18 +34,16 @@ document.addEventListener("alpine:init", () => {
}, },
loadBasket(): BasketItem[] { loadBasket(): BasketItem[] {
if (localStorage.getItem(BASKET_CACHE_KEY) === null) { const cached = versionedLocalStorage.getItem<BasketItem[]>(BASKET_CACHE_KEY, {
return []; version: BASKET_CACHE_VERSION,
} });
try { return cached ?? [];
return JSON.parse(localStorage.getItem(BASKET_CACHE_KEY));
} catch (_err) {
return [];
}
}, },
saveBasket() { 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()); localStorage.setItem("basketTimestamp", Date.now().toString());
}, },
@@ -21,7 +21,6 @@
hx-swap="outerHTML" hx-swap="outerHTML"
hx-target="#billing-infos-fragment" hx-target="#billing-infos-fragment"
x-show="collapsed" x-show="collapsed"
x-cloak
> >
{% csrf_token %} {% csrf_token %}
{{ form.as_p() }} {{ form.as_p() }}
@@ -33,5 +32,7 @@
</form> </form>
</div> </div>
<br> <br>
{{ update_notifications(messages) }} {% if is_fragment %}
{{ update_notifications(messages) }}
{% endif %}
</div> </div>
@@ -15,10 +15,11 @@
{% block content %} {% block content %}
<h3>{% trans %}Eboutic{% endtrans %}</h3> <h3>{% trans %}Eboutic{% endtrans %}</h3>
<div x-data='etransaction( <script type="text/javascript">
{{ billing_infos|tojson }}, let billingInfos = {{ billing_infos|safe }};
{ id: {{ basket.id }}, timeout: new Date('{{ basket.date + settings.SITH_EBOUTIC_BASKET_TIMEOUT }}') } </script>
)'>
<div x-data="etransaction(billingInfos, {{ basket.id }})">
<p>{% trans %}Basket: {% endtrans %}</p> <p>{% trans %}Basket: {% endtrans %}</p>
<table> <table>
<thead> <thead>
@@ -71,11 +72,7 @@
x-cloak x-cloak
type="submit" type="submit"
id="bank-submit-button" id="bank-submit-button"
{% if basket.is_expired %} :disabled="!isCbAvailable"
disabled="disabled"
{% else %}
:disabled="!isCbAvailable"
{% endif %}
class="btn btn-blue" class="btn btn-blue"
value="{% trans %}Pay with credit card{% endtrans %}" value="{% trans %}Pay with credit card{% endtrans %}"
/> />
@@ -96,16 +93,7 @@
{% else %} {% else %}
<form method="post" action="{{ url('eboutic:pay_with_sith', basket_id=basket.id) }}" name="sith-pay-form"> <form method="post" action="{{ url('eboutic:pay_with_sith', basket_id=basket.id) }}" name="sith-pay-form">
{% csrf_token %} {% csrf_token %}
<input <input class="btn btn-blue" type="submit" value="{% trans %}Pay with Sith account{% endtrans %}"/>
{% if basket.is_expired %}
disabled="disabled"
{% else %}
:disabled="!isSithAvailable"
{% endif %}
class="btn btn-blue"
type="submit"
value="{% trans %}Pay with Sith account{% endtrans %}"
/>
</form> </form>
{% endif %} {% endif %}
</div> </div>
+6 -22
View File
@@ -3,7 +3,6 @@ import urllib
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import freezegun
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.hashes import SHA1 from cryptography.hazmat.primitives.hashes import SHA1
@@ -106,7 +105,7 @@ class TestPaymentSith(TestPaymentBase):
), ),
reverse("eboutic:payment_result", kwargs={"result": "success"}), reverse("eboutic:payment_result", kwargs={"result": "success"}),
) )
assert not Basket.objects.filter(id=self.basket.id).exists() assert Basket.objects.filter(id=self.basket.id).first() is None
self.customer.customer.refresh_from_db() self.customer.customer.refresh_from_db()
assert self.customer.customer.amount == Decimal(1) assert self.customer.customer.amount == Decimal(1)
@@ -140,7 +139,10 @@ class TestPaymentSith(TestPaymentBase):
assert len(messages) == 1 assert len(messages) == 1
assert messages[0].level == DEFAULT_LEVELS["ERROR"] assert messages[0].level == DEFAULT_LEVELS["ERROR"]
assert messages[0].message == "Solde insuffisant" assert messages[0].message == "Solde insuffisant"
assert not Basket.objects.filter(id=self.basket.id).exists()
assert Basket.objects.contains(self.basket), (
"After an unsuccessful request, the basket should be kept"
)
def test_refilling_in_basket(self): def test_refilling_in_basket(self):
BasketItem.from_price(self.refilling.prices.first(), 1, self.basket).save() BasketItem.from_price(self.refilling.prices.first(), 1, self.basket).save()
@@ -155,7 +157,7 @@ class TestPaymentSith(TestPaymentBase):
response, response,
reverse("eboutic:payment_result", kwargs={"result": "failure"}), reverse("eboutic:payment_result", kwargs={"result": "failure"}),
) )
assert not Basket.objects.filter(id=self.basket.id).exists() assert Basket.objects.filter(id=self.basket.id).first() is not None
messages = list(get_messages(response.wsgi_request)) messages = list(get_messages(response.wsgi_request))
assert messages[0].level == DEFAULT_LEVELS["ERROR"] assert messages[0].level == DEFAULT_LEVELS["ERROR"]
assert ( assert (
@@ -165,24 +167,6 @@ class TestPaymentSith(TestPaymentBase):
self.customer.customer.refresh_from_db() self.customer.customer.refresh_from_db()
assert self.customer.customer.amount == initial_account_balance assert self.customer.customer.amount == initial_account_balance
def test_basket_expired(self):
self.client.force_login(self.customer)
initial_account_balance = self.customer.customer.amount
with freezegun.freeze_time(settings.SITH_EBOUTIC_BASKET_TIMEOUT):
response = self.client.post(
reverse("eboutic:pay_with_sith", kwargs={"basket_id": self.basket.id})
)
assertRedirects(
response,
reverse("eboutic:payment_result", kwargs={"result": "failure"}),
)
messages = list(get_messages(response.wsgi_request))
assert messages[0].level == DEFAULT_LEVELS["ERROR"]
assert messages[0].message == "Panier expiré"
assert not Basket.objects.filter(id=self.basket.id).exists()
self.customer.customer.refresh_from_db()
assert self.customer.customer.amount == initial_account_balance
class TestPaymentCard(TestPaymentBase): class TestPaymentCard(TestPaymentBase):
def generate_bank_valid_answer(self, basket: Basket): def generate_bank_valid_answer(self, basket: Basket):
+6 -21
View File
@@ -39,8 +39,6 @@ from django.db.utils import cached_property
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.formats import localize
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from django.views.generic import DetailView, FormView, TemplateView, UpdateView, View from django.views.generic import DetailView, FormView, TemplateView, UpdateView, View
@@ -189,7 +187,9 @@ class BillingInfoFormFragment(
def get_initial(self): def get_initial(self):
if self.object is None: if self.object is None:
return {"country": Country(code="FR")} return {
"country": Country(code="FR"),
}
return {} return {}
def render_fragment(self, request, **kwargs) -> SafeString: def render_fragment(self, request, **kwargs) -> SafeString:
@@ -255,19 +255,10 @@ class EbouticCheckout(CanViewMixin, UseFragmentsMixin, DetailView):
kwargs["customer_amount"] = None kwargs["customer_amount"] = None
kwargs["billing_infos"] = {} kwargs["billing_infos"] = {}
if self.object.is_expired: with contextlib.suppress(BillingInfo.DoesNotExist):
messages.error(self.request, _("Basket expired")) kwargs["billing_infos"] = json.dumps(
else: dict(self.object.get_e_transaction_data())
timeout = self.object.date + settings.SITH_EBOUTIC_BASKET_TIMEOUT
messages.warning(
self.request,
_("Basket available until %(until)s")
% {"until": localize(localtime(timeout).time())},
) )
with contextlib.suppress(BillingInfo.DoesNotExist):
kwargs["billing_infos"] = json.dumps(
dict(self.object.get_e_transaction_data())
)
return kwargs return kwargs
@@ -277,14 +268,9 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
basket = self.get_object() basket = self.get_object()
if basket.is_expired:
messages.error(self.request, _("Basket expired"))
basket.delete()
return redirect("eboutic:payment_result", "failure")
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
if basket.items.filter(product__product_type_id=refilling).exists(): if basket.items.filter(product__product_type_id=refilling).exists():
messages.error(self.request, _("You can't buy a refilling with sith money")) messages.error(self.request, _("You can't buy a refilling with sith money"))
basket.delete()
return redirect("eboutic:payment_result", "failure") return redirect("eboutic:payment_result", "failure")
eboutic = get_eboutic() eboutic = get_eboutic()
@@ -302,7 +288,6 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
except DatabaseError as e: except DatabaseError as e:
sentry_sdk.capture_exception(e) sentry_sdk.capture_exception(e)
except ValidationError as e: except ValidationError as e:
basket.delete()
messages.error(self.request, e.message) messages.error(self.request, e.message)
return redirect("eboutic:payment_result", "failure") return redirect("eboutic:payment_result", "failure")
+82 -29
View File
@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-15 11:46+0200\n" "POT-Creation-Date: 2026-05-12 11:12+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -362,6 +362,62 @@ msgstr "Cet email est déjà abonné à cette mailing"
msgid "Unregistered user" msgid "Unregistered user"
msgstr "Utilisateur non enregistré" 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 #: club/templates/club/club_list.jinja
msgid "The list of all clubs existing at UTBM." msgid "The list of all clubs existing at UTBM."
msgstr "La liste de tous les clubs existants à l'UTBM" msgstr "La liste de tous les clubs existants à l'UTBM"
@@ -708,6 +764,14 @@ msgstr "Comptoirs : "
msgid "Edit %(name)s" msgid "Edit %(name)s"
msgstr "Éditer %(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 #: club/templates/club/edit_club.jinja
msgid "Club properties" msgid "Club properties"
msgstr "Propriétés du club" msgstr "Propriétés du club"
@@ -732,6 +796,22 @@ msgstr ""
"Les champs de formulaire suivants sont liées à la description basique d'un " "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. 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 #: club/templates/club/fragments/add_member.jinja
msgid "Add a new member" msgid "Add a new member"
msgstr "Ajouter un nouveau membre" msgstr "Ajouter un nouveau membre"
@@ -1251,10 +1331,6 @@ msgstr ""
msgid "All coming events" msgid "All coming events"
msgstr "Tous les événements à venir" msgstr "Tous les événements à venir"
#: com/templates/com/news_list.jinja
msgid "Links"
msgstr "Liens"
#: com/templates/com/news_list.jinja #: com/templates/com/news_list.jinja
msgid "Our services" msgid "Our services"
msgstr "Nos services" msgstr "Nos services"
@@ -1760,10 +1836,6 @@ msgstr "Visiteur"
msgid "ban type" msgid "ban type"
msgstr "type de ban" msgstr "type de ban"
#: core/models.py counter/models.py
msgid "created at"
msgstr "créé le"
#: core/models.py #: core/models.py
msgid "expires at" msgid "expires at"
msgstr "expire le" msgstr "expire le"
@@ -1853,10 +1925,6 @@ msgstr "taille"
msgid "date" msgid "date"
msgstr "date" msgstr "date"
#: core/models.py counter/models.py
msgid "updated at"
msgstr "mis à jour le"
#: core/models.py #: core/models.py
msgid "asked for removal" msgid "asked for removal"
msgstr "retrait demandé" msgstr "retrait demandé"
@@ -3327,10 +3395,6 @@ msgstr "prix d'achat"
msgid "Initial cost of purchasing the product" msgid "Initial cost of purchasing the product"
msgstr "Coût initial d'achat du produit" msgstr "Coût initial d'achat du produit"
#: counter/models.py
msgid "icon"
msgstr "icône"
#: counter/models.py #: counter/models.py
msgid "limit age" msgid "limit age"
msgstr "âge limite" msgstr "âge limite"
@@ -4441,15 +4505,6 @@ msgstr ""
"souhaitez payer par carte, vous devez rajouter un numéro de téléphone aux " "souhaitez payer par carte, vous devez rajouter un numéro de téléphone aux "
"données que vous aviez déjà fourni." "données que vous aviez déjà fourni."
#: eboutic/views.py
msgid "Basket expired"
msgstr "Panier expiré"
#: eboutic/views.py
#, python-format
msgid "Basket available until %(until)s"
msgstr "Panier disponible jusqu'à %(until)s"
#: eboutic/views.py #: eboutic/views.py
msgid "You can't buy a refilling with sith money" msgid "You can't buy a refilling with sith money"
msgstr "Vous ne pouvez pas acheter un rechargement avec de l'argent du sith" msgstr "Vous ne pouvez pas acheter un rechargement avec de l'argent du sith"
@@ -5506,10 +5561,8 @@ msgid "Amicale/DOCEO member"
msgstr "Membre de l'Amicale/DOCEO" msgstr "Membre de l'Amicale/DOCEO"
#: sith/settings.py #: sith/settings.py
#, fuzzy
#| msgid "UT network member"
msgid "UT network member (excluding UTC)" msgid "UT network member (excluding UTC)"
msgstr "Cotisant du réseau UT" msgstr "Cotisant du réseau UT (hors UTC)"
#: sith/settings.py #: sith/settings.py
msgid "CROUS member" msgid "CROUS member"
+1 -5
View File
@@ -7,7 +7,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-17 10:03+0200\n" "POT-Creation-Date: 2026-04-17 22:42+0200\n"
"PO-Revision-Date: 2024-09-17 11:54+0200\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n"
"Last-Translator: Sli <antoine@bartuccio.fr>\n" "Last-Translator: Sli <antoine@bartuccio.fr>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -263,10 +263,6 @@ msgstr "Types de produits réordonnés !"
msgid "Product type reorganisation failed with status code : %d" msgid "Product type reorganisation failed with status code : %d"
msgstr "La réorganisation des types de produit a échoué avec le code : %d" msgstr "La réorganisation des types de produit a échoué avec le code : %d"
#: eboutic/static/bundled/eboutic/checkout-index.ts
msgid "Basket expired"
msgstr "Panier expiré"
#: sas/static/bundled/sas/pictures-download-index.ts #: sas/static/bundled/sas/pictures-download-index.ts
msgid "pictures.%(extension)s" msgid "pictures.%(extension)s"
msgstr "photos.%(extension)s" msgstr "photos.%(extension)s"
+2
View File
@@ -66,6 +66,8 @@ nav:
- Gestion des permissions: tutorial/perms.md - Gestion des permissions: tutorial/perms.md
- Gestion des groupes: tutorial/groups.md - Gestion des groupes: tutorial/groups.md
- Les fragments: tutorial/fragments.md - Les fragments: tutorial/fragments.md
- Frontend:
- localStorage: tutorial/front/localstorage.md
- API: - API:
- Développement: tutorial/api/dev.md - Développement: tutorial/api/dev.md
- Connexion à l'API: tutorial/api/connect.md - Connexion à l'API: tutorial/api/connect.md
+227 -198
View File
@@ -9,7 +9,7 @@
"version": "3", "version": "3",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"@alpinejs/sort": "^3.15.11", "@alpinejs/sort": "^3.15.12",
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0", "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
"@floating-ui/dom": "^1.7.6", "@floating-ui/dom": "^1.7.6",
"@fortawesome/fontawesome-free": "^7.2.0", "@fortawesome/fontawesome-free": "^7.2.0",
@@ -17,45 +17,46 @@
"@fullcalendar/daygrid": "^6.1.20", "@fullcalendar/daygrid": "^6.1.20",
"@fullcalendar/icalendar": "^6.1.20", "@fullcalendar/icalendar": "^6.1.20",
"@fullcalendar/list": "^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", "@zip.js/zip.js": "^2.8.26",
"3d-force-graph": "^1.80.0", "3d-force-graph": "^1.80.0",
"alpinejs": "^3.15.11", "alpinejs": "^3.15.12",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"country-flag-emoji-polyfill": "^0.1.8", "country-flag-emoji-polyfill": "^0.1.8",
"cytoscape": "^3.33.2", "cytoscape": "^3.33.4",
"cytoscape-cxtmenu": "^3.5.0", "cytoscape-cxtmenu": "^3.5.0",
"cytoscape-klay": "^3.1.4", "cytoscape-klay": "^3.1.4",
"d3-force-3d": "^3.0.6", "d3-force-3d": "^3.0.6",
"easymde": "^2.20.0", "easymde": "^2.21.0",
"glob": "^13.0.6", "glob": "^13.0.6",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"htmx.org": "^2.0.10", "htmx.org": "^2.0.10",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.7",
"lit-html": "^3.3.2", "lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1", "native-file-system-adapter": "^3.0.1",
"three": "^0.184.0", "three": "^0.184.0",
"three-spritetext": "^1.10.0", "three-spritetext": "^1.10.0",
"tom-select": "^2.6.0" "tom-select": "^2.6.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.29.0", "@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2", "@babel/preset-env": "^7.29.5",
"@biomejs/biome": "^2.4.13", "@biomejs/biome": "^2.4.15",
"@hey-api/openapi-ts": "^0.94.5", "@hey-api/openapi-ts": "^0.94.5",
"@types/alpinejs": "^3.13.11", "@types/alpinejs": "^3.13.11",
"@types/alpinejs__sort": "^3.13.0",
"@types/cytoscape-cxtmenu": "^3.4.5", "@types/cytoscape-cxtmenu": "^3.4.5",
"@types/cytoscape-klay": "^3.1.5", "@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.0.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.0.10" "vite": "^8.0.13"
} }
}, },
"node_modules/@alpinejs/sort": { "node_modules/@alpinejs/sort": {
"version": "3.15.11", "version": "3.15.12",
"resolved": "https://registry.npmjs.org/@alpinejs/sort/-/sort-3.15.11.tgz", "resolved": "https://registry.npmjs.org/@alpinejs/sort/-/sort-3.15.12.tgz",
"integrity": "sha512-HaDZ0jP7OYjRJ8Pv1aErOd3EaILq6pQWr+g3ZY9ddHTA37o1NoLfe20tu8AI6SOs3dppqxykXG99RTn/tsyShA==", "integrity": "sha512-DNIS7SQFg4H4o5faluRgqYEPi1Q7Hf+HMDgoCcIHXbXWH0g66WPEzz9OMk1zsUYib0KxMms4xXOqk+xh2tHZzg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"sortablejs": "^1.15.2" "sortablejs": "^1.15.2"
@@ -83,9 +84,9 @@
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.29.0", "version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -480,6 +481,23 @@
"@babel/core": "^7.0.0" "@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": { "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.27.1", "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", "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": { "node_modules/@babel/plugin-transform-modules-systemjs": {
"version": "7.29.0", "version": "7.29.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
"integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1434,19 +1452,20 @@
} }
}, },
"node_modules/@babel/preset-env": { "node_modules/@babel/preset-env": {
"version": "7.29.2", "version": "7.29.5",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz",
"integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.29.0", "@babel/compat-data": "^7.29.3",
"@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1", "@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@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-class-field-initializer-scope": "^7.27.1",
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^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-spread-parameters-in-optional-chaining": "^7.27.1",
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@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", "@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-member-expression-literals": "^7.27.1",
"@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6", "@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-modules-umd": "^7.27.1",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0",
"@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1",
@@ -1591,9 +1610,9 @@
} }
}, },
"node_modules/@biomejs/biome": { "node_modules/@biomejs/biome": {
"version": "2.4.13", "version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.13.tgz", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.15.tgz",
"integrity": "sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA==", "integrity": "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==",
"dev": true, "dev": true,
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"bin": { "bin": {
@@ -1607,20 +1626,20 @@
"url": "https://opencollective.com/biome" "url": "https://opencollective.com/biome"
}, },
"optionalDependencies": { "optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.4.13", "@biomejs/cli-darwin-arm64": "2.4.15",
"@biomejs/cli-darwin-x64": "2.4.13", "@biomejs/cli-darwin-x64": "2.4.15",
"@biomejs/cli-linux-arm64": "2.4.13", "@biomejs/cli-linux-arm64": "2.4.15",
"@biomejs/cli-linux-arm64-musl": "2.4.13", "@biomejs/cli-linux-arm64-musl": "2.4.15",
"@biomejs/cli-linux-x64": "2.4.13", "@biomejs/cli-linux-x64": "2.4.15",
"@biomejs/cli-linux-x64-musl": "2.4.13", "@biomejs/cli-linux-x64-musl": "2.4.15",
"@biomejs/cli-win32-arm64": "2.4.13", "@biomejs/cli-win32-arm64": "2.4.15",
"@biomejs/cli-win32-x64": "2.4.13" "@biomejs/cli-win32-x64": "2.4.15"
} }
}, },
"node_modules/@biomejs/cli-darwin-arm64": { "node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.4.13", "version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.13.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.15.tgz",
"integrity": "sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg==", "integrity": "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1635,9 +1654,9 @@
} }
}, },
"node_modules/@biomejs/cli-darwin-x64": { "node_modules/@biomejs/cli-darwin-x64": {
"version": "2.4.13", "version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.13.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.15.tgz",
"integrity": "sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg==", "integrity": "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1652,9 +1671,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-arm64": { "node_modules/@biomejs/cli-linux-arm64": {
"version": "2.4.13", "version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.13.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.15.tgz",
"integrity": "sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==", "integrity": "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1672,9 +1691,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-arm64-musl": { "node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.4.13", "version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.13.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.15.tgz",
"integrity": "sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg==", "integrity": "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1692,9 +1711,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-x64": { "node_modules/@biomejs/cli-linux-x64": {
"version": "2.4.13", "version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.13.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.15.tgz",
"integrity": "sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==", "integrity": "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1712,9 +1731,9 @@
} }
}, },
"node_modules/@biomejs/cli-linux-x64-musl": { "node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.4.13", "version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.13.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.15.tgz",
"integrity": "sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==", "integrity": "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1732,9 +1751,9 @@
} }
}, },
"node_modules/@biomejs/cli-win32-arm64": { "node_modules/@biomejs/cli-win32-arm64": {
"version": "2.4.13", "version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.13.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.15.tgz",
"integrity": "sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==", "integrity": "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1749,9 +1768,9 @@
} }
}, },
"node_modules/@biomejs/cli-win32-x64": { "node_modules/@biomejs/cli-win32-x64": {
"version": "2.4.13", "version": "2.4.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.13.tgz", "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.15.tgz",
"integrity": "sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ==", "integrity": "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2091,9 +2110,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.127.0", "version": "0.130.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -2101,9 +2120,9 @@
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2118,9 +2137,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2135,9 +2154,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2152,9 +2171,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2169,9 +2188,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2186,9 +2205,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2206,9 +2225,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2226,9 +2245,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -2246,9 +2265,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -2266,9 +2285,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2286,9 +2305,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2306,9 +2325,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2323,9 +2342,9 @@
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -2342,9 +2361,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2359,9 +2378,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2376,82 +2395,82 @@
} }
}, },
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sentry-internal/browser-utils": { "node_modules/@sentry-internal/browser-utils": {
"version": "10.51.0", "version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.51.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.53.1.tgz",
"integrity": "sha512-lNKBS4P7RUvf1niojXQWe9bU3gnBUCbST4Dj0pSiyat1N96cXVyHkeE+uGxowD0RrVWhs+kGHiVX3FcmRWF6sA==", "integrity": "sha512-X4d6y8sBMjmNhcDW4eMBU3ASsNIMz8dqaFkhyIMN/dkYr/yZKnbRZPaVuVUGvHKjnlficPpIH0/HK9KBjrYxPw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/core": "10.51.0" "@sentry/core": "10.53.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/feedback": { "node_modules/@sentry-internal/feedback": {
"version": "10.51.0", "version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.51.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.53.1.tgz",
"integrity": "sha512-bCM95bcpphx28e6aU0bwRLxOgwosYsdNzezM1sM0pVOkb0TB3hDFRamramVDK+/Hp1o8qmRxS4c5w/A7YBZGkA==", "integrity": "sha512-vVpTI/aEYN5d9IgZeYJWMqVaN0+iFgidSrYNAsZTh1US5sJUzF/wrl+68KdpmCtFROrN3jiAn1oPSwL5CKvEJA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/core": "10.51.0" "@sentry/core": "10.53.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/replay": { "node_modules/@sentry-internal/replay": {
"version": "10.51.0", "version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.51.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.53.1.tgz",
"integrity": "sha512-jCpI5HXSwK6ZT2HX70+mDRciAocHzSiDk4DTgvzV69Wvd+Ei5WLgE+d39eaEPsm8lUC0Ydntb5sJIB6uG9D4bw==", "integrity": "sha512-wZNzTBYkgGUPWMuUQv7L64+OJmoCnz7GQNiTrTFK6EVAjJXFBCSsPp/nhif0bLhbk8+0g4xz633uOhpXuQbFdw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/browser-utils": "10.51.0", "@sentry-internal/browser-utils": "10.53.1",
"@sentry/core": "10.51.0" "@sentry/core": "10.53.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/replay-canvas": { "node_modules/@sentry-internal/replay-canvas": {
"version": "10.51.0", "version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.51.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.53.1.tgz",
"integrity": "sha512-8PW1Pp+Yl3lPwYqhBCr5SgkuhDanu9ZLzUqD2bPKL/ElqbM2eDVIWxq4z4ZzePrmZa6IcCjTv6sVQJ7Z4dLyLA==", "integrity": "sha512-aueLaf/2prExwA76BGU5/bOXCKWqtt6jQXWA6WJQNrmKpPEtZJB4ypnpsou0McXQCF8tur2Y8U0TEkwQP13yJQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/replay": "10.51.0", "@sentry-internal/replay": "10.53.1",
"@sentry/core": "10.51.0" "@sentry/core": "10.53.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry/browser": { "node_modules/@sentry/browser": {
"version": "10.51.0", "version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.51.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.53.1.tgz",
"integrity": "sha512-Zdc0sKfenxUtW/OGhtJ7xHFN44bXR7YqxJ1zBDzlZfW0nTbeTTUZBq9z5NUw6qdS0Vs/i3V4qzAKTbRKWfqSEA==", "integrity": "sha512-zXF373hzUOGzUOrqd8xb1U3LQi5uYC3mwv+z5OMKUUinQlu30tTWBs7ypy6YTchtix9QlYaHWlayUF8vBZ5UjA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/browser-utils": "10.51.0", "@sentry-internal/browser-utils": "10.53.1",
"@sentry-internal/feedback": "10.51.0", "@sentry-internal/feedback": "10.53.1",
"@sentry-internal/replay": "10.51.0", "@sentry-internal/replay": "10.53.1",
"@sentry-internal/replay-canvas": "10.51.0", "@sentry-internal/replay-canvas": "10.53.1",
"@sentry/core": "10.51.0" "@sentry/core": "10.53.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry/core": { "node_modules/@sentry/core": {
"version": "10.51.0", "version": "10.53.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.51.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.53.1.tgz",
"integrity": "sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w==", "integrity": "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -2464,9 +2483,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.1", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -2481,6 +2500,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/codemirror": {
"version": "5.60.17", "version": "5.60.17",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz",
@@ -2603,9 +2632,9 @@
} }
}, },
"node_modules/alpinejs": { "node_modules/alpinejs": {
"version": "3.15.11", "version": "3.15.12",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.11.tgz", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz",
"integrity": "sha512-m26gkTg/MId8O+F4jHKK3vB3SjbFxxk/JHP+qzmw1H6aQrZuPAg4CUoAefnASzzp/eNroBjrRQe7950bNeaBJw==", "integrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "~3.1.1" "@vue/reactivity": "~3.1.1"
@@ -2728,9 +2757,9 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "5.0.5", "version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^4.0.2" "balanced-match": "^4.0.2"
@@ -2996,9 +3025,9 @@
} }
}, },
"node_modules/cytoscape": { "node_modules/cytoscape": {
"version": "3.33.2", "version": "3.33.4",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.4.tgz",
"integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", "integrity": "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
@@ -3295,9 +3324,9 @@
} }
}, },
"node_modules/easymde": { "node_modules/easymde": {
"version": "2.20.0", "version": "2.21.0",
"resolved": "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz", "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.21.0.tgz",
"integrity": "sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==", "integrity": "sha512-5uE7I/DEN8gvGRwxaqAv7h1PMEK2ykNXVX5zL0dK3nCYROGja3AMbdQz8eCEELnfvCfy7tRkTmLuvyJG8uSWjQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/codemirror": "^5.60.10", "@types/codemirror": "^5.60.10",
@@ -3656,12 +3685,12 @@
} }
}, },
"node_modules/js-cookie": { "node_modules/js-cookie": {
"version": "3.0.5", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", "integrity": "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=14" "node": ">=20"
} }
}, },
"node_modules/js-tokens": { "node_modules/js-tokens": {
@@ -3990,9 +4019,9 @@
} }
}, },
"node_modules/lit-html": { "node_modules/lit-html": {
"version": "3.3.2", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz",
"integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"@types/trusted-types": "^2.0.2" "@types/trusted-types": "^2.0.2"
@@ -4065,9 +4094,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4332,9 +4361,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.12", "version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4352,7 +4381,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.12",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -4498,14 +4527,14 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.127.0", "@oxc-project/types": "=0.130.0",
"@rolldown/pluginutils": "1.0.0-rc.17" "@rolldown/pluginutils": "^1.0.0"
}, },
"bin": { "bin": {
"rolldown": "bin/cli.mjs" "rolldown": "bin/cli.mjs"
@@ -4514,21 +4543,21 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-android-arm64": "1.0.1",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.1",
"@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.1",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.1",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.1",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.1",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.1",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.1",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.1",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.1",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.1",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.1",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" "@rolldown/binding-win32-x64-msvc": "1.0.1"
} }
}, },
"node_modules/rollup-plugin-visualizer": { "node_modules/rollup-plugin-visualizer": {
@@ -4785,9 +4814,9 @@
} }
}, },
"node_modules/tom-select": { "node_modules/tom-select": {
"version": "2.6.0", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.6.0.tgz", "resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.6.1.tgz",
"integrity": "sha512-o2ToBjhUAnrrQvW/hrY9c//TpOpAKYSlfuFnf0DIwNy+ua+mmYnsF4PxN/PpzBfUIfEFkNYAngeGBfOAZWF3tw==", "integrity": "sha512-d/1kngVOQTGcI/2pVDfDLYjtjUgSSd3fSgkYUpi0y+yRtQQu2kzljj3aUdqMfqc45cjPvDEpfDt/hSX4awDFTg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@orchidjs/sifter": "^1.1.0", "@orchidjs/sifter": "^1.1.0",
@@ -4914,16 +4943,16 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.10", "version": "8.0.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.4",
"postcss": "^8.5.10", "postcss": "^8.5.14",
"rolldown": "1.0.0-rc.17", "rolldown": "1.0.1",
"tinyglobby": "^0.2.16" "tinyglobby": "^0.2.16"
}, },
"bin": { "bin": {
@@ -4940,7 +4969,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0", "@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", "esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0", "jiti": ">=1.21.0",
"less": "^4.0.0", "less": "^4.0.0",
+12 -11
View File
@@ -25,19 +25,20 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.29.0", "@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2", "@babel/preset-env": "^7.29.5",
"@biomejs/biome": "^2.4.13", "@biomejs/biome": "^2.4.15",
"@hey-api/openapi-ts": "^0.94.5", "@hey-api/openapi-ts": "^0.94.5",
"@types/alpinejs": "^3.13.11", "@types/alpinejs": "^3.13.11",
"@types/alpinejs__sort": "^3.13.0",
"@types/cytoscape-cxtmenu": "^3.4.5", "@types/cytoscape-cxtmenu": "^3.4.5",
"@types/cytoscape-klay": "^3.1.5", "@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.0.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.0.10" "vite": "^8.0.13"
}, },
"dependencies": { "dependencies": {
"@alpinejs/sort": "^3.15.11", "@alpinejs/sort": "^3.15.12",
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0", "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
"@floating-ui/dom": "^1.7.6", "@floating-ui/dom": "^1.7.6",
"@fortawesome/fontawesome-free": "^7.2.0", "@fortawesome/fontawesome-free": "^7.2.0",
@@ -45,25 +46,25 @@
"@fullcalendar/daygrid": "^6.1.20", "@fullcalendar/daygrid": "^6.1.20",
"@fullcalendar/icalendar": "^6.1.20", "@fullcalendar/icalendar": "^6.1.20",
"@fullcalendar/list": "^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", "@zip.js/zip.js": "^2.8.26",
"3d-force-graph": "^1.80.0", "3d-force-graph": "^1.80.0",
"alpinejs": "^3.15.11", "alpinejs": "^3.15.12",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"country-flag-emoji-polyfill": "^0.1.8", "country-flag-emoji-polyfill": "^0.1.8",
"cytoscape": "^3.33.2", "cytoscape": "^3.33.4",
"cytoscape-cxtmenu": "^3.5.0", "cytoscape-cxtmenu": "^3.5.0",
"cytoscape-klay": "^3.1.4", "cytoscape-klay": "^3.1.4",
"d3-force-3d": "^3.0.6", "d3-force-3d": "^3.0.6",
"easymde": "^2.20.0", "easymde": "^2.21.0",
"glob": "^13.0.6", "glob": "^13.0.6",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"htmx.org": "^2.0.10", "htmx.org": "^2.0.10",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.7",
"lit-html": "^3.3.2", "lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1", "native-file-system-adapter": "^3.0.1",
"three": "^0.184.0", "three": "^0.184.0",
"three-spritetext": "^1.10.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" } license = { text = "GPL-3.0-only" }
requires-python = "<4.0,>=3.12" requires-python = "<4.0,>=3.12"
dependencies = [ dependencies = [
"django>=5.2.13,<6.0.0", "django>=5.2.14,<6.0.0",
"django-ninja>=1.6.2,<6.0.0", "django-ninja>=1.6.2,<2.0.0",
"django-ninja-extra>=0.31.4", "django-ninja-extra>=0.31.4",
"Pillow>=12.2.0,<13.0.0", "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", "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", "django-phonenumber-field>=8.4.0,<9.0.0",
"phonenumbers>=9.0.29,<10.0.0", "phonenumbers>=9.0.30,<10.0.0",
"reportlab>=4.5.0,<5.0.0", "reportlab>=4.5.1,<5.0.0",
"django-haystack>=3.3.0,<4.0.0", "django-haystack>=3.3.0,<4.0.0",
"xapian-haystack>=4.0.0,<5.0.0", "xapian-haystack>=4.0.0,<5.0.0",
"libsass<1.0.0,>=0.23.0", "libsass>=0.23.0,<1.0.0",
"django-ordered-model<4.0.0,>=3.7.4", "django-ordered-model>=3.7.4,<4.0.0",
"django-simple-captcha<1.0.0,>=0.6.3", "django-simple-captcha>=0.6.3,<1.0.0",
"python-dateutil<3.0.0.0,>=2.9.0.post0", "python-dateutil>=2.9.0.post0,<3.0.0.0",
"sentry-sdk>=2.58.0,<3.0.0", "sentry-sdk>=2.60.0,<3.0.0",
"jinja2<4.0.0,>=3.1.6", "jinja2>=3.1.6,<4.0.0",
"django-countries>=8.2.0,<9.0.0", "django-countries>=8.2.0,<9.0.0",
"dict2xml>=1.7.8,<2.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", "tomli>=2.4.1,<3.0.0",
"django-honeypot>=1.3.0,<2", "django-honeypot>=1.3.0,<2",
"pydantic-extra-types>=2.11.1,<3.0.0", "pydantic-extra-types>=2.11.1,<3.0.0",
"ical>=11.1.0,<14.0.0", "ical>=12.0.0,<14.0.0",
"redis[hiredis]>=6.4.0,<8.0.0", "redis[hiredis]>=3.3.1,<8.0.0",
"environs[django]>=15.0.1,<16.0.0", "environs[django]>=15.0.1,<16",
"requests>=2.32.5,<3.0.0", "requests>=2.34.2,<3.0.0",
"honcho>=2.0.0", "honcho>=2.0.0",
"psutil>=7.2.2,<8.0.0", "psutil>=7.2.2,<8.0.0",
"celery[redis]>=5.6.2,<7", "celery[redis]>=5.6.3,<8",
"django-celery-results>=2.5.1", "django-celery-results>=2.6.0",
"django-celery-beat>=2.9.0", "django-celery-beat>=2.9.0",
] ]
@@ -60,43 +60,43 @@ documentation = "https://sith-ae.readthedocs.io/"
[dependency-groups] [dependency-groups]
prod = [ prod = [
"psycopg[c]>=3.3.3,<4.0.0", "psycopg[c]>=3.3.4,<4.0.0",
] ]
dev = [ dev = [
"django-debug-toolbar>=6.3.0,<7", "django-debug-toolbar>=6.3.0,<7",
"ipython>=9.13.0,<10.0.0", "ipython>=9.13.0,<10.0.0",
"pre-commit>=4.6.0,<5.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", "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", "rjsmin>=1.2.5,<2.0.0",
] ]
tests = [ tests = [
"freezegun>=1.5.5,<2.0.0", "freezegun>=1.5.5,<2.0.0",
"pytest>=9.0.3,<10.0.0", "pytest>=9.0.3,<10.0.0",
"pytest-cov>=7.1.0,<8.0.0", "pytest-cov>=7.1.0,<8.0.0",
"pytest-django<5.0.0,>=4.12.0", "pytest-django>=4.12.0,<5.0.0",
"model-bakery<2.0.0,>=1.23.4", "model-bakery>=1.23.4,<2.0.0",
"beautifulsoup4>=4.14.3,<5", "beautifulsoup4>=4.14.3,<5",
"lxml>=6.1.0,<7", "lxml>=6.1.1,<7",
] ]
docs = [ docs = [
"mkdocs<2.0.0,>=1.6.1", "mkdocs>=1.6.1,<2.0.0",
"mkdocs-material>=9.7.6,<10.0.0", "mkdocs-material>=9.7.6,<10.0.0",
"mkdocstrings>=1.0.4,<2.0.0", "mkdocstrings>=1.0.4,<2.0.0",
"mkdocstrings-python>=2.0.3,<3.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] [tool.uv]
default-groups = ["dev", "tests", "docs"] default-groups = ["dev", "tests", "docs"]
[tool.xapian] [tool.xapian]
version = "1.4.31" version = "2.0.0"
# Those hashes are here to protect against supply chains attacks # Those hashes are here to protect against supply chains attacks
# See `https://ae-utbm.github.io/sith/howto/xapian/` for more information # See `https://ae-utbm.github.io/sith/howto/xapian/` for more information
core-sha256 = "fecf609ea2efdc8a64be369715aac733336a11f7480a6545244964ae6bc80811" core-sha256 = "6cea3f49952a47224439a40bdb3608f928d121ad8721b9921cc42802d548ecf8"
bindings-sha256 = "a38cc7ba4188cc0bd27dc7369f03906772047087a1c54f1b93355d5e9103c304" bindings-sha256 = "9a544b69c31355a92edbcd4102cf0f1ec4407fd0a4645f4870fb52300b736910"
[tool.ruff] [tool.ruff]
output-format = "concise" # makes ruff error logs easier to read 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[], albums: [] as Album[],
async fetchPictures(): Promise<PictureSchema[]> { async fetchPictures(): Promise<PictureSchema[]> {
const localStorageKey = `user${config.userId}Pictures`; // Check the cache before hitting the API.
const localStorageInvalidationKey = `user${config.userId}PicturesNumber`; const storageKey = "userPictures";
const lastCachedNumber = localStorage.getItem(localStorageInvalidationKey); const cacheContent: { userId: number; pictures: PictureSchema[] }[] = JSON.parse(
sessionStorage.getItem(storageKey) || "[]",
);
const userPictures = cacheContent.find((obj) => obj.userId === config.userId);
if ( if (
lastCachedNumber !== null && userPictures !== undefined &&
Number.parseInt(lastCachedNumber, 10) === config.nbPictures 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, { const pictures = await paginated(picturesFetchPictures, {
// biome-ignore lint/style/useNamingConvention: from python api // biome-ignore lint/style/useNamingConvention: from python api
query: { users_identified: [config.userId] }, query: { users_identified: [config.userId] },
} as PicturesFetchPicturesData); } as PicturesFetchPicturesData);
cacheContent.push({ userId: config.userId, pictures: pictures });
try { try {
localStorage.setItem(localStorageInvalidationKey, config.nbPictures.toString()); // cache only the pictures of the last 4 visited profiles
localStorage.setItem(localStorageKey, JSON.stringify(pictures)); sessionStorage.setItem(storageKey, JSON.stringify(cacheContent.slice(-4)));
} catch { } catch {
// an exception is raised if the localstorage is entirely filled // an exception is raised if the storage is entirely filled.
// so just delete all cached user pictures. // To be as safe as possible, delete the cached pictures.
// A cache hit is not worth the page breaking. // A cache hit is not worth the page breaking.
Object.keys(localStorage) sessionStorage.removeItem(storageKey);
.filter(
(key) =>
key.startsWith("user") &&
(key.endsWith("Pictures") || key.endsWith("PicturesNumber")),
)
.forEach((key) => {
localStorage.removeItem(key);
});
} }
return pictures; return pictures;
}, },
+5 -5
View File
@@ -88,6 +88,11 @@ X_FRAME_OPTIONS = "SAMEORIGIN"
ALLOWED_HOSTS = ["*"] 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 # Application definition
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
@@ -566,11 +571,6 @@ SITH_BARMAN_TIMEOUT = 30
# Minutes to delete the last operations # Minutes to delete the last operations
SITH_LAST_OPERATIONS_LIMIT = 10 SITH_LAST_OPERATIONS_LIMIT = 10
# time before a basket is considered expired
SITH_EBOUTIC_BASKET_TIMEOUT = timedelta(minutes=10)
# time that a user can spend on the CB payment page before it to timeout
SITH_EBOUTIC_ETRANSACTION_TIMEOUT = timedelta(minutes=10)
# ET variables # ET variables
SITH_EBOUTIC_CB_ENABLED = env.bool("SITH_EBOUTIC_CB_ENABLED", default=True) SITH_EBOUTIC_CB_ENABLED = env.bool("SITH_EBOUTIC_CB_ENABLED", default=True)
SITH_EBOUTIC_ET_URL = env.str( SITH_EBOUTIC_ET_URL = env.str(
Generated
+391 -381
View File
File diff suppressed because it is too large Load Diff