Merge pull request #1067 from ae-utbm/taiste

bug fixing, external calendar removal and eurockéennes partnership
This commit is contained in:
thomas girod 2025-04-06 23:16:04 +02:00 committed by GitHub
commit d94d90357e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 639 additions and 626 deletions

View File

@ -9,7 +9,7 @@ repos:
# Run the formatter. # Run the formatter.
- id: ruff-format - id: ruff-format
- repo: https://github.com/biomejs/pre-commit - repo: https://github.com/biomejs/pre-commit
rev: "v0.1.0" # Use the sha / tag you want to point at rev: v0.6.1
hooks: hooks:
- id: biome-check - id: biome-check
additional_dependencies: ["@biomejs/biome@1.9.4"] additional_dependencies: ["@biomejs/biome@1.9.4"]

View File

@ -1,8 +1,6 @@
from pathlib import Path
from typing import Literal from typing import Literal
from django.conf import settings from django.http import HttpResponse
from django.http import Http404, HttpResponse
from ninja import Query from ninja import Query
from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra from ninja_extra.pagination import PageNumberPaginationExtra
@ -18,23 +16,6 @@ from core.views.files import send_raw_file
@api_controller("/calendar") @api_controller("/calendar")
class CalendarController(ControllerBase): class CalendarController(ControllerBase):
CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars"
@route.get("/external.ics", url_name="calendar_external")
def calendar_external(self):
"""Return the ICS file of the AE Google Calendar
Because of Google's cors rules, we can't just do a request to google ics
from the frontend. Google is blocking CORS request in its responses headers.
The only way to do it from the frontend is to use Google Calendar API with an API key
This is not especially desirable as your API key is going to be provided to the frontend.
This is why we have this backend based solution.
"""
if (calendar := IcsCalendar.get_external()) is not None:
return send_raw_file(calendar)
raise Http404
@route.get("/internal.ics", url_name="calendar_internal") @route.get("/internal.ics", url_name="calendar_internal")
def calendar_internal(self): def calendar_internal(self):
return send_raw_file(IcsCalendar.get_internal()) return send_raw_file(IcsCalendar.get_internal())

View File

@ -1,8 +1,6 @@
from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import final from typing import final
import requests
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.conf import settings from django.conf import settings
from django.db.models import F, QuerySet from django.db.models import F, QuerySet
@ -19,35 +17,8 @@ from core.models import User
@final @final
class IcsCalendar: class IcsCalendar:
_CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars"
_EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.ics"
_INTERNAL_CALENDAR = _CACHE_FOLDER / "internal.ics" _INTERNAL_CALENDAR = _CACHE_FOLDER / "internal.ics"
@classmethod
def get_external(cls, expiration: timedelta = timedelta(hours=1)) -> Path | None:
if (
cls._EXTERNAL_CALENDAR.exists()
and timezone.make_aware(
datetime.fromtimestamp(cls._EXTERNAL_CALENDAR.stat().st_mtime)
)
+ expiration
> timezone.now()
):
return cls._EXTERNAL_CALENDAR
return cls.make_external()
@classmethod
def make_external(cls) -> Path | None:
calendar = requests.get(
"https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics"
)
if not calendar.ok:
return None
cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True)
with open(cls._EXTERNAL_CALENDAR, "wb") as f:
_ = f.write(calendar.content)
return cls._EXTERNAL_CALENDAR
@classmethod @classmethod
def get_internal(cls) -> Path: def get_internal(cls) -> Path:
if not cls._INTERNAL_CALENDAR.exists(): if not cls._INTERNAL_CALENDAR.exists():

View File

@ -8,7 +8,6 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import iCalendarPlugin from "@fullcalendar/icalendar"; import iCalendarPlugin from "@fullcalendar/icalendar";
import listPlugin from "@fullcalendar/list"; import listPlugin from "@fullcalendar/list";
import { import {
calendarCalendarExternal,
calendarCalendarInternal, calendarCalendarInternal,
calendarCalendarUnpublished, calendarCalendarUnpublished,
newsDeleteNews, newsDeleteNews,
@ -151,11 +150,6 @@ export class IcsCalendar extends inheritHtmlElement("div") {
format: "ics", format: "ics",
className: "internal", className: "internal",
}, },
{
url: `${await makeUrl(calendarCalendarExternal)}${cacheInvalidate}`,
format: "ics",
className: "external",
},
{ {
url: `${await makeUrl(calendarCalendarUnpublished)}${cacheInvalidate}`, url: `${await makeUrl(calendarCalendarUnpublished)}${cacheInvalidate}`,
format: "ics", format: "ics",
@ -224,9 +218,6 @@ export class IcsCalendar extends inheritHtmlElement("div") {
}; };
const makePopupTools = (event: EventImpl) => { const makePopupTools = (event: EventImpl) => {
if (event.source.internalEventSource.ui.classNames.includes("external")) {
return null;
}
if (!(this.canDelete || this.canModerate)) { if (!(this.canDelete || this.canModerate)) {
return null; return null;
} }

View File

@ -1,8 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from typing import Callable
from unittest.mock import MagicMock, patch
from urllib.parse import quote from urllib.parse import quote
import pytest import pytest
@ -11,7 +9,6 @@ from django.contrib.auth.models import Permission
from django.http import HttpResponse from django.http import HttpResponse
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import now from django.utils.timezone import now
from model_bakery import baker, seq from model_bakery import baker, seq
from pytest_django.asserts import assertNumQueries from pytest_django.asserts import assertNumQueries
@ -41,78 +38,6 @@ def accel_redirect_to_file(response: HttpResponse) -> Path | None:
) )
@pytest.mark.django_db
class TestExternalCalendar:
@pytest.fixture
def mock_request(self):
mock = MagicMock()
with patch("requests.get", mock):
yield mock
@pytest.fixture
def mock_current_time(self):
mock = MagicMock()
original = timezone.now
with patch("django.utils.timezone.now", mock):
yield mock, original
@pytest.fixture(autouse=True)
def clear_cache(self):
IcsCalendar._EXTERNAL_CALENDAR.unlink(missing_ok=True)
def test_fetch_error(self, client: Client, mock_request: MagicMock):
mock_request.return_value = MockResponse(ok=False, value="not allowed")
assert client.get(reverse("api:calendar_external")).status_code == 404
def test_fetch_success(self, client: Client, mock_request: MagicMock):
external_response = MockResponse(ok=True, value="Definitely an ICS")
mock_request.return_value = external_response
response = client.get(reverse("api:calendar_external"))
assert response.status_code == 200
out_file = accel_redirect_to_file(response)
assert out_file is not None
assert out_file.exists()
with open(out_file, "r") as f:
assert f.read() == external_response.value
def test_fetch_caching(
self,
client: Client,
mock_request: MagicMock,
mock_current_time: tuple[MagicMock, Callable[[], datetime]],
):
fake_current_time, original_timezone = mock_current_time
start_time = original_timezone()
fake_current_time.return_value = start_time
external_response = MockResponse(200, "Definitely an ICS")
mock_request.return_value = external_response
with open(
accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r"
) as f:
assert f.read() == external_response.value
mock_request.return_value = MockResponse(200, "This should be ignored")
with open(
accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r"
) as f:
assert f.read() == external_response.value
mock_request.assert_called_once()
fake_current_time.return_value = start_time + timedelta(hours=1, seconds=1)
external_response = MockResponse(200, "This won't be ignored")
mock_request.return_value = external_response
with open(
accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r"
) as f:
assert f.read() == external_response.value
assert mock_request.call_count == 2
@pytest.mark.django_db @pytest.mark.django_db
class TestInternalCalendar: class TestInternalCalendar:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)

View File

@ -19,7 +19,7 @@ import pytest
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import html from django.utils import html
from django.utils.timezone import localtime, now from django.utils.timezone import localtime, now
@ -323,7 +323,7 @@ class TestNewsCreation(TestCase):
@pytest.mark.django_db @pytest.mark.django_db
def test_feed(client): def test_feed(client: Client):
"""Smoke test that checks that the atom feed is working""" """Smoke test that checks that the atom feed is working"""
Site.objects.clear_cache() Site.objects.clear_cache()
with assertNumQueries(2): with assertNumQueries(2):
@ -332,3 +332,22 @@ def test_feed(client):
resp = client.get(reverse("com:news_feed")) resp = client.get(reverse("com:news_feed"))
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.headers["Content-Type"] == "application/rss+xml; charset=utf-8" assert resp.headers["Content-Type"] == "application/rss+xml; charset=utf-8"
@pytest.mark.django_db
@pytest.mark.parametrize(
"url",
[
reverse("com:poster_list"),
reverse("com:poster_create"),
reverse("com:poster_moderate_list"),
],
)
def test_poster_management_views_crash_test(client: Client, url: str):
"""Test that poster management views work"""
user = baker.make(
User, groups=[Group.objects.get(pk=settings.SITH_GROUP_COM_ADMIN_ID)]
)
client.force_login(user)
res = client.get(url)
assert res.status_code == 200

View File

@ -61,8 +61,7 @@ sith = Sith.objects.first
class ComTabsMixin(TabedViewMixin): class ComTabsMixin(TabedViewMixin):
def get_tabs_title(self): tabs_title = _("Communication administration")
return _("Communication administration")
def get_list_of_tabs(self): def get_list_of_tabs(self):
return [ return [
@ -559,7 +558,11 @@ class MailingModerateView(View):
raise PermissionDenied raise PermissionDenied
class PosterListBaseView(ListView): class PosterAdminViewMixin(IsComAdminMixin, ComTabsMixin):
current_tab = "posters"
class PosterListBaseView(PosterAdminViewMixin, ListView):
"""List communication posters.""" """List communication posters."""
current_tab = "posters" current_tab = "posters"
@ -586,7 +589,7 @@ class PosterListBaseView(ListView):
return kwargs return kwargs
class PosterCreateBaseView(CreateView): class PosterCreateBaseView(PosterAdminViewMixin, CreateView):
"""Create communication poster.""" """Create communication poster."""
current_tab = "posters" current_tab = "posters"
@ -618,7 +621,7 @@ class PosterCreateBaseView(CreateView):
return super().form_valid(form) return super().form_valid(form)
class PosterEditBaseView(UpdateView): class PosterEditBaseView(PosterAdminViewMixin, UpdateView):
"""Edit communication poster.""" """Edit communication poster."""
pk_url_kwarg = "poster_id" pk_url_kwarg = "poster_id"
@ -664,7 +667,7 @@ class PosterEditBaseView(UpdateView):
return super().form_valid(form) return super().form_valid(form)
class PosterDeleteBaseView(DeleteView): class PosterDeleteBaseView(PosterAdminViewMixin, DeleteView):
"""Edit communication poster.""" """Edit communication poster."""
pk_url_kwarg = "poster_id" pk_url_kwarg = "poster_id"
@ -681,7 +684,7 @@ class PosterDeleteBaseView(DeleteView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView): class PosterListView(PosterListBaseView):
"""List communication posters.""" """List communication posters."""
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -690,7 +693,7 @@ class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView):
return kwargs return kwargs
class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView): class PosterCreateView(PosterCreateBaseView):
"""Create communication poster.""" """Create communication poster."""
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
@ -701,7 +704,7 @@ class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView):
return kwargs return kwargs
class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView): class PosterEditView(PosterEditBaseView):
"""Edit communication poster.""" """Edit communication poster."""
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
@ -712,13 +715,13 @@ class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView):
return kwargs return kwargs
class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView): class PosterDeleteView(PosterDeleteBaseView):
"""Delete communication poster.""" """Delete communication poster."""
success_url = reverse_lazy("com:poster_list") success_url = reverse_lazy("com:poster_list")
class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView): class PosterModerateListView(PosterAdminViewMixin, ListView):
"""Moderate list communication poster.""" """Moderate list communication poster."""
current_tab = "posters" current_tab = "posters"
@ -732,7 +735,7 @@ class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView):
return kwargs return kwargs
class PosterModerateView(IsComAdminMixin, ComTabsMixin, View): class PosterModerateView(PosterAdminViewMixin, View):
"""Moderate communication poster.""" """Moderate communication poster."""
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

View File

@ -4,9 +4,10 @@ import pytest
from django.conf import settings from django.conf import settings
from django.contrib import auth from django.contrib import auth
from django.core.management import call_command from django.core.management import call_command
from django.test import Client, TestCase from django.test import Client, RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.views.generic import DetailView
from model_bakery import baker, seq from model_bakery import baker, seq
from model_bakery.recipe import Recipe, foreign_key from model_bakery.recipe import Recipe, foreign_key
from pytest_django.asserts import assertRedirects from pytest_django.asserts import assertRedirects
@ -18,6 +19,7 @@ from core.baker_recipes import (
very_old_subscriber_user, very_old_subscriber_user,
) )
from core.models import Group, User from core.models import Group, User
from core.views import UserTabsMixin
from counter.models import Counter, Refilling, Selling from counter.models import Counter, Refilling, Selling
from eboutic.models import Invoice, InvoiceItem from eboutic.models import Invoice, InvoiceItem
@ -229,3 +231,88 @@ def test_logout(client: Client):
res = client.post(reverse("core:logout")) res = client.post(reverse("core:logout"))
assertRedirects(res, reverse("core:login")) assertRedirects(res, reverse("core:login"))
assert auth.get_user(client).is_anonymous assert auth.get_user(client).is_anonymous
class UserTabTestView(UserTabsMixin, DetailView): ...
@pytest.mark.django_db
@pytest.mark.parametrize(
["user_factory", "expected_tabs"],
[
(
subscriber_user.make,
[
"infos",
"godfathers",
"pictures",
"tools",
"edit",
"prefs",
"clubs",
"stats",
"account",
],
),
(
# this user is superuser, but still won't see a few tabs,
# because he is not subscribed
lambda: baker.make(User, is_superuser=True),
[
"infos",
"godfathers",
"pictures",
"tools",
"edit",
"prefs",
"clubs",
"groups",
],
),
],
)
def test_displayed_user_self_tabs(user_factory, expected_tabs: list[str]):
"""Test that a user can view the appropriate tabs in its own profile"""
user = user_factory()
request = RequestFactory().get("")
request.user = user
view = UserTabTestView()
view.setup(request)
view.object = user
tabs = [tab["slug"] for tab in view.get_list_of_tabs()]
assert tabs == expected_tabs
@pytest.mark.django_db
@pytest.mark.parametrize(
["user_factory", "expected_tabs"],
[
(subscriber_user.make, ["infos", "godfathers", "pictures", "clubs"]),
(
# this user is superuser, but still won't see a few tabs,
# because he is not subscribed
lambda: baker.make(User, is_superuser=True),
[
"infos",
"godfathers",
"pictures",
"edit",
"prefs",
"clubs",
"groups",
"stats",
"account",
],
),
],
)
def test_displayed_other_user_tabs(user_factory, expected_tabs: list[str]):
"""Test that a user can view the appropriate tabs in another user's profile."""
request_user = user_factory()
request = RequestFactory().get("")
request.user = request_user
view = UserTabTestView()
view.setup(request)
view.object = subscriber_user.make() # user whose page is being seen
tabs = [tab["slug"] for tab in view.get_list_of_tabs()]
assert tabs == expected_tabs

View File

@ -242,7 +242,10 @@ class UserTabsMixin(TabedViewMixin):
if ( if (
hasattr(user, "customer") hasattr(user, "customer")
and user.customer and user.customer
and (user == self.request.user or user.has_perm("counter.view_customer")) and (
user == self.request.user
or self.request.user.has_perm("counter.view_customer")
)
): ):
tab_list.append( tab_list.append(
{ {

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.20 on 2025-04-06 11:29
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("counter", "0030_returnableproduct_returnableproductbalance_and_more")
]
operations = [
migrations.AlterModelOptions(
name="counter",
options={
"permissions": [("view_counter_stats", "Can view counter stats")],
"verbose_name": "counter",
},
),
]

View File

@ -526,6 +526,7 @@ class Counter(models.Model):
class Meta: class Meta:
verbose_name = _("counter") verbose_name = _("counter")
permissions = [("view_counter_stats", "Can view counter stats")]
def __str__(self): def __str__(self):
return self.name return self.name
@ -598,13 +599,12 @@ class Counter(models.Model):
- the promo of the barman - the promo of the barman
- the total number of office hours the barman did attend - the total number of office hours the barman did attend
""" """
name_expr = Concat(F("user__first_name"), Value(" "), F("user__last_name"))
return ( return (
self.permanencies.exclude(end=None) self.permanencies.exclude(end=None)
.annotate( .annotate(
name=Concat(F("user__first_name"), Value(" "), F("user__last_name")) name=name_expr, nickname=F("user__nick_name"), promo=F("user__promo")
) )
.annotate(nickname=F("user__nick_name"))
.annotate(promo=F("user__promo"))
.values("user", "name", "nickname", "promo") .values("user", "name", "nickname", "promo")
.annotate(perm_sum=Sum(F("end") - F("start"))) .annotate(perm_sum=Sum(F("end") - F("start")))
.exclude(perm_sum=None) .exclude(perm_sum=None)
@ -628,18 +628,17 @@ class Counter(models.Model):
since = get_start_of_semester() since = get_start_of_semester()
if isinstance(since, date): if isinstance(since, date):
since = datetime(since.year, since.month, since.day, tzinfo=tz.utc) since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)
name_expr = Concat(
F("customer__user__first_name"), Value(" "), F("customer__user__last_name")
)
return ( return (
self.sellings.filter(date__gte=since) self.sellings.filter(date__gte=since)
.annotate( .annotate(
name=Concat( name=name_expr,
F("customer__user__first_name"), nickname=F("customer__user__nick_name"),
Value(" "), promo=F("customer__user__promo"),
F("customer__user__last_name"), user=F("customer__user"),
) )
)
.annotate(nickname=F("customer__user__nick_name"))
.annotate(promo=F("customer__user__promo"))
.annotate(user=F("customer__user"))
.values("user", "promo", "name", "nickname") .values("user", "promo", "name", "nickname")
.annotate( .annotate(
selling_sum=Sum( selling_sum=Sum(

View File

@ -18,7 +18,7 @@ from decimal import Decimal
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import make_password from django.contrib.auth.models import Permission, make_password
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
@ -28,9 +28,10 @@ from django.utils import timezone
from django.utils.timezone import localdate, now from django.utils.timezone import localdate, now
from freezegun import freeze_time from freezegun import freeze_time
from model_bakery import baker from model_bakery import baker
from model_bakery.recipe import Recipe
from pytest_django.asserts import assertRedirects from pytest_django.asserts import assertRedirects
from club.models import Club, Membership from club.models import Membership
from core.baker_recipes import board_user, subscriber_user, very_old_subscriber_user from core.baker_recipes import board_user, subscriber_user, very_old_subscriber_user
from core.models import BanGroup, User from core.models import BanGroup, User
from counter.baker_recipes import product_recipe, sale_recipe from counter.baker_recipes import product_recipe, sale_recipe
@ -572,121 +573,86 @@ class TestCounterClick(TestFullClickBase):
class TestCounterStats(TestCase): class TestCounterStats(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.counter = Counter.objects.get(id=2) cls.users = subscriber_user.make(_quantity=4)
cls.krophil = User.objects.get(username="krophil") product = product_recipe.make(selling_price=1)
cls.skia = User.objects.get(username="skia") cls.counter = baker.make(
cls.sli = User.objects.get(username="sli") Counter, type=["BAR"], sellers=cls.users[:4], products=[product]
cls.root = User.objects.get(username="root")
cls.subscriber = User.objects.get(username="subscriber")
cls.old_subscriber = User.objects.get(username="old_subscriber")
cls.counter.sellers.add(cls.sli, cls.root, cls.skia, cls.krophil)
barbar = Product.objects.get(code="BARB")
# remove everything to make sure the fixtures bring no side effect
Permanency.objects.all().delete()
Selling.objects.all().delete()
now = timezone.now()
# total of sli : 5 hours
Permanency.objects.create(
user=cls.sli, start=now, end=now + timedelta(hours=1), counter=cls.counter
)
Permanency.objects.create(
user=cls.sli,
start=now + timedelta(hours=4),
end=now + timedelta(hours=6),
counter=cls.counter,
)
Permanency.objects.create(
user=cls.sli,
start=now + timedelta(hours=7),
end=now + timedelta(hours=9),
counter=cls.counter,
) )
# total of skia : 16 days, 2 hours, 35 minutes and 54 seconds _now = timezone.now()
Permanency.objects.create( permanence_recipe = Recipe(Permanency, counter=cls.counter)
user=cls.skia, start=now, end=now + timedelta(hours=1), counter=cls.counter perms = [
) *[ # total of user 0 : 5 hours
Permanency.objects.create( permanence_recipe.prepare(user=cls.users[0], start=start, end=end)
user=cls.skia, for start, end in [
start=now + timedelta(days=4, hours=1), (_now, _now + timedelta(hours=1)),
end=now + timedelta(days=20, hours=2, minutes=35, seconds=54), (_now + timedelta(hours=4), _now + timedelta(hours=6)),
counter=cls.counter, (_now + timedelta(hours=7), _now + timedelta(hours=9)),
) ]
],
*[ # total of user 1 : 16 days, 2 hours, 35 minutes and 54 seconds
permanence_recipe.prepare(user=cls.users[1], start=start, end=end)
for start, end in [
(_now, _now + timedelta(hours=1)),
(
_now + timedelta(days=4, hours=1),
_now + timedelta(days=20, hours=2, minutes=35, seconds=54),
),
]
],
*[ # total of user 2 : 2 hour + 20 hours (but the 20 hours were on last year)
permanence_recipe.prepare(user=cls.users[2], start=start, end=end)
for start, end in [
(_now + timedelta(days=5), _now + timedelta(days=5, hours=1)),
(_now - timedelta(days=300, hours=20), _now - timedelta(days=300)),
]
],
]
# user 3 has 0 hours of permanence
Permanency.objects.bulk_create(perms)
# total of root : 1 hour + 20 hours (but the 20 hours were on last year) _sale_recipe = Recipe(
Permanency.objects.create( Selling,
user=cls.root, club=cls.counter.club,
start=now + timedelta(days=5),
end=now + timedelta(days=5, hours=1),
counter=cls.counter,
)
Permanency.objects.create(
user=cls.root,
start=now - timedelta(days=300, hours=20),
end=now - timedelta(days=300),
counter=cls.counter,
)
# total of krophil : 0 hour
s = Selling(
label=barbar.name,
product=barbar,
club=baker.make(Club),
counter=cls.counter, counter=cls.counter,
product=product,
unit_price=2, unit_price=2,
seller=cls.skia,
) )
sales = [
*_sale_recipe.prepare(
quantity=100, customer=cls.users[0].customer, _quantity=10
), # 2000 €
*_sale_recipe.prepare(
quantity=100, customer=cls.users[1].customer, _quantity=5
), # 1000 €
_sale_recipe.prepare(quantity=1, customer=cls.users[2].customer), # 2€
_sale_recipe.prepare(quantity=50, customer=cls.users[3].customer), # 100€
]
Selling.objects.bulk_create(sales)
krophil_customer = Customer.get_or_create(cls.krophil)[0] def test_not_authenticated_access_fail(self):
sli_customer = Customer.get_or_create(cls.sli)[0] url = reverse("counter:stats", args=[self.counter.id])
skia_customer = Customer.get_or_create(cls.skia)[0] response = self.client.get(url)
root_customer = Customer.get_or_create(cls.root)[0] assertRedirects(response, reverse("core:login") + f"?next={url}")
# moderate drinker. Total : 100 €
s.quantity = 50
s.customer = krophil_customer
s.save(allow_negative=True)
# Sli is a drunkard. Total : 2000 €
s.quantity = 100
s.customer = sli_customer
for _ in range(10):
# little trick to make sure the instance is duplicated in db
s.pk = None
s.save(allow_negative=True) # save ten different sales
# Skia is a heavy drinker too. Total : 1000 €
s.customer = skia_customer
for _ in range(5):
s.pk = None
s.save(allow_negative=True)
# Root is quite an abstemious one. Total : 2 €
s.pk = None
s.quantity = 1
s.customer = root_customer
s.save(allow_negative=True)
def test_not_authenticated_user_fail(self):
# Test with not login user
response = self.client.get(reverse("counter:stats", args=[self.counter.id]))
assert response.status_code == 403
def test_unauthorized_user_fails(self): def test_unauthorized_user_fails(self):
self.client.force_login(User.objects.get(username="public")) self.client.force_login(baker.make(User))
response = self.client.get(reverse("counter:stats", args=[self.counter.id])) response = self.client.get(reverse("counter:stats", args=[self.counter.id]))
assert response.status_code == 403 assert response.status_code == 403
def test_authorized_user_ok(self):
perm = Permission.objects.get(codename="view_counter_stats")
self.client.force_login(baker.make(User, user_permissions=[perm]))
response = self.client.get(reverse("counter:stats", args=[self.counter.id]))
assert response.status_code == 200
def test_get_total_sales(self): def test_get_total_sales(self):
"""Test the result of the Counter.get_total_sales() method.""" """Test the result of the Counter.get_total_sales() method."""
assert self.counter.get_total_sales() == 3102 assert self.counter.get_total_sales() == 3102
def test_top_barmen(self): def test_top_barmen(self):
"""Test the result of Counter.get_top_barmen() is correct.""" """Test the result of Counter.get_top_barmen() is correct."""
users = [self.skia, self.root, self.sli] users = [self.users[1], self.users[2], self.users[0]]
perm_times = [ perm_times = [
timedelta(days=16, hours=2, minutes=35, seconds=54), timedelta(days=16, hours=2, minutes=35, seconds=54),
timedelta(hours=21), timedelta(hours=21),
@ -700,12 +666,12 @@ class TestCounterStats(TestCase):
"nickname": user.nick_name, "nickname": user.nick_name,
"perm_sum": perm_time, "perm_sum": perm_time,
} }
for user, perm_time in zip(users, perm_times, strict=False) for user, perm_time in zip(users, perm_times, strict=True)
] ]
def test_top_customer(self): def test_top_customer(self):
"""Test the result of Counter.get_top_customers() is correct.""" """Test the result of Counter.get_top_customers() is correct."""
users = [self.sli, self.skia, self.krophil, self.root] users = [self.users[0], self.users[1], self.users[3], self.users[2]]
sale_amounts = [2000, 1000, 100, 2] sale_amounts = [2000, 1000, 100, 2]
assert list(self.counter.get_top_customers()) == [ assert list(self.counter.get_top_customers()) == [
{ {
@ -715,7 +681,7 @@ class TestCounterStats(TestCase):
"nickname": user.nick_name, "nickname": user.nick_name,
"selling_sum": sale_amount, "selling_sum": sale_amount,
} }
for user, sale_amount in zip(users, sale_amounts, strict=False) for user, sale_amount in zip(users, sale_amounts, strict=True)
] ]

View File

@ -27,7 +27,7 @@ from django.utils.translation import gettext as _
from django.views.generic import DetailView, ListView, TemplateView from django.views.generic import DetailView, ListView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from core.auth.mixins import CanEditMixin, CanViewMixin from core.auth.mixins import CanViewMixin
from core.utils import get_semester_code, get_start_of_semester from core.utils import get_semester_code, get_start_of_semester
from counter.forms import ( from counter.forms import (
CloseCustomerAccountForm, CloseCustomerAccountForm,
@ -274,12 +274,13 @@ class SellingDeleteView(DeleteView):
raise PermissionDenied raise PermissionDenied
class CounterStatView(DetailView, CounterAdminMixin): class CounterStatView(PermissionRequiredMixin, DetailView):
"""Show the bar stats.""" """Show the bar stats."""
model = Counter model = Counter
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
template_name = "counter/stats.jinja" template_name = "counter/stats.jinja"
permission_required = "counter.view_counter_stats"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add stats to the context.""" """Add stats to the context."""
@ -301,18 +302,6 @@ class CounterStatView(DetailView, CounterAdminMixin):
) )
return kwargs return kwargs
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
except PermissionDenied:
if (
request.user.is_root
or request.user.is_board_member
or self.get_object().is_owned_by(request.user)
):
return super(CanEditMixin, self).dispatch(request, *args, **kwargs)
raise PermissionDenied
class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
"""List of refillings on a counter.""" """List of refillings on a counter."""

View File

@ -26,7 +26,7 @@ class EtransactionInfoController(ControllerBase):
customer=customer, defaults=info.model_dump(exclude_none=True) customer=customer, defaults=info.model_dump(exclude_none=True)
) )
@route.get("/data", url_name="etransaction_data", include_in_schema=False) @route.get("/data", url_name="etransaction_data")
def fetch_etransaction_data(self): def fetch_etransaction_data(self):
"""Generate the data to pay an eboutic command with paybox. """Generate the data to pay an eboutic command with paybox.

View File

@ -0,0 +1,91 @@
import { exportToHtml } from "#core:utils/globals";
import {
type BillingInfoSchema,
etransactioninfoFetchEtransactionData,
etransactioninfoPutUserBillingInfo,
} from "#openapi";
enum BillingInfoReqState {
Success = "0",
Failure = "1",
Sending = "2",
}
exportToHtml("BillingInfoReqState", BillingInfoReqState);
document.addEventListener("alpine:init", () => {
Alpine.data("etransactionData", (initialData) => ({
data: initialData,
async fill() {
const button = document.getElementById("bank-submit-button") as HTMLButtonElement;
button.disabled = true;
const res = await etransactioninfoFetchEtransactionData();
if (res.response.ok) {
this.data = res.data;
button.disabled = false;
}
},
}));
Alpine.data("billing_infos", (userId: number) => ({
/** @type {BillingInfoReqState | null} */
reqState: null,
async sendForm() {
this.reqState = BillingInfoReqState.Sending;
const form = document.getElementById("billing_info_form");
const submitButton = document.getElementById(
"bank-submit-button",
) as HTMLButtonElement;
submitButton.disabled = true;
const payload = Object.fromEntries(
Array.from(form.querySelectorAll("input, select"))
.filter((elem: HTMLInputElement) => elem.type !== "submit" && elem.value)
.map((elem: HTMLInputElement) => [elem.name, elem.value]),
);
const res = await etransactioninfoPutUserBillingInfo({
// biome-ignore lint/style/useNamingConvention: API is snake_case
path: { user_id: userId },
body: payload as unknown as BillingInfoSchema,
});
this.reqState = res.response.ok
? BillingInfoReqState.Success
: BillingInfoReqState.Failure;
if (res.response.status === 422) {
const errors = await res.response
.json()
.detail.flatMap((err: Record<"loc", string>) => err.loc);
for (const elem of Array.from(form.querySelectorAll("input")).filter((elem) =>
errors.includes(elem.name),
)) {
elem.setCustomValidity(gettext("Incorrect value"));
elem.reportValidity();
elem.oninput = () => elem.setCustomValidity("");
}
} else if (res.response.ok) {
this.$dispatch("billing-infos-filled");
}
},
getAlertColor() {
if (this.reqState === BillingInfoReqState.Success) {
return "green";
}
if (this.reqState === BillingInfoReqState.Failure) {
return "red";
}
return "";
},
getAlertMessage() {
if (this.reqState === BillingInfoReqState.Success) {
return gettext("Billing info registration success");
}
if (this.reqState === BillingInfoReqState.Failure) {
return gettext("Billing info registration failure");
}
return "";
},
}));
});

View File

@ -158,4 +158,3 @@
flex-direction: column; flex-direction: column;
} }
} }

View File

@ -1,87 +0,0 @@
/**
* @readonly
* @enum {number}
*/
const BillingInfoReqState = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
SUCCESS: 1,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
FAILURE: 2,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
SENDING: 3,
};
document.addEventListener("alpine:init", () => {
Alpine.store("billing_inputs", {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
data: etData,
async fill() {
document.getElementById("bank-submit-button").disabled = true;
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
const res = await fetch(etDataUrl);
if (res.ok) {
this.data = await res.json();
document.getElementById("bank-submit-button").disabled = false;
}
},
});
Alpine.data("billing_infos", () => ({
/** @type {BillingInfoReqState | null} */
reqState: null,
async sendForm() {
this.reqState = BillingInfoReqState.SENDING;
const form = document.getElementById("billing_info_form");
document.getElementById("bank-submit-button").disabled = true;
const payload = Object.fromEntries(
Array.from(form.querySelectorAll("input, select"))
.filter((elem) => elem.type !== "submit" && elem.value)
.map((elem) => [elem.name, elem.value]),
);
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
const res = await fetch(billingInfoUrl, {
method: "PUT",
body: JSON.stringify(payload),
});
this.reqState = res.ok
? BillingInfoReqState.SUCCESS
: BillingInfoReqState.FAILURE;
if (res.status === 422) {
const errors = (await res.json()).detail.flatMap((err) => err.loc);
for (const elem of Array.from(form.querySelectorAll("input")).filter((elem) =>
errors.includes(elem.name),
)) {
elem.setCustomValidity(gettext("Incorrect value"));
elem.reportValidity();
elem.oninput = () => elem.setCustomValidity("");
}
} else if (res.ok) {
Alpine.store("billing_inputs").fill();
}
},
getAlertColor() {
if (this.reqState === BillingInfoReqState.SUCCESS) {
return "green";
}
if (this.reqState === BillingInfoReqState.FAILURE) {
return "red";
}
return "";
},
getAlertMessage() {
if (this.reqState === BillingInfoReqState.SUCCESS) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
return billingInfoSuccessMessage;
}
if (this.reqState === BillingInfoReqState.FAILURE) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
return billingInfoFailureMessage;
}
return "";
},
}));
});

View File

@ -78,7 +78,11 @@
{% if not request.user.date_of_birth %} {% if not request.user.date_of_birth %}
<div class="alert alert-red" x-data="{show_alert: true}" x-show="show_alert" x-transition> <div class="alert alert-red" x-data="{show_alert: true}" x-show="show_alert" x-transition>
<span class="alert-main"> <span class="alert-main">
{% trans %}You have not filled in your date of birth. As a result, you may not have access to all the products in the online shop. To fill in your date of birth, you can go to{% endtrans %} {% trans trimmed %}
You have not filled in your date of birth.
As a result, you may not have access to all the products in the online shop.
To fill in your date of birth, you can go to
{% endtrans %}
<a href="{{ url("core:user_edit", user_id=request.user.id) }}"> <a href="{{ url("core:user_edit", user_id=request.user.id) }}">
{% trans %}this page{% endtrans %} {% trans %}this page{% endtrans %}
</a> </a>
@ -88,7 +92,43 @@
</span> </span>
</div> </div>
{% endif %} {% endif %}
<section>
<div class="category-header">
<h3 class="margin-bottom">{% trans %}Eurockéennes 2025 partnership{% endtrans %}</h3>
{% if user.is_subscribed %}
<a
title="Logiciel billetterie en ligne"
href="https://widget.weezevent.com/ticket/6ef65533-f5b0-4571-9d21-1f1bc63921f0?id_evenement=1211855&locale=fr-FR&code=34146"
class="weezevent-widget-integration"
target="_blank"
data-src="https://widget.weezevent.com/ticket/6ef65533-f5b0-4571-9d21-1f1bc63921f0?id_evenement=1211855&locale=fr-FR&code=34146"
data-width="650"
data-height="600"
data-resize="1"
data-nopb="0"
data-type="neo"
data-width_auto="1"
data-noscroll="0"
data-id="1211855">
Billetterie Weezevent
</a>
<script type="text/javascript" src="https://widget.weezevent.com/weez.js" async defer></script>
{% else %}
<p>
{%- trans trimmed %}
You must be subscribed to benefit from the partnership with the Eurockéennes.
{% endtrans -%}
</p>
<p>
{%- trans trimmed %}
This partnership offers a discount of up to 33%
on tickets for Friday, Saturday and Sunday,
as well as the 3-day package from Friday to Sunday.
{% endtrans -%}
</p>
{% endif %}
</div>
</section>
{% for priority_groups in products|groupby('order') %} {% for priority_groups in products|groupby('order') %}
{% for category, items in priority_groups.list|groupby('category') %} {% for category, items in priority_groups.list|groupby('category') %}
{% if items|count > 0 %} {% if items|count > 0 %}

View File

@ -9,7 +9,7 @@
{% endblock %} {% endblock %}
{% block additional_js %} {% block additional_js %}
<script src="{{ static('eboutic/js/makecommand.js') }}" defer></script> <script type="module" src="{{ static('bundled/eboutic/makecommand-index.ts') }}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -33,7 +33,7 @@
<td>{{ item.product_unit_price }} €</td> <td>{{ item.product_unit_price }} €</td>
</tr> </tr>
{% endfor %} {% endfor %}
<tbody> </tbody>
</table> </table>
<p> <p>
@ -56,7 +56,7 @@
<div <div
class="collapse" class="collapse"
:class="{'shadow': collapsed}" :class="{'shadow': collapsed}"
x-data="{collapsed: !billingInfoExist}" x-data="{collapsed: !{{ "true" if billing_infos else "false" }}}"
x-cloak x-cloak
> >
<div class="collapse-header clickable" @click="collapsed = !collapsed"> <div class="collapse-header clickable" @click="collapsed = !collapsed">
@ -70,7 +70,7 @@
<form <form
class="collapse-body" class="collapse-body"
id="billing_info_form" id="billing_info_form"
x-data="billing_infos" x-data="billing_infos({{ user.id }})"
x-show="collapsed" x-show="collapsed"
x-transition.scale.origin.top x-transition.scale.origin.top
@submit.prevent="await sendForm()" @submit.prevent="await sendForm()"
@ -79,7 +79,7 @@
{{ billing_form }} {{ billing_form }}
<br /> <br />
<div <div
x-show="[BillingInfoReqState.SUCCESS, BillingInfoReqState.FAILURE].includes(reqState)" x-show="[BillingInfoReqState.Success, BillingInfoReqState.Failure].includes(reqState)"
class="alert" class="alert"
:class="'alert-' + getAlertColor()" :class="'alert-' + getAlertColor()"
x-transition x-transition
@ -92,19 +92,20 @@
<input <input
type="submit" class="btn btn-blue clickable" type="submit" class="btn btn-blue clickable"
value="{% trans %}Validate{% endtrans %}" value="{% trans %}Validate{% endtrans %}"
:disabled="reqState === BillingInfoReqState.SENDING" :disabled="reqState === BillingInfoReqState.Sending"
> >
</form> </form>
</div> </div>
<br> <br>
{% if billing_infos_state == BillingInfoState.EMPTY %} {% if billing_infos_state == BillingInfoState.EMPTY %}
<div class="alert alert-yellow"> <div class="alert alert-yellow">
{% trans %}You must fill your billing infos if you want to pay with your credit {% trans trimmed %}
card{% endtrans %} You must fill your billing infos if you want to pay with your credit card
{% endtrans %}
</div> </div>
{% elif billing_infos_state == BillingInfoState.MISSING_PHONE_NUMBER %} {% elif billing_infos_state == BillingInfoState.MISSING_PHONE_NUMBER %}
<div class="alert alert-yellow"> <div class="alert alert-yellow">
{% trans %} {% trans trimmed %}
The Crédit Agricole changed its policy related to the billing The Crédit Agricole changed its policy related to the billing
information that must be provided in order to pay with a credit card. information that must be provided in order to pay with a credit card.
If you want to pay with your credit card, you must add a phone number If you want to pay with your credit card, you must add a phone number
@ -112,8 +113,14 @@
{% endtrans %} {% endtrans %}
</div> </div>
{% endif %} {% endif %}
<form method="post" action="{{ settings.SITH_EBOUTIC_ET_URL }}" name="bank-pay-form"> <form
<template x-data x-for="[key, value] in Object.entries($store.billing_inputs.data)"> method="post"
action="{{ settings.SITH_EBOUTIC_ET_URL }}"
name="bank-pay-form"
x-data="etransactionData(initialEtData)"
@billing-infos-filled.window="await fill()"
>
<template x-for="[key, value] in Object.entries(data)" :key="key">
<input type="hidden" :name="key" :value="value"> <input type="hidden" :name="key" :value="value">
</template> </template>
<input <input
@ -140,17 +147,11 @@
{% block script %} {% block script %}
<script> <script>
const billingInfoUrl = '{{ url("api:put_billing_info", user_id=request.user.id) }}'; {% if billing_infos -%}
const etDataUrl = '{{ url("api:etransaction_data") }}'; const initialEtData = {{ billing_infos|safe }}
const billingInfoExist = {{ "true" if billing_infos else "false" }}; {%- else -%}
const billingInfoSuccessMessage = "{% trans %}Billing info registration success{% endtrans %}"; const initialEtData = {}
const billingInfoFailureMessage = "{% trans %}Billing info registration failure{% endtrans %}"; {%- endif %}
{% if billing_infos %}
const etData = {{ billing_infos|safe }}
{% else %}
const etData = {}
{% endif %}
</script> </script>
{{ super() }} {{ super() }}
{% endblock %} {% endblock %}

View File

@ -26,7 +26,9 @@ from cryptography.hazmat.primitives.hashes import SHA1
from cryptography.hazmat.primitives.serialization import load_pem_public_key from cryptography.hazmat.primitives.serialization import load_pem_public_key
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import (
LoginRequiredMixin,
)
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.db import DatabaseError, transaction from django.db import DatabaseError, transaction
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse

View File

@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-04 10:35+0200\n" "POT-Creation-Date: 2025-04-06 15:54+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"
@ -60,11 +60,11 @@ msgstr "Retirer"
msgid "Action" msgid "Action"
msgstr "Action" msgstr "Action"
#: club/forms.py club/tests.py #: club/forms.py club/tests/test_mailing.py
msgid "This field is required" msgid "This field is required"
msgstr "Ce champ est obligatoire" msgstr "Ce champ est obligatoire"
#: club/forms.py club/tests.py #: club/forms.py club/tests/test_mailing.py
msgid "One of the selected users doesn't have an email address" msgid "One of the selected users doesn't have an email address"
msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email" msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email"
@ -72,7 +72,7 @@ msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email"
msgid "An action is required" msgid "An action is required"
msgstr "Une action est requise" msgstr "Une action est requise"
#: club/forms.py club/tests.py #: club/forms.py club/tests/test_mailing.py
msgid "You must specify at least an user or an email address" msgid "You must specify at least an user or an email address"
msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" msgstr "vous devez spécifier au moins un utilisateur ou une adresse email"
@ -239,7 +239,7 @@ msgstr "Utilisateur"
msgid "At least user or email is required" msgid "At least user or email is required"
msgstr "Au moins un utilisateur ou un email est nécessaire" msgstr "Au moins un utilisateur ou un email est nécessaire"
#: club/models.py club/tests.py #: club/models.py club/tests/test_mailing.py
msgid "This email is already suscribed in this mailing" msgid "This email is already suscribed in this mailing"
msgstr "Cet email est déjà abonné à cette mailing" msgstr "Cet email est déjà abonné à cette mailing"
@ -3792,6 +3792,27 @@ msgstr ""
msgid "this page" msgid "this page"
msgstr "cette page" msgstr "cette page"
#: eboutic/templates/eboutic/eboutic_main.jinja
msgid "Eurockéennes 2025 partnership"
msgstr "Partenariat Eurockéennes 2025"
#: eboutic/templates/eboutic/eboutic_main.jinja
msgid ""
"You must be subscribed to benefit from the partnership with the Eurockéennes."
msgstr ""
"Vous devez être cotisant pour bénéficier du partenariat avec les "
"Eurockéennes."
#: eboutic/templates/eboutic/eboutic_main.jinja
#, python-format
msgid ""
"This partnership offers a discount of up to 33%% on tickets for Friday, "
"Saturday and Sunday, as well as the 3-day package from Friday to Sunday."
msgstr ""
"Ce partenariat permet de profiter d'une réduction jusqu'à 33%% sur les "
"billets du vendredi, du samedi et du dimanche, ainsi qu'au forfait 3 jours, "
"du vendredi au dimanche."
#: eboutic/templates/eboutic/eboutic_main.jinja #: eboutic/templates/eboutic/eboutic_main.jinja
msgid "There are no items available for sale" msgid "There are no items available for sale"
msgstr "Aucun article n'est disponible à la vente" msgstr "Aucun article n'est disponible à la vente"
@ -3810,25 +3831,18 @@ msgstr "Informations de facturation"
#: eboutic/templates/eboutic/eboutic_makecommand.jinja #: eboutic/templates/eboutic/eboutic_makecommand.jinja
msgid "" msgid ""
"You must fill your billing infos if you want to pay with your credit\n" "You must fill your billing infos if you want to pay with your credit card"
" card"
msgstr "" msgstr ""
"Vous devez renseigner vos coordonnées de facturation si vous voulez payer " "Vous devez renseigner vos coordonnées de facturation si vous voulez payer "
"par carte bancaire" "par carte bancaire"
#: eboutic/templates/eboutic/eboutic_makecommand.jinja #: eboutic/templates/eboutic/eboutic_makecommand.jinja
msgid "" msgid ""
"\n" "The Crédit Agricole changed its policy related to the billing information "
" The Crédit Agricole changed its policy related to the " "that must be provided in order to pay with a credit card. If you want to pay "
"billing\n" "with your credit card, you must add a phone number to the data you already "
" information that must be provided in order to pay with a " "provided."
"credit card.\n"
" If you want to pay with your credit card, you must add a "
"phone number\n"
" to the data you already provided.\n"
" "
msgstr "" msgstr ""
"\n"
"Le Crédit Agricole a changé sa politique relative aux informations à " "Le Crédit Agricole a changé sa politique relative aux informations à "
"fournir pour effectuer un paiement par carte bancaire. De ce fait, si vous " "fournir pour effectuer un paiement par carte bancaire. De ce fait, si vous "
"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 "
@ -3855,14 +3869,6 @@ msgstr ""
msgid "Pay with Sith account" msgid "Pay with Sith account"
msgstr "Payer avec un compte AE" msgstr "Payer avec un compte AE"
#: eboutic/templates/eboutic/eboutic_makecommand.jinja
msgid "Billing info registration success"
msgstr "Informations de facturation enregistrées"
#: eboutic/templates/eboutic/eboutic_makecommand.jinja
msgid "Billing info registration failure"
msgstr "Echec de l'enregistrement des informations de facturation."
#: eboutic/templates/eboutic/eboutic_payment_result.jinja #: eboutic/templates/eboutic/eboutic_payment_result.jinja
msgid "Payment successful" msgid "Payment successful"
msgstr "Le paiement a été effectué" msgstr "Le paiement a été effectué"

View File

@ -7,7 +7,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-28 13:52+0100\n" "POT-Creation-Date: 2025-04-06 15:47+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"
@ -201,10 +201,18 @@ 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/eboutic/js/makecommand.js #: eboutic/static/bundled/eboutic/makecommand-index.ts
msgid "Incorrect value" msgid "Incorrect value"
msgstr "Valeur incorrecte" msgstr "Valeur incorrecte"
#: eboutic/static/bundled/eboutic/makecommand-index.ts
msgid "Billing info registration success"
msgstr "Informations de facturation enregistrées"
#: eboutic/static/bundled/eboutic/makecommand-index.ts
msgid "Billing info registration failure"
msgstr "Echec de l'enregistrement des informations de facturation."
#: 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"

238
package-lock.json generated
View File

@ -46,7 +46,7 @@
"@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-inject": "^5.0.5",
"@types/alpinejs": "^3.13.10", "@types/alpinejs": "^3.13.10",
"@types/jquery": "^3.5.31", "@types/jquery": "^3.5.31",
"vite": "^6.0.7", "vite": "^6.2.3",
"vite-bundle-visualizer": "^1.2.1", "vite-bundle-visualizer": "^1.2.1",
"vite-plugin-static-copy": "^2.1.0" "vite-plugin-static-copy": "^2.1.0"
} }
@ -1514,9 +1514,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.26.0", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
@ -1738,9 +1738,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -1755,9 +1755,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1772,9 +1772,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1789,9 +1789,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1806,9 +1806,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1823,9 +1823,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1840,9 +1840,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1857,9 +1857,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1874,9 +1874,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1891,9 +1891,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1908,9 +1908,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -1925,9 +1925,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -1942,9 +1942,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -1959,9 +1959,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -1976,9 +1976,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -1993,9 +1993,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -2010,9 +2010,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2027,9 +2027,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2044,9 +2044,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2061,9 +2061,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2078,9 +2078,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2095,9 +2095,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2112,9 +2112,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2129,9 +2129,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -2146,9 +2146,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3689,9 +3689,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.24.2", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@ -3702,31 +3702,31 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.24.2", "@esbuild/aix-ppc64": "0.25.1",
"@esbuild/android-arm": "0.24.2", "@esbuild/android-arm": "0.25.1",
"@esbuild/android-arm64": "0.24.2", "@esbuild/android-arm64": "0.25.1",
"@esbuild/android-x64": "0.24.2", "@esbuild/android-x64": "0.25.1",
"@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-arm64": "0.25.1",
"@esbuild/darwin-x64": "0.24.2", "@esbuild/darwin-x64": "0.25.1",
"@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-arm64": "0.25.1",
"@esbuild/freebsd-x64": "0.24.2", "@esbuild/freebsd-x64": "0.25.1",
"@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm": "0.25.1",
"@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-arm64": "0.25.1",
"@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-ia32": "0.25.1",
"@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-loong64": "0.25.1",
"@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-mips64el": "0.25.1",
"@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-ppc64": "0.25.1",
"@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-riscv64": "0.25.1",
"@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-s390x": "0.25.1",
"@esbuild/linux-x64": "0.24.2", "@esbuild/linux-x64": "0.25.1",
"@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-arm64": "0.25.1",
"@esbuild/netbsd-x64": "0.24.2", "@esbuild/netbsd-x64": "0.25.1",
"@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-arm64": "0.25.1",
"@esbuild/openbsd-x64": "0.24.2", "@esbuild/openbsd-x64": "0.25.1",
"@esbuild/sunos-x64": "0.24.2", "@esbuild/sunos-x64": "0.25.1",
"@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-arm64": "0.25.1",
"@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-ia32": "0.25.1",
"@esbuild/win32-x64": "0.24.2" "@esbuild/win32-x64": "0.25.1"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@ -4600,9 +4600,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.8", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -4933,9 +4933,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.1", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -5705,15 +5705,15 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.0.7", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
"integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.24.2", "esbuild": "^0.25.0",
"postcss": "^8.4.49", "postcss": "^8.5.3",
"rollup": "^4.23.0" "rollup": "^4.30.1"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"

View File

@ -31,7 +31,7 @@
"@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-inject": "^5.0.5",
"@types/alpinejs": "^3.13.10", "@types/alpinejs": "^3.13.10",
"@types/jquery": "^3.5.31", "@types/jquery": "^3.5.31",
"vite": "^6.0.7", "vite": "^6.2.3",
"vite-bundle-visualizer": "^1.2.1", "vite-bundle-visualizer": "^1.2.1",
"vite-plugin-static-copy": "^2.1.0" "vite-plugin-static-copy": "^2.1.0"
}, },