mirror of
https://github.com/ae-utbm/sith.git
synced 2026-03-13 15:15:03 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d374ea9651 | ||
|
|
10a4e71b7a | ||
|
|
00acda7ba3 | ||
|
|
1686a9da87 | ||
|
|
83255945c4 | ||
|
|
b4a6b6961b | ||
|
|
0f0702825e | ||
|
|
b74b1ac691 | ||
|
|
33d4a99a2c | ||
|
|
c154b311c3 | ||
|
|
fb8da93c68 | ||
|
|
4f84ec09d7 | ||
|
|
7e649b40c5 | ||
|
|
78c373f84e | ||
|
|
a7c8b318bd | ||
|
|
1701ab5f33 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,6 +24,9 @@ node_modules/
|
|||||||
# compiled documentation
|
# compiled documentation
|
||||||
site/
|
site/
|
||||||
|
|
||||||
|
# rollup-bundle-visualizer report
|
||||||
|
.bundle-size-report.html
|
||||||
|
|
||||||
### Redis ###
|
### Redis ###
|
||||||
|
|
||||||
# Ignore redis binary dump (dump.rdb) files
|
# Ignore redis binary dump (dump.rdb) files
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.15.0
|
rev: v0.15.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check # just check the code, and print the errors
|
- id: ruff-check # just check the code, and print the errors
|
||||||
- id: ruff-check # actually fix the fixable errors, but print nothing
|
- id: ruff-check # actually fix the fixable errors, but print nothing
|
||||||
@@ -12,7 +12,7 @@ repos:
|
|||||||
rev: v0.6.1
|
rev: v0.6.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: biome-check
|
- id: biome-check
|
||||||
additional_dependencies: ["@biomejs/biome@2.3.14"]
|
additional_dependencies: ["@biomejs/biome@2.4.6"]
|
||||||
- repo: https://github.com/rtts/djhtml
|
- repo: https://github.com/rtts/djhtml
|
||||||
rev: 3.0.10
|
rev: 3.0.10
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": false,
|
"ignoreUnknown": false,
|
||||||
"includes": ["**/static/**"]
|
"includes": ["**/static/**", "vite.config.mts"]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export class NfcInput extends inheritHtmlElement("input") {
|
|||||||
window.alert(gettext("Unsupported NFC card"));
|
window.alert(gettext("Unsupported NFC card"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: browser API
|
|
||||||
ndef.addEventListener("reading", (event: NDEFReadingEvent) => {
|
ndef.addEventListener("reading", (event: NDEFReadingEvent) => {
|
||||||
this.removeAttribute("scan");
|
this.removeAttribute("scan");
|
||||||
this.node.value = event.serialNumber.replace(/:/g, "").toUpperCase();
|
this.node.value = event.serialNumber.replace(/:/g, "").toUpperCase();
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ blockquote:before,
|
|||||||
blockquote:after,
|
blockquote:after,
|
||||||
q:before,
|
q:before,
|
||||||
q:after {
|
q:after {
|
||||||
content: "";
|
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from datetime import date, datetime, timezone
|
|||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator
|
from django.core.validators import MaxValueValidator
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.forms import BaseModelFormSet
|
from django.forms import BaseModelFormSet
|
||||||
@@ -15,7 +16,7 @@ from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
|||||||
|
|
||||||
from club.models import Club
|
from club.models import Club
|
||||||
from club.widgets.ajax_select import AutoCompleteSelectClub
|
from club.widgets.ajax_select import AutoCompleteSelectClub
|
||||||
from core.models import User
|
from core.models import User, UserQuerySet
|
||||||
from core.views.forms import (
|
from core.views.forms import (
|
||||||
FutureDateTimeField,
|
FutureDateTimeField,
|
||||||
NFCTextInput,
|
NFCTextInput,
|
||||||
@@ -32,6 +33,7 @@ from core.views.widgets.ajax_select import (
|
|||||||
from counter.models import (
|
from counter.models import (
|
||||||
BillingInfo,
|
BillingInfo,
|
||||||
Counter,
|
Counter,
|
||||||
|
CounterSellers,
|
||||||
Customer,
|
Customer,
|
||||||
Eticket,
|
Eticket,
|
||||||
InvoiceCall,
|
InvoiceCall,
|
||||||
@@ -170,14 +172,39 @@ class RefillForm(forms.ModelForm):
|
|||||||
class CounterEditForm(forms.ModelForm):
|
class CounterEditForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Counter
|
model = Counter
|
||||||
fields = ["sellers", "products"]
|
fields = ["products"]
|
||||||
widgets = {"sellers": AutoCompleteSelectMultipleUser}
|
|
||||||
|
sellers_regular = forms.ModelMultipleChoiceField(
|
||||||
|
label=_("Regular barmen"),
|
||||||
|
help_text=_(
|
||||||
|
"Barmen having regular permanences "
|
||||||
|
"or frequently giving a hand throughout the semester."
|
||||||
|
),
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
widget=AutoCompleteSelectMultipleUser,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
sellers_temporary = forms.ModelMultipleChoiceField(
|
||||||
|
label=_("Temporary barmen"),
|
||||||
|
help_text=_(
|
||||||
|
"Barmen who will be there only for a limited period (e.g. for one evening)"
|
||||||
|
),
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
widget=AutoCompleteSelectMultipleUser,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
field_order = ["sellers_regular", "sellers_temporary", "products"]
|
||||||
|
|
||||||
def __init__(self, *args, user: User, instance: Counter, **kwargs):
|
def __init__(self, *args, user: User, instance: Counter, **kwargs):
|
||||||
super().__init__(*args, instance=instance, **kwargs)
|
super().__init__(*args, instance=instance, **kwargs)
|
||||||
|
# if the user is an admin, he will have access to all products,
|
||||||
|
# else only to active products owned by the counter's club
|
||||||
|
# or already on the counter
|
||||||
if user.has_perm("counter.change_counter"):
|
if user.has_perm("counter.change_counter"):
|
||||||
self.fields["products"].widget = AutoCompleteSelectMultipleProduct()
|
self.fields["products"].widget = AutoCompleteSelectMultipleProduct()
|
||||||
else:
|
else:
|
||||||
|
# updating the queryset of the field also updates the choices of
|
||||||
|
# the widget, so it's important to set the queryset after the widget
|
||||||
self.fields["products"].widget = AutoCompleteSelectMultiple()
|
self.fields["products"].widget = AutoCompleteSelectMultiple()
|
||||||
self.fields["products"].queryset = Product.objects.filter(
|
self.fields["products"].queryset = Product.objects.filter(
|
||||||
Q(club_id=instance.club_id) | Q(counters=instance), archived=False
|
Q(club_id=instance.club_id) | Q(counters=instance), archived=False
|
||||||
@@ -186,6 +213,61 @@ class CounterEditForm(forms.ModelForm):
|
|||||||
"If you want to add a product that is not owned by "
|
"If you want to add a product that is not owned by "
|
||||||
"your club to this counter, you should ask an admin."
|
"your club to this counter, you should ask an admin."
|
||||||
)
|
)
|
||||||
|
self.fields["sellers_regular"].initial = self.instance.sellers.filter(
|
||||||
|
countersellers__is_regular=True
|
||||||
|
).all()
|
||||||
|
self.fields["sellers_temporary"].initial = self.instance.sellers.filter(
|
||||||
|
countersellers__is_regular=False
|
||||||
|
).all()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
regular: UserQuerySet = self.cleaned_data["sellers_regular"]
|
||||||
|
temporary: UserQuerySet = self.cleaned_data["sellers_temporary"]
|
||||||
|
duplicates = list(regular.intersection(temporary))
|
||||||
|
if duplicates:
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"A user cannot be a regular and a temporary barman "
|
||||||
|
"at the same time, "
|
||||||
|
"but the following users have been defined as both : %(users)s"
|
||||||
|
)
|
||||||
|
% {"users": ", ".join([u.get_display_name() for u in duplicates])}
|
||||||
|
)
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
def save_sellers(self):
|
||||||
|
sellers = []
|
||||||
|
for users, is_regular in (
|
||||||
|
(self.cleaned_data["sellers_regular"], True),
|
||||||
|
(self.cleaned_data["sellers_temporary"], False),
|
||||||
|
):
|
||||||
|
sellers.extend(
|
||||||
|
[
|
||||||
|
CounterSellers(counter=self.instance, user=u, is_regular=is_regular)
|
||||||
|
for u in users
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# start by deleting removed CounterSellers objects
|
||||||
|
user_ids = [seller.user.id for seller in sellers]
|
||||||
|
CounterSellers.objects.filter(
|
||||||
|
~Q(user_id__in=user_ids), counter=self.instance
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
# then create or update the new barmen
|
||||||
|
CounterSellers.objects.bulk_create(
|
||||||
|
sellers,
|
||||||
|
update_conflicts=True,
|
||||||
|
update_fields=["is_regular"],
|
||||||
|
unique_fields=["user", "counter"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, commit=True): # noqa: FBT002
|
||||||
|
self.instance = super().save(commit=commit)
|
||||||
|
if commit and any(
|
||||||
|
key in self.changed_data for key in ("sellers_regular", "sellers_temporary")
|
||||||
|
):
|
||||||
|
self.save_sellers()
|
||||||
|
return self.instance
|
||||||
|
|
||||||
|
|
||||||
class ScheduledProductActionForm(forms.ModelForm):
|
class ScheduledProductActionForm(forms.ModelForm):
|
||||||
|
|||||||
88
counter/migrations/0038_countersellers.py
Normal file
88
counter/migrations/0038_countersellers.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-03-04 15:26
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("counter", "0037_productformula"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# cf. https://docs.djangoproject.com/fr/stable/howto/writing-migrations/#changing-a-manytomanyfield-to-use-a-through-model
|
||||||
|
migrations.SeparateDatabaseAndState(
|
||||||
|
database_operations=[
|
||||||
|
migrations.RunSQL(
|
||||||
|
sql="ALTER TABLE counter_counter_sellers RENAME TO counter_countersellers",
|
||||||
|
reverse_sql="ALTER TABLE counter_countersellers RENAME TO counter_counter_sellers",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
state_operations=[
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CounterSellers",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"counter",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="counter.counter",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"constraints": [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("counter", "user"),
|
||||||
|
name="counter_counter_sellers_counter_id_subscriber_id_key",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="counter",
|
||||||
|
name="sellers",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="counters",
|
||||||
|
through="counter.CounterSellers",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="sellers",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="countersellers",
|
||||||
|
name="created_at",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
verbose_name="created at",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="countersellers",
|
||||||
|
name="is_regular",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="regular barman"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -551,7 +551,11 @@ class Counter(models.Model):
|
|||||||
choices=[("BAR", _("Bar")), ("OFFICE", _("Office")), ("EBOUTIC", _("Eboutic"))],
|
choices=[("BAR", _("Bar")), ("OFFICE", _("Office")), ("EBOUTIC", _("Eboutic"))],
|
||||||
)
|
)
|
||||||
sellers = models.ManyToManyField(
|
sellers = models.ManyToManyField(
|
||||||
User, verbose_name=_("sellers"), related_name="counters", blank=True
|
User,
|
||||||
|
verbose_name=_("sellers"),
|
||||||
|
related_name="counters",
|
||||||
|
blank=True,
|
||||||
|
through="CounterSellers",
|
||||||
)
|
)
|
||||||
edit_groups = models.ManyToManyField(
|
edit_groups = models.ManyToManyField(
|
||||||
Group, related_name="editable_counters", blank=True
|
Group, related_name="editable_counters", blank=True
|
||||||
@@ -743,6 +747,26 @@ class Counter(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CounterSellers(models.Model):
|
||||||
|
"""Custom through model for the counter-sellers M2M relationship."""
|
||||||
|
|
||||||
|
counter = models.ForeignKey(Counter, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
is_regular = models.BooleanField(_("regular barman"), default=False)
|
||||||
|
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["counter", "user"],
|
||||||
|
name="counter_counter_sellers_counter_id_subscriber_id_key",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"counter {self.counter_id} - user {self.user_id}"
|
||||||
|
|
||||||
|
|
||||||
class RefillingQuerySet(models.QuerySet):
|
class RefillingQuerySet(models.QuerySet):
|
||||||
def annotate_total(self) -> Self:
|
def annotate_total(self) -> Self:
|
||||||
"""Annotate the Queryset with the total amount.
|
"""Annotate the Queryset with the total amount.
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
|
|
||||||
checkFormulas() {
|
checkFormulas() {
|
||||||
const products = new Set(
|
const products = new Set(
|
||||||
Object.keys(this.basket).map((i: string) => Number.parseInt(i)),
|
Object.keys(this.basket).map((i: string) => Number.parseInt(i, 10)),
|
||||||
);
|
);
|
||||||
const formula: ProductFormula = config.formulas.find((f: ProductFormula) => {
|
const formula: ProductFormula = config.formulas.find((f: ProductFormula) => {
|
||||||
return f.products.every((p: number) => products.has(p));
|
return f.products.every((p: number) => products.has(p));
|
||||||
|
|||||||
@@ -1,13 +1,132 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
|
||||||
from club.models import Membership
|
from club.models import Membership
|
||||||
from core.baker_recipes import subscriber_user
|
from core.baker_recipes import subscriber_user
|
||||||
from core.models import User
|
from core.models import Group, User
|
||||||
from counter.baker_recipes import product_recipe
|
from counter.baker_recipes import product_recipe
|
||||||
from counter.forms import CounterEditForm
|
from counter.forms import CounterEditForm
|
||||||
from counter.models import Counter
|
from counter.models import Counter, CounterSellers
|
||||||
|
|
||||||
|
|
||||||
|
class TestEditCounterSellers(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.counter = baker.make(Counter, type="BAR")
|
||||||
|
cls.products = product_recipe.make(_quantity=2, _bulk_create=True)
|
||||||
|
cls.counter.products.add(*cls.products)
|
||||||
|
users = subscriber_user.make(_quantity=6, _bulk_create=True)
|
||||||
|
cls.regular_barmen = users[:2]
|
||||||
|
cls.tmp_barmen = users[2:4]
|
||||||
|
cls.not_barmen = users[4:]
|
||||||
|
CounterSellers.objects.bulk_create(
|
||||||
|
[
|
||||||
|
*baker.prepare(
|
||||||
|
CounterSellers,
|
||||||
|
counter=cls.counter,
|
||||||
|
user=iter(cls.regular_barmen),
|
||||||
|
is_regular=True,
|
||||||
|
_quantity=len(cls.regular_barmen),
|
||||||
|
),
|
||||||
|
*baker.prepare(
|
||||||
|
CounterSellers,
|
||||||
|
counter=cls.counter,
|
||||||
|
user=iter(cls.tmp_barmen),
|
||||||
|
is_regular=False,
|
||||||
|
_quantity=len(cls.tmp_barmen),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
cls.operator = baker.make(
|
||||||
|
User, groups=[Group.objects.get(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_view_ok(self):
|
||||||
|
url = reverse("counter:admin", kwargs={"counter_id": self.counter.id})
|
||||||
|
self.client.force_login(self.operator)
|
||||||
|
res = self.client.get(url)
|
||||||
|
assert res.status_code == 200
|
||||||
|
res = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
"sellers_regular": [u.id for u in self.regular_barmen],
|
||||||
|
"sellers_temporary": [u.id for u in self.tmp_barmen],
|
||||||
|
"products": [p.id for p in self.products],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertRedirects(res, url)
|
||||||
|
|
||||||
|
def test_add_barmen(self):
|
||||||
|
form = CounterEditForm(
|
||||||
|
data={
|
||||||
|
"sellers_regular": [*self.regular_barmen, self.not_barmen[0]],
|
||||||
|
"sellers_temporary": [*self.tmp_barmen, self.not_barmen[1]],
|
||||||
|
"products": self.products,
|
||||||
|
},
|
||||||
|
instance=self.counter,
|
||||||
|
user=self.operator,
|
||||||
|
)
|
||||||
|
assert form.is_valid()
|
||||||
|
form.save()
|
||||||
|
assert set(self.counter.sellers.filter(countersellers__is_regular=True)) == {
|
||||||
|
*self.regular_barmen,
|
||||||
|
self.not_barmen[0],
|
||||||
|
}
|
||||||
|
assert set(self.counter.sellers.filter(countersellers__is_regular=False)) == {
|
||||||
|
*self.tmp_barmen,
|
||||||
|
self.not_barmen[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_barman_change_status(self):
|
||||||
|
"""Test when a barman goes from temporary to regular"""
|
||||||
|
form = CounterEditForm(
|
||||||
|
data={
|
||||||
|
"sellers_regular": [*self.regular_barmen, self.tmp_barmen[0]],
|
||||||
|
"sellers_temporary": [*self.tmp_barmen[1:]],
|
||||||
|
"products": self.products,
|
||||||
|
},
|
||||||
|
instance=self.counter,
|
||||||
|
user=self.operator,
|
||||||
|
)
|
||||||
|
assert form.is_valid()
|
||||||
|
form.save()
|
||||||
|
assert set(self.counter.sellers.filter(countersellers__is_regular=True)) == {
|
||||||
|
*self.regular_barmen,
|
||||||
|
self.tmp_barmen[0],
|
||||||
|
}
|
||||||
|
assert set(
|
||||||
|
self.counter.sellers.filter(countersellers__is_regular=False)
|
||||||
|
) == set(self.tmp_barmen[1:])
|
||||||
|
|
||||||
|
def test_barman_duplicate(self):
|
||||||
|
"""Test that a barman cannot be regular and temporary at the same time."""
|
||||||
|
form = CounterEditForm(
|
||||||
|
data={
|
||||||
|
"sellers_regular": [*self.regular_barmen, self.not_barmen[0]],
|
||||||
|
"sellers_temporary": [*self.tmp_barmen, self.not_barmen[0]],
|
||||||
|
"products": self.products,
|
||||||
|
},
|
||||||
|
instance=self.counter,
|
||||||
|
user=self.operator,
|
||||||
|
)
|
||||||
|
assert not form.is_valid()
|
||||||
|
assert form.errors == {
|
||||||
|
"__all__": [
|
||||||
|
"Un utilisateur ne peut pas être un barman "
|
||||||
|
"régulier et temporaire en même temps, "
|
||||||
|
"mais les utilisateurs suivants ont été définis "
|
||||||
|
f"comme les deux : {self.not_barmen[0].get_display_name()}"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
assert set(self.counter.sellers.filter(countersellers__is_regular=True)) == set(
|
||||||
|
self.regular_barmen
|
||||||
|
)
|
||||||
|
assert set(
|
||||||
|
self.counter.sellers.filter(countersellers__is_regular=False)
|
||||||
|
) == set(self.tmp_barmen)
|
||||||
|
|
||||||
|
|
||||||
class TestEditCounterProducts(TestCase):
|
class TestEditCounterProducts(TestCase):
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
|
||||||
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
@@ -58,7 +59,9 @@ class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
|
|||||||
current_tab = "counters"
|
current_tab = "counters"
|
||||||
|
|
||||||
|
|
||||||
class CounterEditView(CounterAdminTabsMixin, UserPassesTestMixin, UpdateView):
|
class CounterEditView(
|
||||||
|
CounterAdminTabsMixin, UserPassesTestMixin, SuccessMessageMixin, UpdateView
|
||||||
|
):
|
||||||
"""Edit a counter's main informations (for the counter's manager)."""
|
"""Edit a counter's main informations (for the counter's manager)."""
|
||||||
|
|
||||||
model = Counter
|
model = Counter
|
||||||
@@ -66,6 +69,7 @@ class CounterEditView(CounterAdminTabsMixin, UserPassesTestMixin, UpdateView):
|
|||||||
pk_url_kwarg = "counter_id"
|
pk_url_kwarg = "counter_id"
|
||||||
template_name = "core/edit.jinja"
|
template_name = "core/edit.jinja"
|
||||||
current_tab = "counters"
|
current_tab = "counters"
|
||||||
|
success_message = _("Counter update done")
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
if self.request.user.has_perm("counter.change_counter"):
|
if self.request.user.has_perm("counter.change_counter"):
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from django.test import Client, TestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
from model_bakery.recipe import Recipe
|
||||||
|
from pytest_django.asserts import assertRedirects
|
||||||
|
|
||||||
from core.baker_recipes import subscriber_user
|
from core.baker_recipes import subscriber_user
|
||||||
from core.models import Group, User
|
from core.models import Group, User
|
||||||
@@ -52,6 +54,102 @@ class TestElectionUpdateView(TestElection):
|
|||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
class TestElectionForm(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.election = baker.make(Election, end_date=now() + timedelta(days=1))
|
||||||
|
cls.group = baker.make(Group)
|
||||||
|
cls.election.vote_groups.add(cls.group)
|
||||||
|
cls.election.edit_groups.add(cls.group)
|
||||||
|
lists = baker.make(
|
||||||
|
ElectionList, election=cls.election, _quantity=2, _bulk_create=True
|
||||||
|
)
|
||||||
|
cls.roles = baker.make(
|
||||||
|
Role, election=cls.election, _quantity=2, _bulk_create=True
|
||||||
|
)
|
||||||
|
users = baker.make(User, _quantity=4, _bulk_create=True)
|
||||||
|
recipe = Recipe(Candidature)
|
||||||
|
cls.cand = [
|
||||||
|
recipe.prepare(role=cls.roles[0], user=users[0], election_list=lists[0]),
|
||||||
|
recipe.prepare(role=cls.roles[0], user=users[1], election_list=lists[1]),
|
||||||
|
recipe.prepare(role=cls.roles[1], user=users[2], election_list=lists[0]),
|
||||||
|
recipe.prepare(role=cls.roles[1], user=users[3], election_list=lists[1]),
|
||||||
|
]
|
||||||
|
Candidature.objects.bulk_create(cls.cand)
|
||||||
|
cls.vote_url = reverse("election:vote", kwargs={"election_id": cls.election.id})
|
||||||
|
cls.detail_url = reverse(
|
||||||
|
"election:detail", kwargs={"election_id": cls.election.id}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_election_good_form(self):
|
||||||
|
postes = (self.roles[0].title, self.roles[1].title)
|
||||||
|
votes = [
|
||||||
|
{postes[0]: "", postes[1]: str(self.cand[2].id)},
|
||||||
|
{postes[0]: "", postes[1]: ""},
|
||||||
|
{postes[0]: str(self.cand[0].id), postes[1]: str(self.cand[2].id)},
|
||||||
|
{postes[0]: str(self.cand[0].id), postes[1]: str(self.cand[3].id)},
|
||||||
|
]
|
||||||
|
voters = subscriber_user.make(_quantity=len(votes), _bulk_create=True)
|
||||||
|
self.group.users.set(voters)
|
||||||
|
|
||||||
|
for voter, vote in zip(voters, votes, strict=True):
|
||||||
|
assert self.election.can_vote(voter)
|
||||||
|
self.client.force_login(voter)
|
||||||
|
response = self.client.post(self.vote_url, data=vote)
|
||||||
|
assertRedirects(response, self.detail_url)
|
||||||
|
|
||||||
|
assert set(self.election.voters.all()) == set(voters)
|
||||||
|
assert self.election.results == {
|
||||||
|
postes[0]: {
|
||||||
|
self.cand[0].user.username: {"percent": 50.0, "vote": 2},
|
||||||
|
self.cand[1].user.username: {"percent": 0.0, "vote": 0},
|
||||||
|
"blank vote": {"percent": 50.0, "vote": 2},
|
||||||
|
"total vote": 4,
|
||||||
|
},
|
||||||
|
postes[1]: {
|
||||||
|
self.cand[2].user.username: {"percent": 50.0, "vote": 2},
|
||||||
|
self.cand[3].user.username: {"percent": 25.0, "vote": 1},
|
||||||
|
"blank vote": {"percent": 25.0, "vote": 1},
|
||||||
|
"total vote": 4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_election_bad_form(self):
|
||||||
|
postes = (self.roles[0].title, self.roles[1].title)
|
||||||
|
|
||||||
|
votes = [
|
||||||
|
{postes[0]: "", postes[1]: str(self.cand[0].id)}, # wrong candidate
|
||||||
|
{postes[0]: ""},
|
||||||
|
{
|
||||||
|
postes[0]: "0123456789", # unknow users
|
||||||
|
postes[1]: str(subscriber_user.make().id), # not a candidate
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
]
|
||||||
|
voters = subscriber_user.make(_quantity=len(votes), _bulk_create=True)
|
||||||
|
self.group.users.set(voters)
|
||||||
|
|
||||||
|
for voter, vote in zip(voters, votes, strict=True):
|
||||||
|
self.client.force_login(voter)
|
||||||
|
response = self.client.post(self.vote_url, data=vote)
|
||||||
|
assertRedirects(response, self.detail_url)
|
||||||
|
|
||||||
|
assert self.election.results == {
|
||||||
|
postes[0]: {
|
||||||
|
self.cand[0].user.username: {"percent": 0.0, "vote": 0},
|
||||||
|
self.cand[1].user.username: {"percent": 0.0, "vote": 0},
|
||||||
|
"blank vote": {"percent": 100.0, "vote": 2},
|
||||||
|
"total vote": 2,
|
||||||
|
},
|
||||||
|
postes[1]: {
|
||||||
|
self.cand[2].user.username: {"percent": 0.0, "vote": 0},
|
||||||
|
self.cand[3].user.username: {"percent": 0.0, "vote": 0},
|
||||||
|
"blank vote": {"percent": 100.0, "vote": 2},
|
||||||
|
"total vote": 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_election_create_list_permission(client: Client):
|
def test_election_create_list_permission(client: Client):
|
||||||
election = baker.make(Election, end_candidature=now() + timedelta(hours=1))
|
election = baker.make(Election, end_candidature=now() + timedelta(hours=1))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-03-07 15:47+0100\n"
|
"POT-Creation-Date: 2026-03-10 10:28+0100\n"
|
||||||
"PO-Revision-Date: 2016-07-18\n"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@@ -2937,6 +2937,29 @@ msgstr "Cet UID est invalide"
|
|||||||
msgid "User not found"
|
msgid "User not found"
|
||||||
msgstr "Utilisateur non trouvé"
|
msgstr "Utilisateur non trouvé"
|
||||||
|
|
||||||
|
#: counter/forms.py
|
||||||
|
msgid "Regular barmen"
|
||||||
|
msgstr "Barmen réguliers"
|
||||||
|
|
||||||
|
#: counter/forms.py
|
||||||
|
msgid ""
|
||||||
|
"Barmen having regular permanences or frequently giving a hand throughout the "
|
||||||
|
"semester."
|
||||||
|
msgstr ""
|
||||||
|
"Les barmen assurant des permanences régulières ou donnant régulièrement un "
|
||||||
|
"coup de main au cours du semestre."
|
||||||
|
|
||||||
|
#: counter/forms.py
|
||||||
|
msgid "Temporary barmen"
|
||||||
|
msgstr "Barmen temporaires"
|
||||||
|
|
||||||
|
#: counter/forms.py
|
||||||
|
msgid ""
|
||||||
|
"Barmen who will be there only for a limited period (e.g. for one evening)"
|
||||||
|
msgstr ""
|
||||||
|
"Les barmen qui seront là uniquement pour une durée limitée (par exemple, le "
|
||||||
|
"temps d'une soirée)"
|
||||||
|
|
||||||
#: counter/forms.py
|
#: counter/forms.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you want to add a product that is not owned by your club to this counter, "
|
"If you want to add a product that is not owned by your club to this counter, "
|
||||||
@@ -2945,6 +2968,16 @@ msgstr ""
|
|||||||
"Si vous souhaitez ajouter sur ce comptoir un produit qui n'appartient pas à "
|
"Si vous souhaitez ajouter sur ce comptoir un produit qui n'appartient pas à "
|
||||||
"votre club, vous devriez demander à un admin."
|
"votre club, vous devriez demander à un admin."
|
||||||
|
|
||||||
|
#: counter/forms.py
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"A user cannot be a regular and a temporary barman at the same time, but the "
|
||||||
|
"following users have been defined as both : %(users)s"
|
||||||
|
msgstr ""
|
||||||
|
"Un utilisateur ne peut pas être un barman régulier et temporaire en même "
|
||||||
|
"temps, mais les utilisateurs suivants ont été définis comme les deux : "
|
||||||
|
"%(users)s"
|
||||||
|
|
||||||
#: counter/forms.py
|
#: counter/forms.py
|
||||||
msgid "Date and time of action"
|
msgid "Date and time of action"
|
||||||
msgstr "Date et heure de l'action"
|
msgstr "Date et heure de l'action"
|
||||||
@@ -3193,6 +3226,10 @@ msgstr "vendeurs"
|
|||||||
msgid "token"
|
msgid "token"
|
||||||
msgstr "jeton"
|
msgstr "jeton"
|
||||||
|
|
||||||
|
#: counter/models.py
|
||||||
|
msgid "regular barman"
|
||||||
|
msgstr "barman régulier"
|
||||||
|
|
||||||
#: counter/models.py sith/settings.py
|
#: counter/models.py sith/settings.py
|
||||||
msgid "Credit card"
|
msgid "Credit card"
|
||||||
msgstr "Carte bancaire"
|
msgstr "Carte bancaire"
|
||||||
@@ -3905,6 +3942,10 @@ msgstr "Temps"
|
|||||||
msgid "Top 100 barman %(counter_name)s (all semesters)"
|
msgid "Top 100 barman %(counter_name)s (all semesters)"
|
||||||
msgstr "Top 100 barman %(counter_name)s (tous les semestres)"
|
msgstr "Top 100 barman %(counter_name)s (tous les semestres)"
|
||||||
|
|
||||||
|
#: counter/views/admin.py
|
||||||
|
msgid "Counter update done"
|
||||||
|
msgstr "Mise à jour du comptoir effectuée"
|
||||||
|
|
||||||
#: counter/views/admin.py
|
#: counter/views/admin.py
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(formula)s (formula)"
|
msgid "%(formula)s (formula)"
|
||||||
@@ -5253,8 +5294,6 @@ msgid "One day"
|
|||||||
msgstr "Un jour"
|
msgstr "Un jour"
|
||||||
|
|
||||||
#: sith/settings.py
|
#: sith/settings.py
|
||||||
#, fuzzy
|
|
||||||
#| msgid "GA staff member"
|
|
||||||
msgid "GA staff member"
|
msgid "GA staff member"
|
||||||
msgstr "Membre staff GA"
|
msgstr "Membre staff GA"
|
||||||
|
|
||||||
|
|||||||
2367
package-lock.json
generated
2367
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -8,8 +8,6 @@
|
|||||||
"compile-dev": "vite build --mode development",
|
"compile-dev": "vite build --mode development",
|
||||||
"serve": "vite build --mode development --watch --minify false",
|
"serve": "vite build --mode development --watch --minify false",
|
||||||
"openapi": "openapi-ts",
|
"openapi": "openapi-ts",
|
||||||
"analyse-dev": "vite-bundle-visualizer --mode development",
|
|
||||||
"analyse-prod": "vite-bundle-visualizer --mode production",
|
|
||||||
"check": "tsc && biome check --write"
|
"check": "tsc && biome check --write"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -28,29 +26,28 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.29.0",
|
"@babel/core": "^7.29.0",
|
||||||
"@babel/preset-env": "^7.29.0",
|
"@babel/preset-env": "^7.29.0",
|
||||||
"@biomejs/biome": "^2.3.14",
|
"@biomejs/biome": "^2.4.6",
|
||||||
"@hey-api/openapi-ts": "^0.92.4",
|
"@hey-api/openapi-ts": "^0.94.0",
|
||||||
"@rollup/plugin-inject": "^5.0.5",
|
"@rollup/plugin-inject": "^5.0.5",
|
||||||
"@types/alpinejs": "^3.13.11",
|
"@types/alpinejs": "^3.13.11",
|
||||||
"@types/cytoscape-cxtmenu": "^3.4.5",
|
"@types/cytoscape-cxtmenu": "^3.4.5",
|
||||||
"@types/cytoscape-klay": "^3.1.5",
|
"@types/cytoscape-klay": "^3.1.5",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.0.0"
|
||||||
"vite-bundle-visualizer": "^1.2.1",
|
|
||||||
"vite-plugin-static-copy": "^3.2.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alpinejs/sort": "^3.15.8",
|
"@alpinejs/sort": "^3.15.8",
|
||||||
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
||||||
"@floating-ui/dom": "^1.7.5",
|
"@floating-ui/dom": "^1.7.6",
|
||||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||||
"@fullcalendar/core": "^6.1.20",
|
"@fullcalendar/core": "^6.1.20",
|
||||||
"@fullcalendar/daygrid": "^6.1.20",
|
"@fullcalendar/daygrid": "^6.1.20",
|
||||||
"@fullcalendar/icalendar": "^6.1.20",
|
"@fullcalendar/icalendar": "^6.1.20",
|
||||||
"@fullcalendar/list": "^6.1.20",
|
"@fullcalendar/list": "^6.1.20",
|
||||||
"@sentry/browser": "^10.38.0",
|
"@sentry/browser": "^10.43.0",
|
||||||
"@zip.js/zip.js": "^2.8.20",
|
"@zip.js/zip.js": "^2.8.23",
|
||||||
"3d-force-graph": "^1.79.1",
|
"3d-force-graph": "^1.79.1",
|
||||||
"alpinejs": "^3.15.8",
|
"alpinejs": "^3.15.8",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
@@ -60,14 +57,14 @@
|
|||||||
"cytoscape-klay": "^3.1.4",
|
"cytoscape-klay": "^3.1.4",
|
||||||
"d3-force-3d": "^3.0.6",
|
"d3-force-3d": "^3.0.6",
|
||||||
"easymde": "^2.20.0",
|
"easymde": "^2.20.0",
|
||||||
"glob": "^13.0.2",
|
"glob": "^13.0.6",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"htmx.org": "^2.0.8",
|
"htmx.org": "^2.0.8",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lit-html": "^3.3.2",
|
"lit-html": "^3.3.2",
|
||||||
"native-file-system-adapter": "^3.0.1",
|
"native-file-system-adapter": "^3.0.1",
|
||||||
"three": "^0.182.0",
|
"three": "^0.183.2",
|
||||||
"three-spritetext": "^1.10.0",
|
"three-spritetext": "^1.10.0",
|
||||||
"tom-select": "^2.5.1"
|
"tom-select": "^2.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ authors = [
|
|||||||
license = { text = "GPL-3.0-only" }
|
license = { text = "GPL-3.0-only" }
|
||||||
requires-python = "<4.0,>=3.12"
|
requires-python = "<4.0,>=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django>=5.2.11,<6.0.0",
|
"django>=5.2.12,<6.0.0",
|
||||||
"django-ninja>=1.5.3,<6.0.0",
|
"django-ninja>=1.5.3,<6.0.0",
|
||||||
"django-ninja-extra>=0.31.0",
|
"django-ninja-extra>=0.31.0",
|
||||||
"Pillow>=12.1.1,<13.0.0",
|
"Pillow>=12.1.1,<13.0.0",
|
||||||
@@ -27,15 +27,15 @@ dependencies = [
|
|||||||
"django-jinja<3.0.0,>=2.11.0",
|
"django-jinja<3.0.0,>=2.11.0",
|
||||||
"cryptography>=46.0.5,<47.0.0",
|
"cryptography>=46.0.5,<47.0.0",
|
||||||
"django-phonenumber-field>=8.4.0,<9.0.0",
|
"django-phonenumber-field>=8.4.0,<9.0.0",
|
||||||
"phonenumbers>=9.0.23,<10.0.0",
|
"phonenumbers>=9.0.25,<10.0.0",
|
||||||
"reportlab>=4.4.9,<5.0.0",
|
"reportlab>=4.4.10,<5.0.0",
|
||||||
"django-haystack<4.0.0,>=3.3.0",
|
"django-haystack<4.0.0,>=3.3.0",
|
||||||
"xapian-haystack<4.0.0,>=3.1.0",
|
"xapian-haystack<4.0.0,>=3.1.0",
|
||||||
"libsass<1.0.0,>=0.23.0",
|
"libsass<1.0.0,>=0.23.0",
|
||||||
"django-ordered-model<4.0.0,>=3.7.4",
|
"django-ordered-model<4.0.0,>=3.7.4",
|
||||||
"django-simple-captcha<1.0.0,>=0.6.3",
|
"django-simple-captcha<1.0.0,>=0.6.3",
|
||||||
"python-dateutil<3.0.0.0,>=2.9.0.post0",
|
"python-dateutil<3.0.0.0,>=2.9.0.post0",
|
||||||
"sentry-sdk>=2.52.0,<3.0.0",
|
"sentry-sdk>=2.54.0,<3.0.0",
|
||||||
"jinja2<4.0.0,>=3.1.6",
|
"jinja2<4.0.0,>=3.1.6",
|
||||||
"django-countries>=8.2.0,<9.0.0",
|
"django-countries>=8.2.0,<9.0.0",
|
||||||
"dict2xml>=1.7.8,<2.0.0",
|
"dict2xml>=1.7.8,<2.0.0",
|
||||||
@@ -51,7 +51,7 @@ dependencies = [
|
|||||||
"psutil>=7.2.2,<8.0.0",
|
"psutil>=7.2.2,<8.0.0",
|
||||||
"celery[redis]>=5.6.2,<7",
|
"celery[redis]>=5.6.2,<7",
|
||||||
"django-celery-results>=2.5.1",
|
"django-celery-results>=2.5.1",
|
||||||
"django-celery-beat>=2.7.0",
|
"django-celery-beat>=2.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -60,31 +60,31 @@ documentation = "https://sith-ae.readthedocs.io/"
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
prod = [
|
prod = [
|
||||||
"psycopg[c]>=3.3.2,<4.0.0",
|
"psycopg[c]>=3.3.3,<4.0.0",
|
||||||
]
|
]
|
||||||
dev = [
|
dev = [
|
||||||
"django-debug-toolbar>=6.2.0,<7",
|
"django-debug-toolbar>=6.2.0,<7",
|
||||||
"ipython>=9.10.0,<10.0.0",
|
"ipython>=9.11.0,<10.0.0",
|
||||||
"pre-commit>=4.5.1,<5.0.0",
|
"pre-commit>=4.5.1,<5.0.0",
|
||||||
"ruff>=0.15.0,<1.0.0",
|
"ruff>=0.15.5,<1.0.0",
|
||||||
"djhtml>=3.0.10,<4.0.0",
|
"djhtml>=3.0.10,<4.0.0",
|
||||||
"faker>=40.4.0,<41.0.0",
|
"faker>=40.8.0,<41.0.0",
|
||||||
"rjsmin>=1.2.5,<2.0.0",
|
"rjsmin>=1.2.5,<2.0.0",
|
||||||
]
|
]
|
||||||
tests = [
|
tests = [
|
||||||
"freezegun>=1.5.5,<2.0.0",
|
"freezegun>=1.5.5,<2.0.0",
|
||||||
"pytest>=9.0.2,<10.0.0",
|
"pytest>=9.0.2,<10.0.0",
|
||||||
"pytest-cov>=7.0.0,<8.0.0",
|
"pytest-cov>=7.0.0,<8.0.0",
|
||||||
"pytest-django<5.0.0,>=4.10.0",
|
"pytest-django<5.0.0,>=4.12.0",
|
||||||
"model-bakery<2.0.0,>=1.23.2",
|
"model-bakery<2.0.0,>=1.23.3",
|
||||||
"beautifulsoup4>=4.14.3,<5",
|
"beautifulsoup4>=4.14.3,<5",
|
||||||
"lxml>=6.0.2,<7",
|
"lxml>=6.0.2,<7",
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
"mkdocs<2.0.0,>=1.6.1",
|
"mkdocs<2.0.0,>=1.6.1",
|
||||||
"mkdocs-material>=9.7.1,<10.0.0",
|
"mkdocs-material>=9.7.5,<10.0.0",
|
||||||
"mkdocstrings>=1.0.3,<2.0.0",
|
"mkdocstrings>=1.0.3,<2.0.0",
|
||||||
"mkdocstrings-python>=2.0.2,<3.0.0",
|
"mkdocstrings-python>=2.0.3,<3.0.0",
|
||||||
"mkdocs-include-markdown-plugin>=7.2.1,<8.0.0",
|
"mkdocs-include-markdown-plugin>=7.2.1,<8.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type TomSelect from "tom-select";
|
import type TomSelect from "tom-select";
|
||||||
import type { UserAjaxSelect } from "#core:core/components/ajax-select-index.ts";
|
import type { UserAjaxSelect } from "#core:core/components/ajax-select-index.ts";
|
||||||
import { paginated } from "#core:utils/api.ts";
|
import { paginated } from "#core:utils/api.ts";
|
||||||
import { exportToHtml } from "#core:utils/globals.ts";
|
|
||||||
import { History } from "#core:utils/history.ts";
|
import { History } from "#core:utils/history.ts";
|
||||||
import {
|
import {
|
||||||
type IdentifiedUserSchema,
|
type IdentifiedUserSchema,
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
// biome-ignore lint/correctness/noNodejsModules: this is backend side
|
|
||||||
import { parse, resolve } from "node:path";
|
import { parse, resolve } from "node:path";
|
||||||
import inject from "@rollup/plugin-inject";
|
import inject from "@rollup/plugin-inject";
|
||||||
import { glob } from "glob";
|
import { glob } from "glob";
|
||||||
import type { Rollup } from "vite";
|
import { visualizer } from "rollup-plugin-visualizer";
|
||||||
import { type AliasOptions, defineConfig, type UserConfig } from "vite";
|
import {
|
||||||
|
type AliasOptions,
|
||||||
|
defineConfig,
|
||||||
|
type PluginOption,
|
||||||
|
type Rollup,
|
||||||
|
type UserConfig,
|
||||||
|
} from "vite";
|
||||||
import tsconfig from "./tsconfig.json";
|
import tsconfig from "./tsconfig.json";
|
||||||
|
|
||||||
const outDir = resolve(__dirname, "./staticfiles/generated/bundled");
|
const outDir = resolve(__dirname, "./staticfiles/generated/bundled");
|
||||||
const vendored = resolve(outDir, "vendored");
|
|
||||||
const nodeModules = resolve(__dirname, "node_modules");
|
|
||||||
const collectedFiles = glob.sync(
|
const collectedFiles = glob.sync(
|
||||||
"./!(static)/static/bundled/**/*?(-)index.?(m)[j|t]s?(x)",
|
"./!(static)/static/bundled/**/*?(-)index.?(m)[j|t]s?(x)",
|
||||||
);
|
);
|
||||||
@@ -42,7 +45,6 @@ function getRelativeAssetPath(path: string): string {
|
|||||||
return relativePath.join("/");
|
return relativePath.join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/style/noDefaultExport: this is recommended by documentation
|
|
||||||
export default defineConfig((config: UserConfig) => {
|
export default defineConfig((config: UserConfig) => {
|
||||||
return {
|
return {
|
||||||
base: "/static/bundled/",
|
base: "/static/bundled/",
|
||||||
@@ -86,6 +88,7 @@ export default defineConfig((config: UserConfig) => {
|
|||||||
Alpine: "alpinejs",
|
Alpine: "alpinejs",
|
||||||
htmx: "htmx.org",
|
htmx: "htmx.org",
|
||||||
}),
|
}),
|
||||||
|
visualizer({ filename: ".bundle-size-report.html" }) as PluginOption,
|
||||||
],
|
],
|
||||||
} satisfies UserConfig;
|
} satisfies UserConfig;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user