mirror of
https://github.com/ae-utbm/sith.git
synced 2025-04-09 07:26:24 +00:00
Merge pull request #1067 from ae-utbm/taiste
bug fixing, external calendar removal and eurockéennes partnership
This commit is contained in:
commit
d94d90357e
@ -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"]
|
||||
|
21
com/api.py
21
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())
|
||||
|
@ -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():
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
27
com/views.py
27
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):
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
{
|
||||
|
19
counter/migrations/0031_alter_counter_options.py
Normal file
19
counter/migrations/0031_alter_counter_options.py
Normal 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",
|
||||
},
|
||||
),
|
||||
]
|
@ -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(
|
||||
|
@ -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)
|
||||
]
|
||||
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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.
|
||||
|
||||
|
91
eboutic/static/bundled/eboutic/makecommand-index.ts
Normal file
91
eboutic/static/bundled/eboutic/makecommand-index.ts
Normal 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 "";
|
||||
},
|
||||
}));
|
||||
});
|
@ -158,4 +158,3 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 "";
|
||||
},
|
||||
}));
|
||||
});
|
@ -78,7 +78,11 @@
|
||||
{% if not request.user.date_of_birth %}
|
||||
<div class="alert alert-red" x-data="{show_alert: true}" x-show="show_alert" x-transition>
|
||||
<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) }}">
|
||||
{% trans %}this page{% endtrans %}
|
||||
</a>
|
||||
@ -88,7 +92,43 @@
|
||||
</span>
|
||||
</div>
|
||||
{% 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 category, items in priority_groups.list|groupby('category') %}
|
||||
{% if items|count > 0 %}
|
||||
|
@ -9,7 +9,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_js %}
|
||||
<script src="{{ static('eboutic/js/makecommand.js') }}" defer></script>
|
||||
<script type="module" src="{{ static('bundled/eboutic/makecommand-index.ts') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -33,124 +33,125 @@
|
||||
<td>{{ item.product_unit_price }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<strong>{% trans %}Basket amount: {% endtrans %}{{ "%0.2f"|format(basket.total) }} €</strong>
|
||||
<p>
|
||||
<strong>{% trans %}Basket amount: {% endtrans %}{{ "%0.2f"|format(basket.total) }} €</strong>
|
||||
|
||||
{% if customer_amount != None %}
|
||||
<br>
|
||||
{% trans %}Current account amount: {% endtrans %}
|
||||
<strong>{{ "%0.2f"|format(customer_amount) }} €</strong>
|
||||
|
||||
{% if not basket.contains_refilling_item %}
|
||||
<br>
|
||||
{% trans %}Remaining account amount: {% endtrans %}
|
||||
<strong>{{ "%0.2f"|format(customer_amount|float - basket.total) }} €</strong>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if customer_amount != None %}
|
||||
<br>
|
||||
{% if settings.SITH_EBOUTIC_CB_ENABLED %}
|
||||
<div
|
||||
class="collapse"
|
||||
:class="{'shadow': collapsed}"
|
||||
x-data="{collapsed: !billingInfoExist}"
|
||||
x-cloak
|
||||
>
|
||||
<div class="collapse-header clickable" @click="collapsed = !collapsed">
|
||||
<span class="collapse-header-text">
|
||||
{% trans %}Billing information{% endtrans %}
|
||||
</span>
|
||||
<span class="collapse-header-icon" :class="{'reverse': collapsed}">
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
<form
|
||||
class="collapse-body"
|
||||
id="billing_info_form"
|
||||
x-data="billing_infos"
|
||||
x-show="collapsed"
|
||||
x-transition.scale.origin.top
|
||||
@submit.prevent="await sendForm()"
|
||||
>
|
||||
{% csrf_token %}
|
||||
{{ billing_form }}
|
||||
<br />
|
||||
<div
|
||||
x-show="[BillingInfoReqState.SUCCESS, BillingInfoReqState.FAILURE].includes(reqState)"
|
||||
class="alert"
|
||||
:class="'alert-' + getAlertColor()"
|
||||
x-transition
|
||||
>
|
||||
<div class="alert-main" x-text="getAlertMessage()"></div>
|
||||
<div class="clickable" @click="reqState = null">
|
||||
<i class="fa fa-close"></i>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="submit" class="btn btn-blue clickable"
|
||||
value="{% trans %}Validate{% endtrans %}"
|
||||
:disabled="reqState === BillingInfoReqState.SENDING"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
{% trans %}Current account amount: {% endtrans %}
|
||||
<strong>{{ "%0.2f"|format(customer_amount) }} €</strong>
|
||||
|
||||
{% if not basket.contains_refilling_item %}
|
||||
<br>
|
||||
{% if billing_infos_state == BillingInfoState.EMPTY %}
|
||||
<div class="alert alert-yellow">
|
||||
{% trans %}You must fill your billing infos if you want to pay with your credit
|
||||
card{% endtrans %}
|
||||
</div>
|
||||
{% elif billing_infos_state == BillingInfoState.MISSING_PHONE_NUMBER %}
|
||||
<div class="alert alert-yellow">
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ settings.SITH_EBOUTIC_ET_URL }}" name="bank-pay-form">
|
||||
<template x-data x-for="[key, value] in Object.entries($store.billing_inputs.data)">
|
||||
<input type="hidden" :name="key" :value="value">
|
||||
</template>
|
||||
<input
|
||||
type="submit"
|
||||
id="bank-submit-button"
|
||||
{% if billing_infos_state != BillingInfoState.VALID %}disabled="disabled"{% endif %}
|
||||
value="{% trans %}Pay with credit card{% endtrans %}"
|
||||
/>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if basket.contains_refilling_item %}
|
||||
<p>{% trans %}AE account payment disabled because your basket contains refilling items.{% endtrans %}</p>
|
||||
{% elif basket.total > user.account_balance %}
|
||||
<p>{% trans %}AE account payment disabled because you do not have enough money remaining.{% endtrans %}</p>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url('eboutic:pay_with_sith') }}" name="sith-pay-form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="pay_with_sith_account">
|
||||
<input type="submit" value="{% trans %}Pay with Sith account{% endtrans %}"/>
|
||||
</form>
|
||||
{% trans %}Remaining account amount: {% endtrans %}
|
||||
<strong>{{ "%0.2f"|format(customer_amount|float - basket.total) }} €</strong>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<br>
|
||||
{% if settings.SITH_EBOUTIC_CB_ENABLED %}
|
||||
<div
|
||||
class="collapse"
|
||||
:class="{'shadow': collapsed}"
|
||||
x-data="{collapsed: !{{ "true" if billing_infos else "false" }}}"
|
||||
x-cloak
|
||||
>
|
||||
<div class="collapse-header clickable" @click="collapsed = !collapsed">
|
||||
<span class="collapse-header-text">
|
||||
{% trans %}Billing information{% endtrans %}
|
||||
</span>
|
||||
<span class="collapse-header-icon" :class="{'reverse': collapsed}">
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
<form
|
||||
class="collapse-body"
|
||||
id="billing_info_form"
|
||||
x-data="billing_infos({{ user.id }})"
|
||||
x-show="collapsed"
|
||||
x-transition.scale.origin.top
|
||||
@submit.prevent="await sendForm()"
|
||||
>
|
||||
{% csrf_token %}
|
||||
{{ billing_form }}
|
||||
<br />
|
||||
<div
|
||||
x-show="[BillingInfoReqState.Success, BillingInfoReqState.Failure].includes(reqState)"
|
||||
class="alert"
|
||||
:class="'alert-' + getAlertColor()"
|
||||
x-transition
|
||||
>
|
||||
<div class="alert-main" x-text="getAlertMessage()"></div>
|
||||
<div class="clickable" @click="reqState = null">
|
||||
<i class="fa fa-close"></i>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="submit" class="btn btn-blue clickable"
|
||||
value="{% trans %}Validate{% endtrans %}"
|
||||
:disabled="reqState === BillingInfoReqState.Sending"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
<br>
|
||||
{% if billing_infos_state == BillingInfoState.EMPTY %}
|
||||
<div class="alert alert-yellow">
|
||||
{% trans trimmed %}
|
||||
You must fill your billing infos if you want to pay with your credit card
|
||||
{% endtrans %}
|
||||
</div>
|
||||
{% elif billing_infos_state == BillingInfoState.MISSING_PHONE_NUMBER %}
|
||||
<div class="alert alert-yellow">
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form
|
||||
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">
|
||||
</template>
|
||||
<input
|
||||
type="submit"
|
||||
id="bank-submit-button"
|
||||
{% if billing_infos_state != BillingInfoState.VALID %}disabled="disabled"{% endif %}
|
||||
value="{% trans %}Pay with credit card{% endtrans %}"
|
||||
/>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if basket.contains_refilling_item %}
|
||||
<p>{% trans %}AE account payment disabled because your basket contains refilling items.{% endtrans %}</p>
|
||||
{% elif basket.total > user.account_balance %}
|
||||
<p>{% trans %}AE account payment disabled because you do not have enough money remaining.{% endtrans %}</p>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url('eboutic:pay_with_sith') }}" name="sith-pay-form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="pay_with_sith_account">
|
||||
<input type="submit" value="{% trans %}Pay with Sith account{% endtrans %}"/>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
const billingInfoUrl = '{{ url("api:put_billing_info", user_id=request.user.id) }}';
|
||||
const etDataUrl = '{{ url("api:etransaction_data") }}';
|
||||
const billingInfoExist = {{ "true" if billing_infos else "false" }};
|
||||
const billingInfoSuccessMessage = "{% trans %}Billing info registration success{% endtrans %}";
|
||||
const billingInfoFailureMessage = "{% trans %}Billing info registration failure{% endtrans %}";
|
||||
|
||||
{% if billing_infos %}
|
||||
const etData = {{ billing_infos|safe }}
|
||||
{% else %}
|
||||
const etData = {}
|
||||
{% endif %}
|
||||
{% if billing_infos -%}
|
||||
const initialEtData = {{ billing_infos|safe }}
|
||||
{%- else -%}
|
||||
const initialEtData = {}
|
||||
{%- endif %}
|
||||
</script>
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
@ -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
|
||||
|
@ -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 <thomas.girod@utbm.fr\n"
|
||||
"Language-Team: AE info <ae.info@utbm.fr>\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é"
|
||||
|
@ -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 <antoine@bartuccio.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"
|
||||
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"
|
||||
|
238
package-lock.json
generated
238
package-lock.json
generated
@ -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"
|
||||
|
@ -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"
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user