diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d72985b..b334a7c0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: # Run the formatter. - id: ruff-format - 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: - id: biome-check additional_dependencies: ["@biomejs/biome@1.9.4"] diff --git a/com/api.py b/com/api.py index 6de78a3c..9dd70606 100644 --- a/com/api.py +++ b/com/api.py @@ -1,8 +1,6 @@ -from pathlib import Path from typing import Literal -from django.conf import settings -from django.http import Http404, HttpResponse +from django.http import HttpResponse from ninja import Query from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra.pagination import PageNumberPaginationExtra @@ -18,23 +16,6 @@ from core.views.files import send_raw_file @api_controller("/calendar") 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") def calendar_internal(self): return send_raw_file(IcsCalendar.get_internal()) diff --git a/com/ics_calendar.py b/com/ics_calendar.py index 1c95a2b3..e5324c8a 100644 --- a/com/ics_calendar.py +++ b/com/ics_calendar.py @@ -1,8 +1,6 @@ -from datetime import datetime, timedelta from pathlib import Path from typing import final -import requests from dateutil.relativedelta import relativedelta from django.conf import settings from django.db.models import F, QuerySet @@ -19,35 +17,8 @@ from core.models import User @final class IcsCalendar: _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" - _EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.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 def get_internal(cls) -> Path: if not cls._INTERNAL_CALENDAR.exists(): diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 0b4976b0..d8fc79d7 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -8,7 +8,6 @@ import dayGridPlugin from "@fullcalendar/daygrid"; import iCalendarPlugin from "@fullcalendar/icalendar"; import listPlugin from "@fullcalendar/list"; import { - calendarCalendarExternal, calendarCalendarInternal, calendarCalendarUnpublished, newsDeleteNews, @@ -151,11 +150,6 @@ export class IcsCalendar extends inheritHtmlElement("div") { format: "ics", className: "internal", }, - { - url: `${await makeUrl(calendarCalendarExternal)}${cacheInvalidate}`, - format: "ics", - className: "external", - }, { url: `${await makeUrl(calendarCalendarUnpublished)}${cacheInvalidate}`, format: "ics", @@ -224,9 +218,6 @@ export class IcsCalendar extends inheritHtmlElement("div") { }; const makePopupTools = (event: EventImpl) => { - if (event.source.internalEventSource.ui.classNames.includes("external")) { - return null; - } if (!(this.canDelete || this.canModerate)) { return null; } diff --git a/com/tests/test_api.py b/com/tests/test_api.py index 7c3bcb7b..bfb7bb94 100644 --- a/com/tests/test_api.py +++ b/com/tests/test_api.py @@ -1,8 +1,6 @@ from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path -from typing import Callable -from unittest.mock import MagicMock, patch from urllib.parse import quote import pytest @@ -11,7 +9,6 @@ from django.contrib.auth.models import Permission from django.http import HttpResponse from django.test import Client, TestCase from django.urls import reverse -from django.utils import timezone from django.utils.timezone import now from model_bakery import baker, seq 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 class TestInternalCalendar: @pytest.fixture(autouse=True) diff --git a/com/tests/test_views.py b/com/tests/test_views.py index 03d28adc..607d4b3f 100644 --- a/com/tests/test_views.py +++ b/com/tests/test_views.py @@ -19,7 +19,7 @@ import pytest from django.conf import settings from django.contrib.sites.models import Site 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.utils import html from django.utils.timezone import localtime, now @@ -323,7 +323,7 @@ class TestNewsCreation(TestCase): @pytest.mark.django_db -def test_feed(client): +def test_feed(client: Client): """Smoke test that checks that the atom feed is working""" Site.objects.clear_cache() with assertNumQueries(2): @@ -332,3 +332,22 @@ def test_feed(client): resp = client.get(reverse("com:news_feed")) assert resp.status_code == 200 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 diff --git a/com/views.py b/com/views.py index f6e12fd2..024cb781 100644 --- a/com/views.py +++ b/com/views.py @@ -61,8 +61,7 @@ sith = Sith.objects.first class ComTabsMixin(TabedViewMixin): - def get_tabs_title(self): - return _("Communication administration") + tabs_title = _("Communication administration") def get_list_of_tabs(self): return [ @@ -559,7 +558,11 @@ class MailingModerateView(View): raise PermissionDenied -class PosterListBaseView(ListView): +class PosterAdminViewMixin(IsComAdminMixin, ComTabsMixin): + current_tab = "posters" + + +class PosterListBaseView(PosterAdminViewMixin, ListView): """List communication posters.""" current_tab = "posters" @@ -586,7 +589,7 @@ class PosterListBaseView(ListView): return kwargs -class PosterCreateBaseView(CreateView): +class PosterCreateBaseView(PosterAdminViewMixin, CreateView): """Create communication poster.""" current_tab = "posters" @@ -618,7 +621,7 @@ class PosterCreateBaseView(CreateView): return super().form_valid(form) -class PosterEditBaseView(UpdateView): +class PosterEditBaseView(PosterAdminViewMixin, UpdateView): """Edit communication poster.""" pk_url_kwarg = "poster_id" @@ -664,7 +667,7 @@ class PosterEditBaseView(UpdateView): return super().form_valid(form) -class PosterDeleteBaseView(DeleteView): +class PosterDeleteBaseView(PosterAdminViewMixin, DeleteView): """Edit communication poster.""" pk_url_kwarg = "poster_id" @@ -681,7 +684,7 @@ class PosterDeleteBaseView(DeleteView): return super().dispatch(request, *args, **kwargs) -class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView): +class PosterListView(PosterListBaseView): """List communication posters.""" def get_context_data(self, **kwargs): @@ -690,7 +693,7 @@ class PosterListView(IsComAdminMixin, ComTabsMixin, PosterListBaseView): return kwargs -class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView): +class PosterCreateView(PosterCreateBaseView): """Create communication poster.""" success_url = reverse_lazy("com:poster_list") @@ -701,7 +704,7 @@ class PosterCreateView(IsComAdminMixin, ComTabsMixin, PosterCreateBaseView): return kwargs -class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView): +class PosterEditView(PosterEditBaseView): """Edit communication poster.""" success_url = reverse_lazy("com:poster_list") @@ -712,13 +715,13 @@ class PosterEditView(IsComAdminMixin, ComTabsMixin, PosterEditBaseView): return kwargs -class PosterDeleteView(IsComAdminMixin, ComTabsMixin, PosterDeleteBaseView): +class PosterDeleteView(PosterDeleteBaseView): """Delete communication poster.""" success_url = reverse_lazy("com:poster_list") -class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView): +class PosterModerateListView(PosterAdminViewMixin, ListView): """Moderate list communication poster.""" current_tab = "posters" @@ -732,7 +735,7 @@ class PosterModerateListView(IsComAdminMixin, ComTabsMixin, ListView): return kwargs -class PosterModerateView(IsComAdminMixin, ComTabsMixin, View): +class PosterModerateView(PosterAdminViewMixin, View): """Moderate communication poster.""" def get(self, request, *args, **kwargs): diff --git a/core/tests/test_user.py b/core/tests/test_user.py index 133f26a5..d14bfc3c 100644 --- a/core/tests/test_user.py +++ b/core/tests/test_user.py @@ -4,9 +4,10 @@ import pytest from django.conf import settings from django.contrib import auth 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.utils.timezone import now +from django.views.generic import DetailView from model_bakery import baker, seq from model_bakery.recipe import Recipe, foreign_key from pytest_django.asserts import assertRedirects @@ -18,6 +19,7 @@ from core.baker_recipes import ( very_old_subscriber_user, ) from core.models import Group, User +from core.views import UserTabsMixin from counter.models import Counter, Refilling, Selling from eboutic.models import Invoice, InvoiceItem @@ -229,3 +231,88 @@ def test_logout(client: Client): res = client.post(reverse("core:logout")) assertRedirects(res, reverse("core:login")) 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 diff --git a/core/views/user.py b/core/views/user.py index a9ce811f..cd27cbba 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -242,7 +242,10 @@ class UserTabsMixin(TabedViewMixin): if ( hasattr(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( { diff --git a/counter/migrations/0031_alter_counter_options.py b/counter/migrations/0031_alter_counter_options.py new file mode 100644 index 00000000..c6d68529 --- /dev/null +++ b/counter/migrations/0031_alter_counter_options.py @@ -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", + }, + ), + ] diff --git a/counter/models.py b/counter/models.py index ee6088d9..8581b19d 100644 --- a/counter/models.py +++ b/counter/models.py @@ -526,6 +526,7 @@ class Counter(models.Model): class Meta: verbose_name = _("counter") + permissions = [("view_counter_stats", "Can view counter stats")] def __str__(self): return self.name @@ -598,13 +599,12 @@ class Counter(models.Model): - the promo of the barman - the total number of office hours the barman did attend """ + name_expr = Concat(F("user__first_name"), Value(" "), F("user__last_name")) return ( self.permanencies.exclude(end=None) .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") .annotate(perm_sum=Sum(F("end") - F("start"))) .exclude(perm_sum=None) @@ -628,18 +628,17 @@ class Counter(models.Model): since = get_start_of_semester() if isinstance(since, date): 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 ( self.sellings.filter(date__gte=since) .annotate( - name=Concat( - F("customer__user__first_name"), - Value(" "), - F("customer__user__last_name"), - ) + name=name_expr, + nickname=F("customer__user__nick_name"), + promo=F("customer__user__promo"), + 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") .annotate( selling_sum=Sum( diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index f0773897..e3b885b7 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -18,7 +18,7 @@ from decimal import Decimal import pytest 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.http import HttpResponse from django.shortcuts import resolve_url @@ -28,9 +28,10 @@ from django.utils import timezone from django.utils.timezone import localdate, now from freezegun import freeze_time from model_bakery import baker +from model_bakery.recipe import Recipe 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.models import BanGroup, User from counter.baker_recipes import product_recipe, sale_recipe @@ -572,121 +573,86 @@ class TestCounterClick(TestFullClickBase): class TestCounterStats(TestCase): @classmethod def setUpTestData(cls): - cls.counter = Counter.objects.get(id=2) - cls.krophil = User.objects.get(username="krophil") - cls.skia = User.objects.get(username="skia") - cls.sli = User.objects.get(username="sli") - 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, + cls.users = subscriber_user.make(_quantity=4) + product = product_recipe.make(selling_price=1) + cls.counter = baker.make( + Counter, type=["BAR"], sellers=cls.users[:4], products=[product] ) - # total of skia : 16 days, 2 hours, 35 minutes and 54 seconds - Permanency.objects.create( - user=cls.skia, start=now, end=now + timedelta(hours=1), counter=cls.counter - ) - Permanency.objects.create( - user=cls.skia, - start=now + timedelta(days=4, hours=1), - end=now + timedelta(days=20, hours=2, minutes=35, seconds=54), - counter=cls.counter, - ) + _now = timezone.now() + permanence_recipe = Recipe(Permanency, counter=cls.counter) + perms = [ + *[ # total of user 0 : 5 hours + permanence_recipe.prepare(user=cls.users[0], start=start, end=end) + for start, end in [ + (_now, _now + timedelta(hours=1)), + (_now + timedelta(hours=4), _now + timedelta(hours=6)), + (_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) - Permanency.objects.create( - user=cls.root, - 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), + _sale_recipe = Recipe( + Selling, + club=cls.counter.club, counter=cls.counter, + product=product, 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] - sli_customer = Customer.get_or_create(cls.sli)[0] - skia_customer = Customer.get_or_create(cls.skia)[0] - root_customer = Customer.get_or_create(cls.root)[0] - - # 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_not_authenticated_access_fail(self): + url = reverse("counter:stats", args=[self.counter.id]) + response = self.client.get(url) + assertRedirects(response, reverse("core:login") + f"?next={url}") 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])) 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): """Test the result of the Counter.get_total_sales() method.""" assert self.counter.get_total_sales() == 3102 def test_top_barmen(self): """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 = [ timedelta(days=16, hours=2, minutes=35, seconds=54), timedelta(hours=21), @@ -700,12 +666,12 @@ class TestCounterStats(TestCase): "nickname": user.nick_name, "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): """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] assert list(self.counter.get_top_customers()) == [ { @@ -715,7 +681,7 @@ class TestCounterStats(TestCase): "nickname": user.nick_name, "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) ] diff --git a/counter/views/admin.py b/counter/views/admin.py index f7d4a66b..ddd7a40e 100644 --- a/counter/views/admin.py +++ b/counter/views/admin.py @@ -27,7 +27,7 @@ from django.utils.translation import gettext as _ from django.views.generic import DetailView, ListView, TemplateView 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 counter.forms import ( CloseCustomerAccountForm, @@ -274,12 +274,13 @@ class SellingDeleteView(DeleteView): raise PermissionDenied -class CounterStatView(DetailView, CounterAdminMixin): +class CounterStatView(PermissionRequiredMixin, DetailView): """Show the bar stats.""" model = Counter pk_url_kwarg = "counter_id" template_name = "counter/stats.jinja" + permission_required = "counter.view_counter_stats" def get_context_data(self, **kwargs): """Add stats to the context.""" @@ -301,18 +302,6 @@ class CounterStatView(DetailView, CounterAdminMixin): ) 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): """List of refillings on a counter.""" diff --git a/eboutic/api.py b/eboutic/api.py index 0054c02a..797adf20 100644 --- a/eboutic/api.py +++ b/eboutic/api.py @@ -26,7 +26,7 @@ class EtransactionInfoController(ControllerBase): 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): """Generate the data to pay an eboutic command with paybox. diff --git a/eboutic/static/bundled/eboutic/makecommand-index.ts b/eboutic/static/bundled/eboutic/makecommand-index.ts new file mode 100644 index 00000000..c1e4b52f --- /dev/null +++ b/eboutic/static/bundled/eboutic/makecommand-index.ts @@ -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 ""; + }, + })); +}); diff --git a/eboutic/static/eboutic/css/eboutic.css b/eboutic/static/eboutic/css/eboutic.css index abf121d0..6ca6beef 100644 --- a/eboutic/static/eboutic/css/eboutic.css +++ b/eboutic/static/eboutic/css/eboutic.css @@ -158,4 +158,3 @@ flex-direction: column; } } - diff --git a/eboutic/static/eboutic/js/makecommand.js b/eboutic/static/eboutic/js/makecommand.js deleted file mode 100644 index 3ccb4280..00000000 --- a/eboutic/static/eboutic/js/makecommand.js +++ /dev/null @@ -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 ""; - }, - })); -}); diff --git a/eboutic/templates/eboutic/eboutic_main.jinja b/eboutic/templates/eboutic/eboutic_main.jinja index 3e6049f2..ee563dd0 100644 --- a/eboutic/templates/eboutic/eboutic_main.jinja +++ b/eboutic/templates/eboutic/eboutic_main.jinja @@ -78,7 +78,11 @@ {% if not request.user.date_of_birth %}
- {% 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 %} {% trans %}this page{% endtrans %} @@ -88,7 +92,43 @@
{% endif %} - +
+
+

{% trans %}Eurockéennes 2025 partnership{% endtrans %}

+ {% if user.is_subscribed %} + + Billetterie Weezevent + + + {% else %} +

+ {%- trans trimmed %} + You must be subscribed to benefit from the partnership with the Eurockéennes. + {% endtrans -%} +

+

+ {%- 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 -%} +

+ {% endif %} +
+
{% for priority_groups in products|groupby('order') %} {% for category, items in priority_groups.list|groupby('category') %} {% if items|count > 0 %} diff --git a/eboutic/templates/eboutic/eboutic_makecommand.jinja b/eboutic/templates/eboutic/eboutic_makecommand.jinja index e18514e9..62053af5 100644 --- a/eboutic/templates/eboutic/eboutic_makecommand.jinja +++ b/eboutic/templates/eboutic/eboutic_makecommand.jinja @@ -9,7 +9,7 @@ {% endblock %} {% block additional_js %} - + {% endblock %} {% block content %} @@ -33,124 +33,125 @@ {{ item.product_unit_price }} € {% endfor %} - - + + -

- {% trans %}Basket amount: {% endtrans %}{{ "%0.2f"|format(basket.total) }} € +

+ {% trans %}Basket amount: {% endtrans %}{{ "%0.2f"|format(basket.total) }} € - {% if customer_amount != None %} -
- {% trans %}Current account amount: {% endtrans %} - {{ "%0.2f"|format(customer_amount) }} € - - {% if not basket.contains_refilling_item %} -
- {% trans %}Remaining account amount: {% endtrans %} - {{ "%0.2f"|format(customer_amount|float - basket.total) }} € - {% endif %} - {% endif %} -

+ {% if customer_amount != None %}
- {% if settings.SITH_EBOUTIC_CB_ENABLED %} -
-
- - {% trans %}Billing information{% endtrans %} - - - - -
-
- {% csrf_token %} - {{ billing_form }} -
-
-
-
- -
-
- -
-
+ {% trans %}Current account amount: {% endtrans %} + {{ "%0.2f"|format(customer_amount) }} € + + {% if not basket.contains_refilling_item %}
- {% if billing_infos_state == BillingInfoState.EMPTY %} -
- {% trans %}You must fill your billing infos if you want to pay with your credit - card{% endtrans %} -
- {% elif billing_infos_state == BillingInfoState.MISSING_PHONE_NUMBER %} -
- {% trans %} - The Crédit Agricole changed its policy related to the billing - 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 - to the data you already provided. - {% endtrans %} -
- {% endif %} -
- - -
- {% endif %} - {% if basket.contains_refilling_item %} -

{% trans %}AE account payment disabled because your basket contains refilling items.{% endtrans %}

- {% elif basket.total > user.account_balance %} -

{% trans %}AE account payment disabled because you do not have enough money remaining.{% endtrans %}

- {% else %} -
- {% csrf_token %} - - -
+ {% trans %}Remaining account amount: {% endtrans %} + {{ "%0.2f"|format(customer_amount|float - basket.total) }} € {% endif %} + {% endif %} +

+
+ {% if settings.SITH_EBOUTIC_CB_ENABLED %} +
+
+ + {% trans %}Billing information{% endtrans %} + + + + +
+
+ {% csrf_token %} + {{ billing_form }} +
+
+
+
+ +
+
+ +
+
+ {% if billing_infos_state == BillingInfoState.EMPTY %} +
+ {% trans trimmed %} + You must fill your billing infos if you want to pay with your credit card + {% endtrans %} +
+ {% elif billing_infos_state == BillingInfoState.MISSING_PHONE_NUMBER %} +
+ {% trans trimmed %} + The Crédit Agricole changed its policy related to the billing + 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 + to the data you already provided. + {% endtrans %} +
+ {% endif %} +
+ + +
+ {% endif %} + {% if basket.contains_refilling_item %} +

{% trans %}AE account payment disabled because your basket contains refilling items.{% endtrans %}

+ {% elif basket.total > user.account_balance %} +

{% trans %}AE account payment disabled because you do not have enough money remaining.{% endtrans %}

+ {% else %} +
+ {% csrf_token %} + + +
+ {% endif %} + {% endblock %} {% block script %} {{ super() }} {% endblock %} diff --git a/eboutic/views.py b/eboutic/views.py index 14f129fd..dfa79c22 100644 --- a/eboutic/views.py +++ b/eboutic/views.py @@ -26,7 +26,9 @@ from cryptography.hazmat.primitives.hashes import SHA1 from cryptography.hazmat.primitives.serialization import load_pem_public_key from django.conf import settings 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.db import DatabaseError, transaction from django.http import HttpRequest, HttpResponse diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 25d03fa4..1e944299 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "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" "Last-Translator: Maréchal \n" @@ -60,11 +60,11 @@ msgstr "Retirer" msgid "Action" msgstr "Action" -#: club/forms.py club/tests.py +#: club/forms.py club/tests/test_mailing.py msgid "This field is required" 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" 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" 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" 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" 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" msgstr "Cet email est déjà abonné à cette mailing" @@ -3792,6 +3792,27 @@ msgstr "" msgid "this 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 msgid "There are no items available for sale" msgstr "Aucun article n'est disponible à la vente" @@ -3810,25 +3831,18 @@ msgstr "Informations de facturation" #: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "" -"You must fill your billing infos if you want to pay with your credit\n" -" card" +"You must fill your billing infos if you want to pay with your credit card" msgstr "" "Vous devez renseigner vos coordonnées de facturation si vous voulez payer " "par carte bancaire" #: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "" -"\n" -" The Crédit Agricole changed its policy related to the " -"billing\n" -" information that must be provided in order to pay with a " -"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" -" " +"The Crédit Agricole changed its policy related to the billing 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 to the data you already " +"provided." msgstr "" -"\n" "Le Crédit Agricole a changé sa politique relative aux informations à " "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 " @@ -3855,14 +3869,6 @@ msgstr "" msgid "Pay with Sith account" 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 msgid "Payment successful" msgstr "Le paiement a été effectué" diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 9b967354..7952baa4 100644 --- a/locale/fr/LC_MESSAGES/djangojs.po +++ b/locale/fr/LC_MESSAGES/djangojs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "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" "Last-Translator: Sli \n" "Language-Team: AE info \n" @@ -201,10 +201,18 @@ msgstr "Types de produits réordonnés !" msgid "Product type reorganisation failed with status 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" 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 msgid "pictures.%(extension)s" msgstr "photos.%(extension)s" diff --git a/package-lock.json b/package-lock.json index d6d496e8..dfa465c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "@rollup/plugin-inject": "^5.0.5", "@types/alpinejs": "^3.13.10", "@types/jquery": "^3.5.31", - "vite": "^6.0.7", + "vite": "^6.2.3", "vite-bundle-visualizer": "^1.2.1", "vite-plugin-static-copy": "^2.1.0" } @@ -1514,9 +1514,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1738,9 +1738,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", "cpu": [ "ppc64" ], @@ -1755,9 +1755,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", "cpu": [ "arm" ], @@ -1772,9 +1772,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", "cpu": [ "arm64" ], @@ -1789,9 +1789,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", "cpu": [ "x64" ], @@ -1806,9 +1806,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", "cpu": [ "arm64" ], @@ -1823,9 +1823,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", "cpu": [ "x64" ], @@ -1840,9 +1840,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", "cpu": [ "arm64" ], @@ -1857,9 +1857,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", "cpu": [ "x64" ], @@ -1874,9 +1874,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", "cpu": [ "arm" ], @@ -1891,9 +1891,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", "cpu": [ "arm64" ], @@ -1908,9 +1908,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", "cpu": [ "ia32" ], @@ -1925,9 +1925,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", "cpu": [ "loong64" ], @@ -1942,9 +1942,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", "cpu": [ "mips64el" ], @@ -1959,9 +1959,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", "cpu": [ "ppc64" ], @@ -1976,9 +1976,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", "cpu": [ "riscv64" ], @@ -1993,9 +1993,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", "cpu": [ "s390x" ], @@ -2010,9 +2010,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", "cpu": [ "x64" ], @@ -2027,9 +2027,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", "cpu": [ "arm64" ], @@ -2044,9 +2044,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", "cpu": [ "x64" ], @@ -2061,9 +2061,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", "cpu": [ "arm64" ], @@ -2078,9 +2078,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", "cpu": [ "x64" ], @@ -2095,9 +2095,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", "cpu": [ "x64" ], @@ -2112,9 +2112,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", "cpu": [ "arm64" ], @@ -2129,9 +2129,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", "cpu": [ "ia32" ], @@ -2146,9 +2146,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", "cpu": [ "x64" ], @@ -3689,9 +3689,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3702,31 +3702,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" } }, "node_modules/escalade": { @@ -4600,9 +4600,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -4933,9 +4933,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -5705,15 +5705,15 @@ } }, "node_modules/vite": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", - "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", + "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" }, "bin": { "vite": "bin/vite.js" diff --git a/package.json b/package.json index 83b65145..d5a23a86 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@rollup/plugin-inject": "^5.0.5", "@types/alpinejs": "^3.13.10", "@types/jquery": "^3.5.31", - "vite": "^6.0.7", + "vite": "^6.2.3", "vite-bundle-visualizer": "^1.2.1", "vite-plugin-static-copy": "^2.1.0" },