From b06a06f50cac1f237dfadedd831879559764bd3e Mon Sep 17 00:00:00 2001
From: imperosol
Date: Sat, 7 Dec 2024 13:38:31 +0100
Subject: [PATCH 01/17] feat: add restore on backspace plugin for tom select
---
core/static/bundled/core/components/ajax-select-base.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/core/static/bundled/core/components/ajax-select-base.ts b/core/static/bundled/core/components/ajax-select-base.ts
index 674b7b73..525de097 100644
--- a/core/static/bundled/core/components/ajax-select-base.ts
+++ b/core/static/bundled/core/components/ajax-select-base.ts
@@ -67,6 +67,8 @@ export class AutoCompleteSelectBase extends inheritHtmlElement("select") {
remove_button: {
title: gettext("Remove"),
},
+ // biome-ignore lint/style/useNamingConvention: this is required by the api
+ restore_on_backspace: {}
},
persist: false,
maxItems: this.node.multiple ? this.max : 1,
From e680124d7b9ea9ae1058bbc32bfaea0155ec0379 Mon Sep 17 00:00:00 2001
From: imperosol
Date: Fri, 13 Dec 2024 23:41:24 +0100
Subject: [PATCH 02/17] fix makemessages command in docs
---
docs/howto/translation.md | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/docs/howto/translation.md b/docs/howto/translation.md
index 6ae299ed..02f9f87b 100644
--- a/docs/howto/translation.md
+++ b/docs/howto/translation.md
@@ -37,8 +37,11 @@ Il faut d'abord générer un fichier de traductions,
l'éditer et enfin le compiler au format binaire pour qu'il soit lu par le serveur.
```bash
-./manage.py makemessages --locale=fr -e py,jinja --ignore=node_modules # Pour le backend
-./manage.py makemessages --locale=fr -d djangojs -e js,ts --ignore=node_modules # Pour le frontend
+# Pour le backend
+./manage.py makemessages --locale=fr -e py,jinja --ignore=node_modules
+
+# Pour le frontend
+./manage.py makemessages --locale=fr -d djangojs -e js,ts --ignore=node_modules --ignore=staticfiles/generated
```
## Éditer le fichier django.po
From 6c8a6008d591561367dde8cf646114de974adac4 Mon Sep 17 00:00:00 2001
From: imperosol
Date: Fri, 13 Dec 2024 23:58:25 +0100
Subject: [PATCH 03/17] api route to search products with detailed infos.
---
.../core/components/ajax-select-base.ts | 2 +-
counter/api.py | 50 ++++++++++++-----
counter/schemas.py | 48 +++++++++++++++-
.../counter/components/ajax-select-index.ts | 6 +-
counter/tests/test_product.py | 55 +++++++++++++++++++
counter/widgets/select.py | 6 +-
6 files changed, 144 insertions(+), 23 deletions(-)
diff --git a/core/static/bundled/core/components/ajax-select-base.ts b/core/static/bundled/core/components/ajax-select-base.ts
index 525de097..06c3508e 100644
--- a/core/static/bundled/core/components/ajax-select-base.ts
+++ b/core/static/bundled/core/components/ajax-select-base.ts
@@ -68,7 +68,7 @@ export class AutoCompleteSelectBase extends inheritHtmlElement("select") {
title: gettext("Remove"),
},
// biome-ignore lint/style/useNamingConvention: this is required by the api
- restore_on_backspace: {}
+ restore_on_backspace: {},
},
persist: false,
maxItems: this.node.multiple ? this.max : 1,
diff --git a/counter/api.py b/counter/api.py
index f3f0f101..7c181aa0 100644
--- a/counter/api.py
+++ b/counter/api.py
@@ -12,21 +12,21 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
-from typing import Annotated
-
-from annotated_types import MinLen
-from django.db.models import Q
+from django.conf import settings
+from django.db.models import F
from ninja import Query
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema
-from core.api_permissions import CanAccessLookup, CanView, IsRoot
+from core.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoot
from counter.models import Counter, Product
from counter.schemas import (
CounterFilterSchema,
CounterSchema,
+ ProductFilterSchema,
ProductSchema,
+ SimpleProductSchema,
SimplifiedCounterSchema,
)
@@ -64,15 +64,39 @@ class CounterController(ControllerBase):
class ProductController(ControllerBase):
@route.get(
"/search",
- response=PaginatedResponseSchema[ProductSchema],
+ response=PaginatedResponseSchema[SimpleProductSchema],
permissions=[CanAccessLookup],
)
@paginate(PageNumberPaginationExtra, page_size=50)
- def search_products(self, search: Annotated[str, MinLen(1)]):
- return (
- Product.objects.filter(
- Q(name__icontains=search) | Q(code__icontains=search)
- )
- .filter(archived=False)
- .values()
+ def search_products(self, filters: Query[ProductFilterSchema]):
+ return filters.filter(
+ Product.objects.order_by(
+ F("product_type__priority").desc(nulls_last=True),
+ "product_type",
+ "name",
+ ).values()
+ )
+
+ @route.get(
+ "/search/detailed",
+ response=PaginatedResponseSchema[ProductSchema],
+ permissions=[
+ IsRoot
+ | IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)
+ | IsInGroup(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
+ ],
+ url_name="search_products_detailed",
+ )
+ @paginate(PageNumberPaginationExtra, page_size=50)
+ def search_products_detailed(self, filters: Query[ProductFilterSchema]):
+ """Get the detailed information about the products."""
+ return filters.filter(
+ Product.objects.select_related("club")
+ .prefetch_related("buying_groups")
+ .select_related("product_type")
+ .order_by(
+ F("product_type__priority").desc(nulls_last=True),
+ "product_type",
+ "name",
+ )
)
diff --git a/counter/schemas.py b/counter/schemas.py
index ec1a842d..7ecb346b 100644
--- a/counter/schemas.py
+++ b/counter/schemas.py
@@ -1,10 +1,12 @@
from typing import Annotated
from annotated_types import MinLen
+from django.urls import reverse
from ninja import Field, FilterSchema, ModelSchema
-from core.schemas import SimpleUserSchema
-from counter.models import Counter, Product
+from club.schemas import ClubSchema
+from core.schemas import GroupSchema, SimpleUserSchema
+from counter.models import Counter, Product, ProductType
class CounterSchema(ModelSchema):
@@ -26,7 +28,47 @@ class SimplifiedCounterSchema(ModelSchema):
fields = ["id", "name"]
-class ProductSchema(ModelSchema):
+class ProductTypeSchema(ModelSchema):
+ class Meta:
+ model = ProductType
+ fields = ["id", "name"]
+
+
+class SimpleProductSchema(ModelSchema):
class Meta:
model = Product
fields = ["id", "name", "code"]
+
+
+class ProductSchema(ModelSchema):
+ class Meta:
+ model = Product
+ fields = [
+ "id",
+ "name",
+ "code",
+ "description",
+ "purchase_price",
+ "selling_price",
+ "icon",
+ "limit_age",
+ "archived",
+ ]
+
+ buying_groups: list[GroupSchema]
+ club: ClubSchema
+ product_type: ProductTypeSchema | None
+ url: str
+
+ @staticmethod
+ def resolve_url(obj: Product) -> str:
+ return reverse("counter:product_edit", kwargs={"product_id": obj.id})
+
+
+class ProductFilterSchema(FilterSchema):
+ search: Annotated[str, MinLen(1)] | None = Field(
+ None, q=["name__icontains", "code__icontains"]
+ )
+ is_archived: bool | None = Field(None, q="archived")
+ buying_groups: set[int] | None = Field(None, q="buying_groups__in")
+ product_type: set[int] | None = Field(None, q="product_type__in")
diff --git a/counter/static/bundled/counter/components/ajax-select-index.ts b/counter/static/bundled/counter/components/ajax-select-index.ts
index 147e4733..a2d61a48 100644
--- a/counter/static/bundled/counter/components/ajax-select-index.ts
+++ b/counter/static/bundled/counter/components/ajax-select-index.ts
@@ -4,7 +4,7 @@ import type { TomOption } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/dist/types/utils";
import {
type CounterSchema,
- type ProductSchema,
+ type SimpleProductSchema,
counterSearchCounter,
productSearchProducts,
} from "#openapi";
@@ -23,13 +23,13 @@ export class ProductAjaxSelect extends AjaxSelect {
return [];
}
- protected renderOption(item: ProductSchema, sanitize: typeof escape_html) {
+ protected renderOption(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `
${sanitize(item.code)} - ${sanitize(item.name)}
`;
}
- protected renderItem(item: ProductSchema, sanitize: typeof escape_html) {
+ protected renderItem(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `${sanitize(item.code)} - ${sanitize(item.name)}`;
}
}
diff --git a/counter/tests/test_product.py b/counter/tests/test_product.py
index d18c6f11..a5eb39c4 100644
--- a/counter/tests/test_product.py
+++ b/counter/tests/test_product.py
@@ -1,11 +1,19 @@
from io import BytesIO
+from typing import Callable
from uuid import uuid4
import pytest
+from django.conf import settings
+from django.core.cache import cache
from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import Client
+from django.urls import reverse
from model_bakery import baker
from PIL import Image
+from pytest_django.asserts import assertNumQueries
+from core.baker_recipes import board_user, subscriber_user
+from core.models import RealGroup, User
from counter.models import Product, ProductType
@@ -31,3 +39,50 @@ def test_resize_product_icon(model):
assert product.icon.height == 70
assert product.icon.name == f"products/{name}.webp"
assert Image.open(product.icon).format == "WEBP"
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize(
+ ("user_factory", "status_code"),
+ [
+ (lambda: baker.make(User, is_superuser=True), 200),
+ (board_user.make, 403),
+ (subscriber_user.make, 403),
+ (
+ lambda: baker.make(
+ User,
+ groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
+ ),
+ 200,
+ ),
+ (
+ lambda: baker.make(
+ User,
+ groups=[
+ RealGroup.objects.get(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
+ ],
+ ),
+ 200,
+ ),
+ ],
+)
+def test_fetch_product_access(
+ client: Client, user_factory: Callable[[], User], status_code: int
+):
+ """Test that only authorized users can use the `GET /product` route."""
+ client.force_login(user_factory())
+ assert (
+ client.get(reverse("api:search_products_detailed")).status_code == status_code
+ )
+
+
+@pytest.mark.django_db
+def test_fetch_product_nb_queries(client: Client):
+ client.force_login(baker.make(User, is_superuser=True))
+ cache.clear()
+ with assertNumQueries(5):
+ # - 2 for authentication
+ # - 1 for pagination
+ # - 1 for the actual request
+ # - 1 to prefetch the related buying_groups
+ client.get(reverse("api:search_products_detailed"))
diff --git a/counter/widgets/select.py b/counter/widgets/select.py
index 68b0bfc1..78c92862 100644
--- a/counter/widgets/select.py
+++ b/counter/widgets/select.py
@@ -2,7 +2,7 @@ from pydantic import TypeAdapter
from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
from counter.models import Counter, Product
-from counter.schemas import ProductSchema, SimplifiedCounterSchema
+from counter.schemas import SimpleProductSchema, SimplifiedCounterSchema
_js = ["bundled/counter/components/ajax-select-index.ts"]
@@ -24,12 +24,12 @@ class AutoCompleteSelectMultipleCounter(AutoCompleteSelectMultiple):
class AutoCompleteSelectProduct(AutoCompleteSelect):
component_name = "product-ajax-select"
model = Product
- adapter = TypeAdapter(list[ProductSchema])
+ adapter = TypeAdapter(list[SimpleProductSchema])
js = _js
class AutoCompleteSelectMultipleProduct(AutoCompleteSelectMultiple):
component_name = "product-ajax-select"
model = Product
- adapter = TypeAdapter(list[ProductSchema])
+ adapter = TypeAdapter(list[SimpleProductSchema])
js = _js
From 483670e79863afa6f616d2a0643619c687a6d8fc Mon Sep 17 00:00:00 2001
From: imperosol
Date: Sun, 15 Dec 2024 18:55:09 +0100
Subject: [PATCH 04/17] Make `ProductType` an `OrderedModel`
---
counter/admin.py | 2 +-
counter/api.py | 4 +-
...0028_alter_producttype_comment_and_more.py | 62 +++++++
counter/models.py | 17 +-
counter/views/admin.py | 6 +-
eboutic/models.py | 2 +-
eboutic/templates/eboutic/eboutic_main.jinja | 2 +-
locale/fr/LC_MESSAGES/django.po | 172 +++++++++---------
8 files changed, 167 insertions(+), 100 deletions(-)
create mode 100644 counter/migrations/0028_alter_producttype_comment_and_more.py
diff --git a/counter/admin.py b/counter/admin.py
index b3e6a91a..5dc795f2 100644
--- a/counter/admin.py
+++ b/counter/admin.py
@@ -129,7 +129,7 @@ class PermanencyAdmin(SearchModelAdmin):
@admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin):
- list_display = ("name", "priority")
+ list_display = ("name", "order")
@admin.register(CashRegisterSummary)
diff --git a/counter/api.py b/counter/api.py
index 7c181aa0..8c37d0ac 100644
--- a/counter/api.py
+++ b/counter/api.py
@@ -71,7 +71,7 @@ class ProductController(ControllerBase):
def search_products(self, filters: Query[ProductFilterSchema]):
return filters.filter(
Product.objects.order_by(
- F("product_type__priority").desc(nulls_last=True),
+ F("product_type__order").asc(nulls_last=True),
"product_type",
"name",
).values()
@@ -95,7 +95,7 @@ class ProductController(ControllerBase):
.prefetch_related("buying_groups")
.select_related("product_type")
.order_by(
- F("product_type__priority").desc(nulls_last=True),
+ F("product_type__order").asc(nulls_last=True),
"product_type",
"name",
)
diff --git a/counter/migrations/0028_alter_producttype_comment_and_more.py b/counter/migrations/0028_alter_producttype_comment_and_more.py
new file mode 100644
index 00000000..f7fabb83
--- /dev/null
+++ b/counter/migrations/0028_alter_producttype_comment_and_more.py
@@ -0,0 +1,62 @@
+# Generated by Django 4.2.17 on 2024-12-15 17:53
+
+from django.db import migrations, models
+from django.db.migrations.state import StateApps
+
+
+def move_priority_to_order(apps: StateApps, schema_editor):
+ """Migrate the previous homemade `priority` to `OrderedModel.order`.
+
+ `priority` was a system were click managers set themselves the priority
+ of a ProductType.
+ The higher the priority, the higher it was to be displayed in the eboutic.
+ Multiple product types could share the same priority, in which
+ case they were ordered by alphabetic order.
+
+ The new field is unique per object, and works in the other way :
+ the nearer from 0, the higher it should appear.
+ """
+ ProductType = apps.get_model("counter", "ProductType")
+ product_types = list(ProductType.objects.order_by("-priority", "name"))
+ for order, product_type in enumerate(product_types):
+ product_type.order = order
+ ProductType.objects.bulk_update(product_types, ["order"])
+
+
+class Migration(migrations.Migration):
+ dependencies = [("counter", "0027_alter_refilling_payment_method")]
+
+ operations = [
+ migrations.AlterField(
+ model_name="producttype",
+ name="comment",
+ field=models.TextField(
+ default="",
+ help_text="A text that will be shown on the eboutic.",
+ verbose_name="comment",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="producttype",
+ name="description",
+ field=models.TextField(default="", verbose_name="description"),
+ ),
+ migrations.AlterModelOptions(
+ name="producttype",
+ options={"ordering": ["order"], "verbose_name": "product type"},
+ ),
+ migrations.AddField(
+ model_name="producttype",
+ name="order",
+ field=models.PositiveIntegerField(
+ db_index=True, default=0, editable=False, verbose_name="order"
+ ),
+ preserve_default=False,
+ ),
+ migrations.RunPython(
+ move_priority_to_order,
+ reverse_code=migrations.RunPython.noop,
+ elidable=True,
+ ),
+ migrations.RemoveField(model_name="producttype", name="priority"),
+ ]
diff --git a/counter/models.py b/counter/models.py
index 087baffc..b55207fb 100644
--- a/counter/models.py
+++ b/counter/models.py
@@ -35,6 +35,7 @@ from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django_countries.fields import CountryField
+from ordered_model.models import OrderedModel
from phonenumber_field.modelfields import PhoneNumberField
from accounting.models import CurrencyField
@@ -289,26 +290,26 @@ class AccountDump(models.Model):
)
-class ProductType(models.Model):
+class ProductType(OrderedModel):
"""A product type.
Useful only for categorizing.
"""
name = models.CharField(_("name"), max_length=30)
- description = models.TextField(_("description"), null=True, blank=True)
- comment = models.TextField(_("comment"), null=True, blank=True)
+ description = models.TextField(_("description"), default="")
+ comment = models.TextField(
+ _("comment"),
+ default="",
+ help_text=_("A text that will be shown on the eboutic."),
+ )
icon = ResizedImageField(
height=70, force_format="WEBP", upload_to="products", null=True, blank=True
)
- # priority holds no real backend logic but helps to handle the order in which
- # the items are to be shown to the user
- priority = models.PositiveIntegerField(default=0)
-
class Meta:
verbose_name = _("product type")
- ordering = ["-priority", "name"]
+ ordering = ["order"]
def __str__(self):
return self.name
diff --git a/counter/views/admin.py b/counter/views/admin.py
index fbf466b3..c1f5c63b 100644
--- a/counter/views/admin.py
+++ b/counter/views/admin.py
@@ -109,7 +109,7 @@ class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView
"""A create view for the admins."""
model = ProductType
- fields = ["name", "description", "comment", "icon", "priority"]
+ fields = ["name", "description", "comment", "icon"]
template_name = "core/create.jinja"
current_tab = "products"
@@ -119,7 +119,7 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
model = ProductType
template_name = "core/edit.jinja"
- fields = ["name", "description", "comment", "icon", "priority"]
+ fields = ["name", "description", "comment", "icon"]
pk_url_kwarg = "type_id"
current_tab = "products"
@@ -129,7 +129,7 @@ class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
queryset = Product.objects.values("id", "name", "code", "product_type__name")
template_name = "counter/product_list.jinja"
ordering = [
- F("product_type__priority").desc(nulls_last=True),
+ F("product_type__order").asc(nulls_last=True),
"product_type",
"name",
]
diff --git a/eboutic/models.py b/eboutic/models.py
index 7ec9deef..7f7282b1 100644
--- a/eboutic/models.py
+++ b/eboutic/models.py
@@ -36,7 +36,7 @@ def get_eboutic_products(user: User) -> list[Product]:
.products.filter(product_type__isnull=False)
.filter(archived=False)
.filter(limit_age__lte=user.age)
- .annotate(priority=F("product_type__priority"))
+ .annotate(order=F("product_type__order"))
.annotate(category=F("product_type__name"))
.annotate(category_comment=F("product_type__comment"))
.prefetch_related("buying_groups") # <-- used in `Product.can_be_sold_to`
diff --git a/eboutic/templates/eboutic/eboutic_main.jinja b/eboutic/templates/eboutic/eboutic_main.jinja
index bf5d7556..b71eb434 100644
--- a/eboutic/templates/eboutic/eboutic_main.jinja
+++ b/eboutic/templates/eboutic/eboutic_main.jinja
@@ -88,7 +88,7 @@
{% endif %}
- {% for priority_groups in products|groupby('priority')|reverse %}
+ {% for priority_groups in products|groupby('order') %}
{% for category, items in priority_groups.list|groupby('category') %}
{% if items|count > 0 %}
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 1f74ddaa..20ddf28a 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-12-17 10:53+0100\n"
+"POT-Creation-Date: 2024-12-17 13:09+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal \n"
@@ -18,8 +18,8 @@ msgstr ""
#: accounting/models.py:62 accounting/models.py:101 accounting/models.py:132
#: accounting/models.py:190 club/models.py:55 com/models.py:274
-#: com/models.py:293 counter/models.py:298 counter/models.py:329
-#: counter/models.py:480 forum/models.py:60 launderette/models.py:29
+#: com/models.py:293 counter/models.py:299 counter/models.py:330
+#: counter/models.py:481 forum/models.py:60 launderette/models.py:29
#: launderette/models.py:80 launderette/models.py:116
msgid "name"
msgstr "nom"
@@ -65,8 +65,8 @@ msgid "account number"
msgstr "numéro de compte"
#: accounting/models.py:107 accounting/models.py:136 club/models.py:345
-#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:358
-#: counter/models.py:482 trombi/models.py:209
+#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:359
+#: counter/models.py:483 trombi/models.py:209
msgid "club"
msgstr "club"
@@ -87,12 +87,12 @@ msgstr "Compte club"
msgid "%(club_account)s on %(bank_account)s"
msgstr "%(club_account)s sur %(bank_account)s"
-#: accounting/models.py:188 club/models.py:351 counter/models.py:965
+#: accounting/models.py:188 club/models.py:351 counter/models.py:966
#: election/models.py:16 launderette/models.py:165
msgid "start date"
msgstr "date de début"
-#: accounting/models.py:189 club/models.py:352 counter/models.py:966
+#: accounting/models.py:189 club/models.py:352 counter/models.py:967
#: election/models.py:17
msgid "end date"
msgstr "date de fin"
@@ -105,8 +105,8 @@ msgstr "est fermé"
msgid "club account"
msgstr "compte club"
-#: accounting/models.py:199 accounting/models.py:255 counter/models.py:92
-#: counter/models.py:683
+#: accounting/models.py:199 accounting/models.py:255 counter/models.py:93
+#: counter/models.py:684
msgid "amount"
msgstr "montant"
@@ -128,18 +128,18 @@ msgstr "classeur"
#: accounting/models.py:256 core/models.py:956 core/models.py:1467
#: core/models.py:1512 core/models.py:1541 core/models.py:1565
-#: counter/models.py:693 counter/models.py:797 counter/models.py:1001
+#: counter/models.py:694 counter/models.py:798 counter/models.py:1002
#: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312
#: forum/models.py:413
msgid "date"
msgstr "date"
-#: accounting/models.py:257 counter/models.py:300 counter/models.py:1002
+#: accounting/models.py:257 counter/models.py:302 counter/models.py:1003
#: pedagogy/models.py:208
msgid "comment"
msgstr "commentaire"
-#: accounting/models.py:259 counter/models.py:695 counter/models.py:799
+#: accounting/models.py:259 counter/models.py:696 counter/models.py:800
#: subscription/models.py:56
msgid "payment method"
msgstr "méthode de paiement"
@@ -166,7 +166,7 @@ msgstr "type comptable"
#: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460
#: accounting/models.py:492 core/models.py:1540 core/models.py:1566
-#: counter/models.py:763
+#: counter/models.py:764
msgid "label"
msgstr "étiquette"
@@ -264,7 +264,7 @@ msgstr ""
"Vous devez fournir soit un type comptable simplifié ou un type comptable "
"standard"
-#: accounting/models.py:421 counter/models.py:339 pedagogy/models.py:41
+#: accounting/models.py:421 counter/models.py:340 pedagogy/models.py:41
msgid "code"
msgstr "code"
@@ -1041,7 +1041,7 @@ msgstr "Vous ne pouvez pas faire de boucles dans les clubs"
msgid "A club with that unix_name already exists"
msgstr "Un club avec ce nom UNIX existe déjà."
-#: club/models.py:337 counter/models.py:956 counter/models.py:992
+#: club/models.py:337 counter/models.py:957 counter/models.py:993
#: eboutic/models.py:53 eboutic/models.py:189 election/models.py:183
#: launderette/models.py:130 launderette/models.py:184 sas/models.py:273
#: trombi/models.py:205
@@ -1053,8 +1053,8 @@ msgstr "nom d'utilisateur"
msgid "role"
msgstr "rôle"
-#: club/models.py:359 core/models.py:90 counter/models.py:299
-#: counter/models.py:330 election/models.py:13 election/models.py:115
+#: club/models.py:359 core/models.py:90 counter/models.py:300
+#: counter/models.py:331 election/models.py:13 election/models.py:115
#: election/models.py:188 forum/models.py:61 forum/models.py:245
msgid "description"
msgstr "description"
@@ -2501,7 +2501,7 @@ msgstr "Forum"
msgid "Gallery"
msgstr "Photos"
-#: core/templates/core/base/navbar.jinja:22 counter/models.py:490
+#: core/templates/core/base/navbar.jinja:22 counter/models.py:491
#: counter/templates/counter/counter_list.jinja:11
#: eboutic/templates/eboutic/eboutic_main.jinja:4
#: eboutic/templates/eboutic/eboutic_main.jinja:22
@@ -3607,13 +3607,13 @@ msgstr "Chèque"
msgid "Cash"
msgstr "Espèces"
-#: counter/apps.py:30 counter/models.py:801 sith/settings.py:415
+#: counter/apps.py:30 counter/models.py:802 sith/settings.py:415
#: sith/settings.py:420
msgid "Credit card"
msgstr "Carte bancaire"
-#: counter/apps.py:36 counter/models.py:506 counter/models.py:962
-#: counter/models.py:998 launderette/models.py:32
+#: counter/apps.py:36 counter/models.py:507 counter/models.py:963
+#: counter/models.py:999 launderette/models.py:32
msgid "counter"
msgstr "comptoir"
@@ -3637,180 +3637,184 @@ msgstr "Vidange de votre compte AE"
msgid "Ecocup regularization"
msgstr "Régularization des ecocups"
-#: counter/models.py:91
+#: counter/models.py:92
msgid "account id"
msgstr "numéro de compte"
-#: counter/models.py:93
+#: counter/models.py:94
msgid "recorded product"
msgstr "produits consignés"
-#: counter/models.py:98
+#: counter/models.py:99
msgid "customer"
msgstr "client"
-#: counter/models.py:99
+#: counter/models.py:100
msgid "customers"
msgstr "clients"
-#: counter/models.py:111 counter/views/click.py:68
+#: counter/models.py:112 counter/views/click.py:68
msgid "Not enough money"
msgstr "Solde insuffisant"
-#: counter/models.py:197
+#: counter/models.py:198
msgid "First name"
msgstr "Prénom"
-#: counter/models.py:198
+#: counter/models.py:199
msgid "Last name"
msgstr "Nom de famille"
-#: counter/models.py:199
+#: counter/models.py:200
msgid "Address 1"
msgstr "Adresse 1"
-#: counter/models.py:200
+#: counter/models.py:201
msgid "Address 2"
msgstr "Adresse 2"
-#: counter/models.py:201
+#: counter/models.py:202
msgid "Zip code"
msgstr "Code postal"
-#: counter/models.py:202
+#: counter/models.py:203
msgid "City"
msgstr "Ville"
-#: counter/models.py:203
+#: counter/models.py:204
msgid "Country"
msgstr "Pays"
-#: counter/models.py:211
+#: counter/models.py:212
msgid "Phone number"
msgstr "Numéro de téléphone"
-#: counter/models.py:253
+#: counter/models.py:254
msgid "When the mail warning that the account was about to be dumped was sent."
msgstr "Quand le mail d'avertissement de la vidange du compte a été envoyé."
-#: counter/models.py:258
+#: counter/models.py:259
msgid "Set this to True if the warning mail received an error"
msgstr "Mettre à True si le mail a reçu une erreur"
-#: counter/models.py:265
+#: counter/models.py:266
msgid "The operation that emptied the account."
msgstr "L'opération qui a vidé le compte."
-#: counter/models.py:310 counter/models.py:334
+#: counter/models.py:304
+msgid "A text that will be shown on the eboutic."
+msgstr "Un texte qui sera affiché sur l'eboutic."
+
+#: counter/models.py:311 counter/models.py:335
msgid "product type"
msgstr "type du produit"
-#: counter/models.py:341
+#: counter/models.py:342
msgid "purchase price"
msgstr "prix d'achat"
-#: counter/models.py:342
+#: counter/models.py:343
msgid "Initial cost of purchasing the product"
msgstr "Coût initial d'achat du produit"
-#: counter/models.py:344
+#: counter/models.py:345
msgid "selling price"
msgstr "prix de vente"
-#: counter/models.py:346
+#: counter/models.py:347
msgid "special selling price"
msgstr "prix de vente spécial"
-#: counter/models.py:347
+#: counter/models.py:348
msgid "Price for barmen during their permanence"
msgstr "Prix pour les barmen durant leur permanence"
-#: counter/models.py:355
+#: counter/models.py:356
msgid "icon"
msgstr "icône"
-#: counter/models.py:360
+#: counter/models.py:361
msgid "limit age"
msgstr "âge limite"
-#: counter/models.py:361
+#: counter/models.py:362
msgid "tray price"
msgstr "prix plateau"
-#: counter/models.py:363
+#: counter/models.py:364
msgid "buying groups"
msgstr "groupe d'achat"
-#: counter/models.py:365 election/models.py:50
+#: counter/models.py:366 election/models.py:50
msgid "archived"
msgstr "archivé"
-#: counter/models.py:368 counter/models.py:1096
+#: counter/models.py:369 counter/models.py:1097
msgid "product"
msgstr "produit"
-#: counter/models.py:485
+#: counter/models.py:486
msgid "products"
msgstr "produits"
-#: counter/models.py:488
+#: counter/models.py:489
msgid "counter type"
msgstr "type de comptoir"
-#: counter/models.py:490
+#: counter/models.py:491
msgid "Bar"
msgstr "Bar"
-#: counter/models.py:490
+#: counter/models.py:491
msgid "Office"
msgstr "Bureau"
-#: counter/models.py:493
+#: counter/models.py:494
msgid "sellers"
msgstr "vendeurs"
-#: counter/models.py:501 launderette/models.py:178
+#: counter/models.py:502 launderette/models.py:178
msgid "token"
msgstr "jeton"
-#: counter/models.py:701
+#: counter/models.py:702
msgid "bank"
msgstr "banque"
-#: counter/models.py:703 counter/models.py:804
+#: counter/models.py:704 counter/models.py:805
msgid "is validated"
msgstr "est validé"
-#: counter/models.py:708
+#: counter/models.py:709
msgid "refilling"
msgstr "rechargement"
-#: counter/models.py:781 eboutic/models.py:249
+#: counter/models.py:782 eboutic/models.py:249
msgid "unit price"
msgstr "prix unitaire"
-#: counter/models.py:782 counter/models.py:1076 eboutic/models.py:250
+#: counter/models.py:783 counter/models.py:1077 eboutic/models.py:250
msgid "quantity"
msgstr "quantité"
-#: counter/models.py:801
+#: counter/models.py:802
msgid "Sith account"
msgstr "Compte utilisateur"
-#: counter/models.py:809
+#: counter/models.py:810
msgid "selling"
msgstr "vente"
-#: counter/models.py:913
+#: counter/models.py:914
msgid "Unknown event"
msgstr "Événement inconnu"
-#: counter/models.py:914
+#: counter/models.py:915
#, python-format
msgid "Eticket bought for the event %(event)s"
msgstr "Eticket acheté pour l'événement %(event)s"
-#: counter/models.py:916 counter/models.py:929
+#: counter/models.py:917 counter/models.py:930
#, python-format
msgid ""
"You bought an eticket for the event %(event)s.\n"
@@ -3822,67 +3826,67 @@ msgstr ""
"Vous pouvez également retrouver tous vos e-tickets sur votre page de compte "
"%(url)s."
-#: counter/models.py:967
+#: counter/models.py:968
msgid "last activity date"
msgstr "dernière activité"
-#: counter/models.py:970
+#: counter/models.py:971
msgid "permanency"
msgstr "permanence"
-#: counter/models.py:1003
+#: counter/models.py:1004
msgid "emptied"
msgstr "coffre vidée"
-#: counter/models.py:1006
+#: counter/models.py:1007
msgid "cash register summary"
msgstr "relevé de caisse"
-#: counter/models.py:1072
+#: counter/models.py:1073
msgid "cash summary"
msgstr "relevé"
-#: counter/models.py:1075
+#: counter/models.py:1076
msgid "value"
msgstr "valeur"
-#: counter/models.py:1078
+#: counter/models.py:1079
msgid "check"
msgstr "chèque"
-#: counter/models.py:1080
+#: counter/models.py:1081
msgid "True if this is a bank check, else False"
msgstr "Vrai si c'est un chèque, sinon Faux."
-#: counter/models.py:1084
+#: counter/models.py:1085
msgid "cash register summary item"
msgstr "élément de relevé de caisse"
-#: counter/models.py:1100
+#: counter/models.py:1101
msgid "banner"
msgstr "bannière"
-#: counter/models.py:1102
+#: counter/models.py:1103
msgid "event date"
msgstr "date de l'événement"
-#: counter/models.py:1104
+#: counter/models.py:1105
msgid "event title"
msgstr "titre de l'événement"
-#: counter/models.py:1106
+#: counter/models.py:1107
msgid "secret"
msgstr "secret"
-#: counter/models.py:1145
+#: counter/models.py:1146
msgid "uid"
msgstr "uid"
-#: counter/models.py:1150 counter/models.py:1155
+#: counter/models.py:1151 counter/models.py:1156
msgid "student card"
msgstr "carte étudiante"
-#: counter/models.py:1156
+#: counter/models.py:1157
msgid "student cards"
msgstr "cartes étudiantes"
@@ -4194,15 +4198,15 @@ msgid "There is no products in this website."
msgstr "Il n'y a pas de produits dans ce site web."
#: counter/templates/counter/producttype_list.jinja:4
-#: counter/templates/counter/producttype_list.jinja:10
+#: counter/templates/counter/producttype_list.jinja:26
msgid "Product type list"
msgstr "Liste des types de produit"
-#: counter/templates/counter/producttype_list.jinja:8
+#: counter/templates/counter/producttype_list.jinja:16
msgid "New product type"
msgstr "Nouveau type de produit"
-#: counter/templates/counter/producttype_list.jinja:17
+#: counter/templates/counter/producttype_list.jinja:42
msgid "There is no product types in this website."
msgstr "Il n'y a pas de types de produit dans ce site web."
From c79c251ba7657adf641a42dcf2a537bc626952a1 Mon Sep 17 00:00:00 2001
From: imperosol
Date: Mon, 16 Dec 2024 19:46:34 +0100
Subject: [PATCH 05/17] Add `ProductTypeController`
---
counter/api.py | 54 ++++++++++++++++--
counter/schemas.py | 32 ++++++++++-
counter/tests/test_product_type.py | 91 ++++++++++++++++++++++++++++++
3 files changed, 168 insertions(+), 9 deletions(-)
create mode 100644 counter/tests/test_product_type.py
diff --git a/counter/api.py b/counter/api.py
index 8c37d0ac..43fada5a 100644
--- a/counter/api.py
+++ b/counter/api.py
@@ -14,22 +14,31 @@
#
from django.conf import settings
from django.db.models import F
+from django.shortcuts import get_object_or_404
from ninja import Query
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema
from core.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoot
-from counter.models import Counter, Product
+from counter.models import Counter, Product, ProductType
from counter.schemas import (
CounterFilterSchema,
CounterSchema,
ProductFilterSchema,
ProductSchema,
+ ProductTypeSchema,
+ ReorderProductTypeSchema,
SimpleProductSchema,
SimplifiedCounterSchema,
)
+IsCounterAdmin = (
+ IsRoot
+ | IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)
+ | IsInGroup(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
+)
+
@api_controller("/counter")
class CounterController(ControllerBase):
@@ -80,11 +89,7 @@ class ProductController(ControllerBase):
@route.get(
"/search/detailed",
response=PaginatedResponseSchema[ProductSchema],
- permissions=[
- IsRoot
- | IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)
- | IsInGroup(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
- ],
+ permissions=[IsCounterAdmin],
url_name="search_products_detailed",
)
@paginate(PageNumberPaginationExtra, page_size=50)
@@ -100,3 +105,40 @@ class ProductController(ControllerBase):
"name",
)
)
+
+
+@api_controller("/product-type", permissions=[IsCounterAdmin])
+class ProductTypeController(ControllerBase):
+ @route.get("", response=list[ProductTypeSchema], url_name="fetch-product-types")
+ def fetch_all(self):
+ return ProductType.objects.order_by("order")
+
+ @route.patch("/{type_id}/move")
+ def reorder(self, type_id: int, other_id: Query[ReorderProductTypeSchema]):
+ """Change the order of a product type.
+
+ To use this route, give either the id of the product type
+ this one should be above of,
+ of the id of the product type this one should be below of.
+
+ Order affects the display order of the product types.
+
+ Examples:
+ ```
+ GET /api/counter/product-type
+ => [<1: type A>, <2: type B>, <3: type C>]
+
+ PATCH /api/counter/product-type/3/move?below=1
+
+ GET /api/counter/product-type
+ => [<1: type A>, <3: type C>, <2: type B>]
+ ```
+ """
+ product_type: ProductType = self.get_object_or_exception(
+ ProductType, pk=type_id
+ )
+ other = get_object_or_404(ProductType, pk=other_id.above or other_id.below)
+ if other_id.below is not None:
+ product_type.below(other)
+ else:
+ product_type.above(other)
diff --git a/counter/schemas.py b/counter/schemas.py
index 7ecb346b..1b770a3d 100644
--- a/counter/schemas.py
+++ b/counter/schemas.py
@@ -1,8 +1,9 @@
-from typing import Annotated
+from typing import Annotated, Self
from annotated_types import MinLen
from django.urls import reverse
-from ninja import Field, FilterSchema, ModelSchema
+from ninja import Field, FilterSchema, ModelSchema, Schema
+from pydantic import model_validator
from club.schemas import ClubSchema
from core.schemas import GroupSchema, SimpleUserSchema
@@ -29,11 +30,36 @@ class SimplifiedCounterSchema(ModelSchema):
class ProductTypeSchema(ModelSchema):
+ class Meta:
+ model = ProductType
+ fields = ["id", "name", "description", "comment", "icon", "order"]
+
+ url: str
+
+ @staticmethod
+ def resolve_url(obj: ProductType) -> str:
+ return reverse("counter:producttype_edit", kwargs={"type_id": obj.id})
+
+
+class SimpleProductTypeSchema(ModelSchema):
class Meta:
model = ProductType
fields = ["id", "name"]
+class ReorderProductTypeSchema(Schema):
+ below: int | None = None
+ above: int | None = None
+
+ @model_validator(mode="after")
+ def validate_exclusive(self) -> Self:
+ if self.below is None and self.above is None:
+ raise ValueError("Either 'below' or 'above' must be set.")
+ if self.below is not None and self.above is not None:
+ raise ValueError("Only one of 'below' or 'above' must be set.")
+ return self
+
+
class SimpleProductSchema(ModelSchema):
class Meta:
model = Product
@@ -57,7 +83,7 @@ class ProductSchema(ModelSchema):
buying_groups: list[GroupSchema]
club: ClubSchema
- product_type: ProductTypeSchema | None
+ product_type: SimpleProductTypeSchema | None
url: str
@staticmethod
diff --git a/counter/tests/test_product_type.py b/counter/tests/test_product_type.py
new file mode 100644
index 00000000..45ed5797
--- /dev/null
+++ b/counter/tests/test_product_type.py
@@ -0,0 +1,91 @@
+import pytest
+from django.conf import settings
+from django.test import Client
+from django.urls import reverse
+from model_bakery import baker, seq
+from ninja_extra.testing import TestClient
+
+from core.baker_recipes import board_user, subscriber_user
+from core.models import RealGroup, User
+from counter.api import ProductTypeController
+from counter.models import ProductType
+
+
+@pytest.fixture
+def product_types(db) -> list[ProductType]:
+ """All existing product types, ordered by their `order` field"""
+ # delete product types that have been created in the `populate` command
+ ProductType.objects.all().delete()
+ return baker.make(ProductType, _quantity=5, order=seq(0))
+
+
+@pytest.mark.django_db
+def test_fetch_product_types(product_types: list[ProductType]):
+ """Test that the API returns the right products in the right order"""
+ client = TestClient(ProductTypeController)
+ response = client.get("")
+ assert response.status_code == 200
+ assert [i["id"] for i in response.json()] == [t.id for t in product_types]
+
+
+@pytest.mark.django_db
+def test_move_below_product_type(product_types: list[ProductType]):
+ """Test that moving a product below another works"""
+ client = TestClient(ProductTypeController)
+ response = client.patch(
+ f"/{product_types[-1].id}/move", query={"below": product_types[0].id}
+ )
+ assert response.status_code == 200
+ new_order = [i["id"] for i in client.get("").json()]
+ assert new_order == [
+ product_types[0].id,
+ product_types[-1].id,
+ *[t.id for t in product_types[1:-1]],
+ ]
+
+
+@pytest.mark.django_db
+def test_move_above_product_type(product_types: list[ProductType]):
+ """Test that moving a product above another works"""
+ client = TestClient(ProductTypeController)
+ response = client.patch(
+ f"/{product_types[1].id}/move", query={"above": product_types[0].id}
+ )
+ assert response.status_code == 200
+ new_order = [i["id"] for i in client.get("").json()]
+ assert new_order == [
+ product_types[1].id,
+ product_types[0].id,
+ *[t.id for t in product_types[2:]],
+ ]
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize(
+ ("user_factory", "status_code"),
+ [
+ (lambda: baker.make(User, is_superuser=True), 200),
+ (subscriber_user.make, 403),
+ (board_user.make, 403),
+ (
+ lambda: baker.make(
+ User,
+ groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
+ ),
+ 200,
+ ),
+ (
+ lambda: baker.make(
+ User,
+ groups=[
+ RealGroup.objects.get(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
+ ],
+ ),
+ 200,
+ ),
+ ],
+)
+def test_controller_permissions(client: Client, user_factory, status_code):
+ client.force_login(user_factory())
+ response = client.get(reverse("api:fetch-product-types"))
+ assert response.status_code == status_code
From 47876e3971cfafc42cb7770c23b7f195bc3828e3 Mon Sep 17 00:00:00 2001
From: imperosol
Date: Tue, 17 Dec 2024 00:53:47 +0100
Subject: [PATCH 06/17] Make product types dynamically orderable.
---
core/static/bundled/alpine-index.js | 2 +
core/static/core/forms.scss | 18 ++
core/static/core/style.scss | 11 +
.../bundled/counter/product-type-index.ts | 64 ++++++
counter/static/counter/css/product_type.scss | 15 ++
.../templates/counter/producttype_list.jinja | 40 +++-
locale/fr/LC_MESSAGES/django.po | 2 +-
locale/fr/LC_MESSAGES/djangojs.po | 211 +++++++++---------
package-lock.json | 16 +-
package.json | 3 +-
10 files changed, 269 insertions(+), 113 deletions(-)
create mode 100644 counter/static/bundled/counter/product-type-index.ts
create mode 100644 counter/static/counter/css/product_type.scss
diff --git a/core/static/bundled/alpine-index.js b/core/static/bundled/alpine-index.js
index d07e0bf2..211600a5 100644
--- a/core/static/bundled/alpine-index.js
+++ b/core/static/bundled/alpine-index.js
@@ -1,5 +1,7 @@
+import sort from "@alpinejs/sort";
import Alpine from "alpinejs";
+Alpine.plugin(sort);
window.Alpine = Alpine;
window.addEventListener("DOMContentLoaded", () => {
diff --git a/core/static/core/forms.scss b/core/static/core/forms.scss
index 7dab0484..e439bd8d 100644
--- a/core/static/core/forms.scss
+++ b/core/static/core/forms.scss
@@ -87,3 +87,21 @@ a:not(.button) {
color: $primary-color;
}
}
+
+
+form {
+ .row {
+ label {
+ margin: unset;
+ }
+ }
+
+ fieldset {
+ margin-bottom: 1rem;
+ }
+
+ .helptext {
+ margin-top: .25rem;
+ font-size: 80%;
+ }
+}
diff --git a/core/static/core/style.scss b/core/static/core/style.scss
index cbe8d326..dd44fda0 100644
--- a/core/static/core/style.scss
+++ b/core/static/core/style.scss
@@ -314,6 +314,17 @@ body {
}
}
+ .snackbar {
+ width: 250px;
+ margin-left: -125px;
+ box-sizing: border-box;
+ position: fixed;
+ z-index: 1;
+ left: 50%;
+ top: 60px;
+ text-align: center;
+ }
+
.tabs {
border-radius: 5px;
diff --git a/counter/static/bundled/counter/product-type-index.ts b/counter/static/bundled/counter/product-type-index.ts
new file mode 100644
index 00000000..4d1bdda4
--- /dev/null
+++ b/counter/static/bundled/counter/product-type-index.ts
@@ -0,0 +1,64 @@
+import Alpine from "alpinejs";
+import { producttypeReorder } from "#openapi";
+
+document.addEventListener("alpine:init", () => {
+ Alpine.data("productTypesList", () => ({
+ loading: false,
+ alertMessage: {
+ open: false,
+ success: true,
+ content: "",
+ timeout: null,
+ },
+
+ async reorder(itemId: number, newPosition: number) {
+ // The sort plugin of Alpine doesn't manage dynamic lists with x-sort
+ // (cf. https://github.com/alpinejs/alpine/discussions/4157).
+ // There is an open PR that fixes this issue
+ // (cf. https://github.com/alpinejs/alpine/pull/4361).
+ // However, it hasn't been merged yet.
+ // To overcome this, I get the list of DOM elements
+ // And fetch the `x-sort:item` attribute, which value is
+ // the id of the object in database.
+ // Please make this a little bit cleaner when the fix has been merged
+ // into the main Alpine repo.
+ this.loading = true;
+ const productTypes = this.$refs.productTypes
+ .childNodes as NodeListOf;
+ const getId = (elem: HTMLLIElement) =>
+ Number.parseInt(elem.getAttribute("x-sort:item"));
+ const query =
+ newPosition === 0
+ ? { above: getId(productTypes.item(1)) }
+ : { below: getId(productTypes.item(newPosition - 1)) };
+ const response = await producttypeReorder({
+ // biome-ignore lint/style/useNamingConvention: api is snake_case
+ path: { type_id: itemId },
+ query: query,
+ });
+ this.openAlertMessage(response.response);
+ this.loading = false;
+ },
+
+ openAlertMessage(response: Response) {
+ if (response.ok) {
+ this.alertMessage.success = true;
+ this.alertMessage.content = gettext("Products types successfully reordered");
+ } else {
+ this.alertMessage.success = false;
+ this.alertMessage.content = interpolate(
+ gettext("Product type reorganisation failed with status code : %d"),
+ [response.status],
+ );
+ }
+ this.alertMessage.open = true;
+ if (this.alertMessage.timeout !== null) {
+ clearTimeout(this.alertMessage.timeout);
+ }
+ this.alertMessage.timeout = setTimeout(() => {
+ this.alertMessage.open = false;
+ }, 2000);
+ this.loading = false;
+ },
+ }));
+});
diff --git a/counter/static/counter/css/product_type.scss b/counter/static/counter/css/product_type.scss
new file mode 100644
index 00000000..16bd43a9
--- /dev/null
+++ b/counter/static/counter/css/product_type.scss
@@ -0,0 +1,15 @@
+.product-type-list {
+ li {
+ list-style: none;
+ margin-bottom: 10px;
+
+ i {
+ cursor: grab;
+ visibility: hidden;
+ }
+ }
+}
+
+body:not(.sorting) .product-type-list li:hover i {
+ visibility: visible;
+}
\ No newline at end of file
diff --git a/counter/templates/counter/producttype_list.jinja b/counter/templates/counter/producttype_list.jinja
index 0c4ff0c5..5d7ddc26 100644
--- a/counter/templates/counter/producttype_list.jinja
+++ b/counter/templates/counter/producttype_list.jinja
@@ -4,21 +4,49 @@
{% trans %}Product type list{% endtrans %}
{% endblock %}
+{% block additional_css %}
+
+{% endblock %}
+
+{% block additional_js %}
+
+{% endblock %}
+
{% block content %}
{% trans %}New product type{% endtrans %}
{% if producttype_list %}
- {% trans %}Product type list{% endtrans %}
-
- {% for t in producttype_list %}
- - {{ t }}
- {% endfor %}
-
+
+
+
{% trans %}Product type list{% endtrans %}
+
+ {%- for t in producttype_list -%}
+ -
+
+ {{ t }}
+
+ {%- endfor -%}
+
+
{% else %}
{% trans %}There is no product types in this website.{% endtrans %}
{% endif %}
{% endblock %}
+{% block script %}
+
+{% endblock %}
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 20ddf28a..fdfef6b9 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-12-17 13:09+0100\n"
+"POT-Creation-Date: 2024-12-17 13:10+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal \n"
diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po
index e907e571..c89f7eb1 100644
--- a/locale/fr/LC_MESSAGES/djangojs.po
+++ b/locale/fr/LC_MESSAGES/djangojs.po
@@ -7,7 +7,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-11-14 10:24+0100\n"
+"POT-Creation-Date: 2024-12-17 00:46+0100\n"
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
"Last-Translator: Sli \n"
"Language-Team: AE info \n"
@@ -17,119 +17,128 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+#: core/static/bundled/core/components/ajax-select-base.ts:68
+msgid "Remove"
+msgstr "Retirer"
+
+#: core/static/bundled/core/components/ajax-select-base.ts:90
+msgid "You need to type %(number)s more characters"
+msgstr "Vous devez taper %(number)s caractères de plus"
+
+#: core/static/bundled/core/components/ajax-select-base.ts:94
+msgid "No results found"
+msgstr "Aucun résultat trouvé"
+
+#: core/static/bundled/core/components/easymde-index.ts:38
+msgid "Heading"
+msgstr "Titre"
+
+#: core/static/bundled/core/components/easymde-index.ts:44
+msgid "Italic"
+msgstr "Italique"
+
+#: core/static/bundled/core/components/easymde-index.ts:50
+msgid "Bold"
+msgstr "Gras"
+
+#: core/static/bundled/core/components/easymde-index.ts:56
+msgid "Strikethrough"
+msgstr "Barré"
+
+#: core/static/bundled/core/components/easymde-index.ts:65
+msgid "Underline"
+msgstr "Souligné"
+
+#: core/static/bundled/core/components/easymde-index.ts:74
+msgid "Superscript"
+msgstr "Exposant"
+
+#: core/static/bundled/core/components/easymde-index.ts:83
+msgid "Subscript"
+msgstr "Indice"
+
+#: core/static/bundled/core/components/easymde-index.ts:89
+msgid "Code"
+msgstr "Code"
+
+#: core/static/bundled/core/components/easymde-index.ts:96
+msgid "Quote"
+msgstr "Citation"
+
+#: core/static/bundled/core/components/easymde-index.ts:102
+msgid "Unordered list"
+msgstr "Liste non ordonnée"
+
+#: core/static/bundled/core/components/easymde-index.ts:108
+msgid "Ordered list"
+msgstr "Liste ordonnée"
+
+#: core/static/bundled/core/components/easymde-index.ts:115
+msgid "Insert link"
+msgstr "Insérer lien"
+
+#: core/static/bundled/core/components/easymde-index.ts:121
+msgid "Insert image"
+msgstr "Insérer image"
+
+#: core/static/bundled/core/components/easymde-index.ts:127
+msgid "Insert table"
+msgstr "Insérer tableau"
+
+#: core/static/bundled/core/components/easymde-index.ts:134
+msgid "Clean block"
+msgstr "Nettoyer bloc"
+
+#: core/static/bundled/core/components/easymde-index.ts:141
+msgid "Toggle preview"
+msgstr "Activer la prévisualisation"
+
+#: core/static/bundled/core/components/easymde-index.ts:147
+msgid "Toggle side by side"
+msgstr "Activer la vue côte à côte"
+
+#: core/static/bundled/core/components/easymde-index.ts:153
+msgid "Toggle fullscreen"
+msgstr "Activer le plein écran"
+
+#: core/static/bundled/core/components/easymde-index.ts:160
+msgid "Markdown guide"
+msgstr "Guide markdown"
+
+#: core/static/bundled/core/components/nfc-input-index.ts:26
+msgid "Unsupported NFC card"
+msgstr "Carte NFC non supportée"
+
+#: core/static/bundled/user/family-graph-index.js:233
+msgid "family_tree.%(extension)s"
+msgstr "arbre_genealogique.%(extension)s"
+
+#: core/static/bundled/user/pictures-index.js:76
+msgid "pictures.%(extension)s"
+msgstr "photos.%(extension)s"
+
#: core/static/user/js/user_edit.js:91
#, javascript-format
msgid "captured.%s"
msgstr "capture.%s"
-#: core/static/webpack/core/components/ajax-select-base.ts:68
-msgid "Remove"
-msgstr "Retirer"
+#: counter/static/bundled/counter/product-type-index.ts:36
+msgid "Products types successfully reordered"
+msgstr "Types de produits réordonnés."
-#: core/static/webpack/core/components/ajax-select-base.ts:88
-msgid "You need to type %(number)s more characters"
-msgstr "Vous devez taper %(number)s caractères de plus"
-
-#: core/static/webpack/core/components/ajax-select-base.ts:92
-msgid "No results found"
-msgstr "Aucun résultat trouvé"
-
-#: core/static/webpack/core/components/easymde-index.ts:38
-msgid "Heading"
-msgstr "Titre"
-
-#: core/static/webpack/core/components/easymde-index.ts:44
-msgid "Italic"
-msgstr "Italique"
-
-#: core/static/webpack/core/components/easymde-index.ts:50
-msgid "Bold"
-msgstr "Gras"
-
-#: core/static/webpack/core/components/easymde-index.ts:56
-msgid "Strikethrough"
-msgstr "Barré"
-
-#: core/static/webpack/core/components/easymde-index.ts:65
-msgid "Underline"
-msgstr "Souligné"
-
-#: core/static/webpack/core/components/easymde-index.ts:74
-msgid "Superscript"
-msgstr "Exposant"
-
-#: core/static/webpack/core/components/easymde-index.ts:83
-msgid "Subscript"
-msgstr "Indice"
-
-#: core/static/webpack/core/components/easymde-index.ts:89
-msgid "Code"
-msgstr "Code"
-
-#: core/static/webpack/core/components/easymde-index.ts:96
-msgid "Quote"
-msgstr "Citation"
-
-#: core/static/webpack/core/components/easymde-index.ts:102
-msgid "Unordered list"
-msgstr "Liste non ordonnée"
-
-#: core/static/webpack/core/components/easymde-index.ts:108
-msgid "Ordered list"
-msgstr "Liste ordonnée"
-
-#: core/static/webpack/core/components/easymde-index.ts:115
-msgid "Insert link"
-msgstr "Insérer lien"
-
-#: core/static/webpack/core/components/easymde-index.ts:121
-msgid "Insert image"
-msgstr "Insérer image"
-
-#: core/static/webpack/core/components/easymde-index.ts:127
-msgid "Insert table"
-msgstr "Insérer tableau"
-
-#: core/static/webpack/core/components/easymde-index.ts:134
-msgid "Clean block"
-msgstr "Nettoyer bloc"
-
-#: core/static/webpack/core/components/easymde-index.ts:141
-msgid "Toggle preview"
-msgstr "Activer la prévisualisation"
-
-#: core/static/webpack/core/components/easymde-index.ts:147
-msgid "Toggle side by side"
-msgstr "Activer la vue côte à côte"
-
-#: core/static/webpack/core/components/easymde-index.ts:153
-msgid "Toggle fullscreen"
-msgstr "Activer le plein écran"
-
-#: core/static/webpack/core/components/easymde-index.ts:160
-msgid "Markdown guide"
-msgstr "Guide markdown"
-
-#: core/static/webpack/core/components/nfc-input-index.ts:24
-msgid "Unsupported NFC card"
-msgstr "Carte NFC non supportée"
-
-#: core/static/webpack/user/family-graph-index.js:233
-msgid "family_tree.%(extension)s"
-msgstr "arbre_genealogique.%(extension)s"
-
-#: core/static/webpack/user/pictures-index.js:76
-msgid "pictures.%(extension)s"
-msgstr "photos.%(extension)s"
+#: counter/static/bundled/counter/product-type-index.ts:40
+#, javascript-format
+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:56
msgid "Incorrect value"
msgstr "Valeur incorrecte"
-#: sas/static/webpack/sas/viewer-index.ts:271
+#: sas/static/bundled/sas/viewer-index.ts:271
msgid "Couldn't moderate picture"
msgstr "Il n'a pas été possible de modérer l'image"
-#: sas/static/webpack/sas/viewer-index.ts:284
+#: sas/static/bundled/sas/viewer-index.ts:284
msgid "Couldn't delete picture"
msgstr "Il n'a pas été possible de supprimer l'image"
diff --git a/package-lock.json b/package-lock.json
index 05418a69..c46ef180 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,12 +9,13 @@
"version": "3",
"license": "GPL-3.0-only",
"dependencies": {
+ "@alpinejs/sort": "^3.14.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@hey-api/client-fetch": "^0.4.0",
"@sentry/browser": "^8.34.0",
"@zip.js/zip.js": "^2.7.52",
"3d-force-graph": "^1.73.4",
- "alpinejs": "^3.14.1",
+ "alpinejs": "^3.14.7",
"chart.js": "^4.4.4",
"cytoscape": "^3.30.2",
"cytoscape-cxtmenu": "^3.5.0",
@@ -44,6 +45,12 @@
"vite-plugin-static-copy": "^2.1.0"
}
},
+ "node_modules/@alpinejs/sort": {
+ "version": "3.14.7",
+ "resolved": "https://registry.npmjs.org/@alpinejs/sort/-/sort-3.14.7.tgz",
+ "integrity": "sha512-EJzxTBSoKvOxKHAUFeTSgxJR4rJQQPm10b4dB38kGcsxjUtOeNkbBF3xV4nlc0ZyTv7DarTWdppdoR/iP8jfdQ==",
+ "license": "MIT"
+ },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -3064,9 +3071,10 @@
}
},
"node_modules/alpinejs": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.1.tgz",
- "integrity": "sha512-ICar8UsnRZAYvv/fCNfNeKMXNoXGUfwHrjx7LqXd08zIP95G2d9bAOuaL97re+1mgt/HojqHsfdOLo/A5LuWgQ==",
+ "version": "3.14.7",
+ "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.7.tgz",
+ "integrity": "sha512-ScnbydNBcWVnCiVupD3wWUvoMPm8244xkvDNMxVCspgmap9m4QuJ7pjc+77UtByU+1+Ejg0wzYkP4mQaOMcvng==",
+ "license": "MIT",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
diff --git a/package.json b/package.json
index 2ca46967..77572a6f 100644
--- a/package.json
+++ b/package.json
@@ -33,12 +33,13 @@
"vite-plugin-static-copy": "^2.1.0"
},
"dependencies": {
+ "@alpinejs/sort": "^3.14.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@hey-api/client-fetch": "^0.4.0",
"@sentry/browser": "^8.34.0",
"@zip.js/zip.js": "^2.7.52",
"3d-force-graph": "^1.73.4",
- "alpinejs": "^3.14.1",
+ "alpinejs": "^3.14.7",
"chart.js": "^4.4.4",
"cytoscape": "^3.30.2",
"cytoscape-cxtmenu": "^3.5.0",
From 8d643fc6b449dab32f8b0e6f1e61de149e99ba3d Mon Sep 17 00:00:00 2001
From: imperosol
Date: Tue, 17 Dec 2024 17:23:13 +0100
Subject: [PATCH 07/17] Apply review comments
---
counter/api.py | 2 +-
.../templates/counter/producttype_list.jinja | 26 ++++++++++++-------
counter/tests/test_product_type.py | 2 +-
locale/fr/LC_MESSAGES/django.po | 20 +++++++++++---
locale/fr/LC_MESSAGES/djangojs.po | 4 +--
5 files changed, 37 insertions(+), 17 deletions(-)
diff --git a/counter/api.py b/counter/api.py
index 43fada5a..dd7b75f0 100644
--- a/counter/api.py
+++ b/counter/api.py
@@ -109,7 +109,7 @@ class ProductController(ControllerBase):
@api_controller("/product-type", permissions=[IsCounterAdmin])
class ProductTypeController(ControllerBase):
- @route.get("", response=list[ProductTypeSchema], url_name="fetch-product-types")
+ @route.get("", response=list[ProductTypeSchema], url_name="fetch_product_types")
def fetch_all(self):
return ProductType.objects.order_by("order")
diff --git a/counter/templates/counter/producttype_list.jinja b/counter/templates/counter/producttype_list.jinja
index 5d7ddc26..042925df 100644
--- a/counter/templates/counter/producttype_list.jinja
+++ b/counter/templates/counter/producttype_list.jinja
@@ -13,8 +13,24 @@
{% endblock %}
{% block content %}
- {% trans %}New product type{% endtrans %}
+
+
+ {% trans %}New product type{% endtrans %}
+
+
+
{% if producttype_list %}
+
\n"
@@ -4198,15 +4198,27 @@ msgid "There is no products in this website."
msgstr "Il n'y a pas de produits dans ce site web."
#: counter/templates/counter/producttype_list.jinja:4
-#: counter/templates/counter/producttype_list.jinja:26
+#: counter/templates/counter/producttype_list.jinja:42
msgid "Product type list"
msgstr "Liste des types de produit"
-#: counter/templates/counter/producttype_list.jinja:16
+#: counter/templates/counter/producttype_list.jinja:18
msgid "New product type"
msgstr "Nouveau type de produit"
-#: counter/templates/counter/producttype_list.jinja:42
+#: counter/templates/counter/producttype_list.jinja:25
+msgid "Product types are in the same order on this page and on the eboutic."
+msgstr "Les types de produit sont dans le même ordre sur cette page et sur l'eboutic."
+
+#: counter/templates/counter/producttype_list.jinja:28
+msgid ""
+"You can reorder them here by drag-and-drop. The changes will then be applied "
+"globally immediately."
+msgstr ""
+"Vous pouvez les réorganiser ici. Les changements seront alors immédiatement "
+"appliqués globalement."
+
+#: counter/templates/counter/producttype_list.jinja:58
msgid "There is no product types in this website."
msgstr "Il n'y a pas de types de produit dans ce site web."
diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po
index c89f7eb1..414bb603 100644
--- a/locale/fr/LC_MESSAGES/djangojs.po
+++ b/locale/fr/LC_MESSAGES/djangojs.po
@@ -123,8 +123,8 @@ msgid "captured.%s"
msgstr "capture.%s"
#: counter/static/bundled/counter/product-type-index.ts:36
-msgid "Products types successfully reordered"
-msgstr "Types de produits réordonnés."
+msgid "Products types reordered!"
+msgstr "Types de produits réordonnés !"
#: counter/static/bundled/counter/product-type-index.ts:40
#, javascript-format
From be6a077c8e57b0146a053b26f2ba4af0b1d2e2f8 Mon Sep 17 00:00:00 2001
From: imperosol
Date: Wed, 18 Dec 2024 14:13:39 +0100
Subject: [PATCH 08/17] fix access to the subscription page
---
core/models.py | 6 ++----
subscription/tests/test_new_susbcription.py | 15 +++++++++++----
2 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/core/models.py b/core/models.py
index 7a9d3a46..2d578d6d 100644
--- a/core/models.py
+++ b/core/models.py
@@ -530,10 +530,8 @@ class User(AbstractBaseUser):
@cached_property
def can_create_subscription(self) -> bool:
- from club.models import Membership
-
- return (
- Membership.objects.board()
+ return self.is_root or (
+ self.memberships.board()
.ongoing()
.filter(club_id__in=settings.SITH_CAN_CREATE_SUBSCRIPTIONS)
.exists()
diff --git a/subscription/tests/test_new_susbcription.py b/subscription/tests/test_new_susbcription.py
index 8ea51d68..ccdff407 100644
--- a/subscription/tests/test_new_susbcription.py
+++ b/subscription/tests/test_new_susbcription.py
@@ -90,13 +90,20 @@ def test_form_new_user(settings: SettingsWrapper):
@pytest.mark.django_db
@pytest.mark.parametrize(
- "user_factory", [lambda: baker.make(User, is_superuser=True), board_user.make]
+ ("user_factory", "status_code"),
+ [
+ (lambda: baker.make(User, is_superuser=True), 200),
+ (board_user.make, 200),
+ (subscriber_user.make, 403),
+ ],
)
-def test_load_page(client: Client, user_factory: Callable[[], User]):
- """Just check the page doesn't crash."""
+def test_page_access(
+ client: Client, user_factory: Callable[[], User], status_code: int
+):
+ """Check that only authorized users may access this page."""
client.force_login(user_factory())
res = client.get(reverse("subscription:subscription"))
- assert res.status_code == 200
+ assert res.status_code == status_code
@pytest.mark.django_db
From 5da27bb26657a1012822579373beaea4065b82c4 Mon Sep 17 00:00:00 2001
From: imperosol
Date: Wed, 18 Dec 2024 12:16:24 +0100
Subject: [PATCH 09/17] rename `producttype` to `product_type`
---
core/templates/core/user_tools.jinja | 2 +-
counter/models.py | 2 +-
counter/schemas.py | 2 +-
...cttype_list.jinja => product_type_list.jinja} | 16 ++++++++++------
counter/urls.py | 12 ++++++------
counter/views/admin.py | 3 ++-
counter/views/mixins.py | 2 +-
locale/fr/LC_MESSAGES/django.po | 14 +++++++-------
8 files changed, 29 insertions(+), 24 deletions(-)
rename counter/templates/counter/{producttype_list.jinja => product_type_list.jinja} (77%)
diff --git a/core/templates/core/user_tools.jinja b/core/templates/core/user_tools.jinja
index e00b24da..d9a6c0c7 100644
--- a/core/templates/core/user_tools.jinja
+++ b/core/templates/core/user_tools.jinja
@@ -52,7 +52,7 @@
%}
{% trans %}General counters management{% endtrans %}
{% trans %}Products management{% endtrans %}
- {% trans %}Product types management{% endtrans %}
+ {% trans %}Product types management{% endtrans %}
{% trans %}Cash register summaries{% endtrans %}
{% trans %}Invoices call{% endtrans %}
{% trans %}Etickets{% endtrans %}
diff --git a/counter/models.py b/counter/models.py
index b55207fb..48bb841f 100644
--- a/counter/models.py
+++ b/counter/models.py
@@ -315,7 +315,7 @@ class ProductType(OrderedModel):
return self.name
def get_absolute_url(self):
- return reverse("counter:producttype_list")
+ return reverse("counter:product_type_list")
def is_owned_by(self, user):
"""Method to see if that object can be edited by the given user."""
diff --git a/counter/schemas.py b/counter/schemas.py
index 1b770a3d..adc8094b 100644
--- a/counter/schemas.py
+++ b/counter/schemas.py
@@ -38,7 +38,7 @@ class ProductTypeSchema(ModelSchema):
@staticmethod
def resolve_url(obj: ProductType) -> str:
- return reverse("counter:producttype_edit", kwargs={"type_id": obj.id})
+ return reverse("counter:product_type_edit", kwargs={"type_id": obj.id})
class SimpleProductTypeSchema(ModelSchema):
diff --git a/counter/templates/counter/producttype_list.jinja b/counter/templates/counter/product_type_list.jinja
similarity index 77%
rename from counter/templates/counter/producttype_list.jinja
rename to counter/templates/counter/product_type_list.jinja
index 042925df..68548829 100644
--- a/counter/templates/counter/producttype_list.jinja
+++ b/counter/templates/counter/product_type_list.jinja
@@ -14,12 +14,12 @@
{% block content %}
-
+
{% trans %}New product type{% endtrans %}
- {% if producttype_list %}
+ {% if product_types %}
{% else %}
- {% trans %}There is no product types in this website.{% endtrans %}
+
+ {% trans %}There are no product types in this website.{% endtrans %}
+
{% endif %}
{% endblock %}
diff --git a/counter/urls.py b/counter/urls.py
index 0ca77f73..91564a8b 100644
--- a/counter/urls.py
+++ b/counter/urls.py
@@ -121,19 +121,19 @@ urlpatterns = [
name="product_edit",
),
path(
- "admin/producttype/list/",
+ "admin/product-type/list/",
ProductTypeListView.as_view(),
- name="producttype_list",
+ name="product_type_list",
),
path(
- "admin/producttype/create/",
+ "admin/product-type/create/",
ProductTypeCreateView.as_view(),
- name="new_producttype",
+ name="new_product_type",
),
path(
- "admin/producttype//",
+ "admin/product-type//",
ProductTypeEditView.as_view(),
- name="producttype_edit",
+ name="product_type_edit",
),
path("admin/eticket/list/", EticketListView.as_view(), name="eticket_list"),
path("admin/eticket/new/", EticketCreateView.as_view(), name="new_eticket"),
diff --git a/counter/views/admin.py b/counter/views/admin.py
index c1f5c63b..aa7e2c50 100644
--- a/counter/views/admin.py
+++ b/counter/views/admin.py
@@ -101,8 +101,9 @@ class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
"""A list view for the admins."""
model = ProductType
- template_name = "counter/producttype_list.jinja"
+ template_name = "counter/product_type_list.jinja"
current_tab = "product_types"
+ context_object_name = "product_types"
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
diff --git a/counter/views/mixins.py b/counter/views/mixins.py
index 4a07d848..2e88f54c 100644
--- a/counter/views/mixins.py
+++ b/counter/views/mixins.py
@@ -99,7 +99,7 @@ class CounterAdminTabsMixin(TabedViewMixin):
"name": _("Archived products"),
},
{
- "url": reverse_lazy("counter:producttype_list"),
+ "url": reverse_lazy("counter:product_type_list"),
"slug": "product_types",
"name": _("Product types"),
},
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 98b4fbd7..8ef1ade5 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -4197,20 +4197,20 @@ msgstr "Sans catégorie"
msgid "There is no products in this website."
msgstr "Il n'y a pas de produits dans ce site web."
-#: counter/templates/counter/producttype_list.jinja:4
-#: counter/templates/counter/producttype_list.jinja:42
+#: counter/templates/counter/product_type_list.jinja:4
+#: counter/templates/counter/product_type_list.jinja:42
msgid "Product type list"
msgstr "Liste des types de produit"
-#: counter/templates/counter/producttype_list.jinja:18
+#: counter/templates/counter/product_type_list.jinja:18
msgid "New product type"
msgstr "Nouveau type de produit"
-#: counter/templates/counter/producttype_list.jinja:25
+#: counter/templates/counter/product_type_list.jinja:25
msgid "Product types are in the same order on this page and on the eboutic."
msgstr "Les types de produit sont dans le même ordre sur cette page et sur l'eboutic."
-#: counter/templates/counter/producttype_list.jinja:28
+#: counter/templates/counter/product_type_list.jinja:28
msgid ""
"You can reorder them here by drag-and-drop. The changes will then be applied "
"globally immediately."
@@ -4218,8 +4218,8 @@ msgstr ""
"Vous pouvez les réorganiser ici. Les changements seront alors immédiatement "
"appliqués globalement."
-#: counter/templates/counter/producttype_list.jinja:58
-msgid "There is no product types in this website."
+#: counter/templates/counter/product_type_list.jinja:58
+msgid "There are no product types in this website."
msgstr "Il n'y a pas de types de produit dans ce site web."
#: counter/templates/counter/refilling_list.jinja:15
From 17e4c63737eb3b7d52bcf4e52417dcd955cd997c Mon Sep 17 00:00:00 2001
From: imperosol
Date: Mon, 16 Dec 2024 16:32:07 +0100
Subject: [PATCH 10/17] refactor news model and creation form
---
...r_news_club_alter_news_content_and_more.py | 56 ++++++
com/models.py | 25 ++-
com/templates/com/news_edit.jinja | 96 ++++++---
com/views.py | 6 +-
core/static/core/forms.scss | 33 +++-
core/static/core/style.scss | 15 --
locale/fr/LC_MESSAGES/django.po | 185 ++++++++++--------
7 files changed, 276 insertions(+), 140 deletions(-)
create mode 100644 com/migrations/0007_alter_news_club_alter_news_content_and_more.py
diff --git a/com/migrations/0007_alter_news_club_alter_news_content_and_more.py b/com/migrations/0007_alter_news_club_alter_news_content_and_more.py
new file mode 100644
index 00000000..99145cb7
--- /dev/null
+++ b/com/migrations/0007_alter_news_club_alter_news_content_and_more.py
@@ -0,0 +1,56 @@
+# Generated by Django 4.2.17 on 2024-12-16 14:51
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("club", "0011_auto_20180426_2013"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("com", "0006_remove_sith_index_page"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="news",
+ name="club",
+ field=models.ForeignKey(
+ help_text="The club which organizes the event.",
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="news",
+ to="club.club",
+ verbose_name="club",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="news",
+ name="content",
+ field=models.TextField(
+ blank=True,
+ default="",
+ help_text="A more detailed and exhaustive description of the event.",
+ verbose_name="content",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="news",
+ name="moderator",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="moderated_news",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="moderator",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="news",
+ name="summary",
+ field=models.TextField(
+ help_text="A description of the event (what is the activity ? is there an associated clic ? is there a inscription form ?)",
+ verbose_name="summary",
+ ),
+ ),
+ ]
diff --git a/com/models.py b/com/models.py
index 5c1466ca..cf2cb961 100644
--- a/com/models.py
+++ b/com/models.py
@@ -62,16 +62,31 @@ NEWS_TYPES = [
class News(models.Model):
- """The news class."""
+ """News about club events."""
title = models.CharField(_("title"), max_length=64)
- summary = models.TextField(_("summary"))
- content = models.TextField(_("content"))
+ summary = models.TextField(
+ _("summary"),
+ help_text=_(
+ "A description of the event (what is the activity ? "
+ "is there an associated clic ? is there a inscription form ?)"
+ ),
+ )
+ content = models.TextField(
+ _("content"),
+ blank=True,
+ default="",
+ help_text=_("A more detailed and exhaustive description of the event."),
+ )
type = models.CharField(
_("type"), max_length=16, choices=NEWS_TYPES, default="EVENT"
)
club = models.ForeignKey(
- Club, related_name="news", verbose_name=_("club"), on_delete=models.CASCADE
+ Club,
+ related_name="news",
+ verbose_name=_("club"),
+ on_delete=models.CASCADE,
+ help_text=_("The club which organizes the event."),
)
author = models.ForeignKey(
User,
@@ -85,7 +100,7 @@ class News(models.Model):
related_name="moderated_news",
verbose_name=_("moderator"),
null=True,
- on_delete=models.CASCADE,
+ on_delete=models.SET_NULL,
)
def __str__(self):
diff --git a/com/templates/com/news_edit.jinja b/com/templates/com/news_edit.jinja
index 858ec9a8..74040dc8 100644
--- a/com/templates/com/news_edit.jinja
+++ b/com/templates/com/news_edit.jinja
@@ -34,43 +34,90 @@
{% csrf_token %}
{{ form.non_field_errors() }}
{{ form.author }}
- {{ form.type.errors }}
+
+ {{ form.type.errors }}
+
- {% trans %}Notice: Information, election result - no date{% endtrans %}
- {% trans %}Event: punctual event, associated with one date{% endtrans %}
- - {% trans %}Weekly: recurrent event, associated with many dates (specify the first one, and a deadline){% endtrans %}
- - {% trans %}Call: long time event, associated with a long date (election appliance, ...){% endtrans %}
+ -
+ {% trans trimmed%}
+ Weekly: recurrent event, associated with many dates
+ (specify the first one, and a deadline)
+ {% endtrans %}
+
+ -
+ {% trans trimmed %}
+ Call: long time event, associated with a long date (like election appliance)
+ {% endtrans %}
+
- {{ form.type }}
- {{ form.start_date.errors }} {{ form.start_date }}
- {{ form.end_date.errors }} {{ form.end_date }}
- {{ form.until.errors }} {{ form.until }}
- {{ form.title.errors }} {{ form.title }}
- {{ form.club.errors }} {{ form.club }}
- {{ form.summary.errors }} {{ form.summary }}
- {{ form.content.errors }} {{ form.content }}
+ {{ form.type }}
+
+
+ {{ form.start_date.errors }}
+
+ {{ form.start_date }}
+
+
+ {{ form.end_date.errors }}
+
+ {{ form.end_date }}
+
+
+ {{ form.until.errors }}
+
+ {{ form.until }}
+
+
+ {{ form.title.errors }}
+
+ {{ form.title }}
+
+
+ {{ form.club.errors }}
+
+ {{ form.club.help_text }}
+ {{ form.club }}
+
+
+ {{ form.summary.errors }}
+
+ {{ form.summary.help_text }}
+ {{ form.summary }}
+
+
+ {{ form.content.errors }}
+
+ {{ form.content.help_text }}
+ {{ form.content }}
+
{% if user.is_com_admin %}
- {{ form.automoderation.errors }}
- {{ form.automoderation }}
+
+ {{ form.automoderation.errors }}
+
+ {{ form.automoderation }}
+
{% endif %}
-
-
+
+
{% endblock %}
{% block script %}
{{ super() }}
{% endblock %}
diff --git a/com/views.py b/com/views.py
index 69ee7221..c3835605 100644
--- a/com/views.py
+++ b/com/views.py
@@ -223,15 +223,13 @@ class NewsForm(forms.ModelForm):
):
self.add_error(
"end_date",
- ValidationError(
- _("You crazy? You can not finish an event before starting it.")
- ),
+ ValidationError(_("An event cannot end before its beginning.")),
)
if self.cleaned_data["type"] == "WEEKLY" and not self.cleaned_data["until"]:
self.add_error("until", ValidationError(_("This field is required.")))
return self.cleaned_data
- def save(self):
+ def save(self, *args, **kwargs):
ret = super().save()
self.instance.dates.all().delete()
if self.instance.type == "EVENT" or self.instance.type == "CALL":
diff --git a/core/static/core/forms.scss b/core/static/core/forms.scss
index e439bd8d..42a4d719 100644
--- a/core/static/core/forms.scss
+++ b/core/static/core/forms.scss
@@ -88,20 +88,37 @@ a:not(.button) {
}
}
-
form {
- .row {
- label {
- margin: unset;
- }
+ margin: 0 auto 10px;
+
+ .helptext {
+ margin-top: .25rem;
+ margin-bottom: .25rem;
+ font-size: 80%;
}
fieldset {
margin-bottom: 1rem;
}
- .helptext {
- margin-top: .25rem;
- font-size: 80%;
+ .row {
+ label {
+ margin: unset;
+ }
+ }
+
+ label {
+ display: block;
+ margin-bottom: 8px;
+
+ &.required:after {
+ margin-left: 4px;
+ content: "*";
+ color: red;
+ }
+ }
+
+ .choose_file_widget {
+ display: none;
}
}
diff --git a/core/static/core/style.scss b/core/static/core/style.scss
index dd44fda0..d7a396d1 100644
--- a/core/static/core/style.scss
+++ b/core/static/core/style.scss
@@ -1412,21 +1412,6 @@ footer {
}
}
-/*---------------------------------FORMS-------------------------------*/
-
-form {
- margin: 0 auto;
- margin-bottom: 10px;
-}
-
-label {
- display: block;
- margin-bottom: 8px;
-}
-
-.choose_file_widget {
- display: none;
-}
.ui-dialog .ui-dialog-buttonpane {
bottom: 0;
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 8ef1ade5..ae6ed43a 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-12-17 17:04+0100\n"
+"POT-Creation-Date: 2024-12-18 15:53+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal \n"
@@ -17,8 +17,8 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: accounting/models.py:62 accounting/models.py:101 accounting/models.py:132
-#: accounting/models.py:190 club/models.py:55 com/models.py:274
-#: com/models.py:293 counter/models.py:299 counter/models.py:330
+#: accounting/models.py:190 club/models.py:55 com/models.py:289
+#: com/models.py:308 counter/models.py:299 counter/models.py:330
#: counter/models.py:481 forum/models.py:60 launderette/models.py:29
#: launderette/models.py:80 launderette/models.py:116
msgid "name"
@@ -65,7 +65,7 @@ msgid "account number"
msgstr "numéro de compte"
#: accounting/models.py:107 accounting/models.py:136 club/models.py:345
-#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:359
+#: com/models.py:87 com/models.py:274 com/models.py:314 counter/models.py:359
#: counter/models.py:483 trombi/models.py:209
msgid "club"
msgstr "club"
@@ -126,8 +126,8 @@ msgstr "numéro"
msgid "journal"
msgstr "classeur"
-#: accounting/models.py:256 core/models.py:956 core/models.py:1467
-#: core/models.py:1512 core/models.py:1541 core/models.py:1565
+#: accounting/models.py:256 core/models.py:954 core/models.py:1465
+#: core/models.py:1510 core/models.py:1539 core/models.py:1563
#: counter/models.py:694 counter/models.py:798 counter/models.py:1002
#: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312
#: forum/models.py:413
@@ -165,7 +165,7 @@ msgid "accounting type"
msgstr "type comptable"
#: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460
-#: accounting/models.py:492 core/models.py:1540 core/models.py:1566
+#: accounting/models.py:492 core/models.py:1538 core/models.py:1564
#: counter/models.py:764
msgid "label"
msgstr "étiquette"
@@ -760,7 +760,7 @@ msgid "Linked operation:"
msgstr "Opération liée : "
#: accounting/templates/accounting/operation_edit.jinja:55
-#: com/templates/com/news_edit.jinja:57 com/templates/com/poster_edit.jinja:33
+#: com/templates/com/news_edit.jinja:103 com/templates/com/poster_edit.jinja:33
#: com/templates/com/screen_edit.jinja:25 com/templates/com/weekmail.jinja:74
#: core/templates/core/create.jinja:12 core/templates/core/edit.jinja:7
#: core/templates/core/edit.jinja:15 core/templates/core/edit.jinja:20
@@ -1068,11 +1068,11 @@ msgid "Enter a valid address. Only the root of the address is needed."
msgstr ""
"Entrez une adresse valide. Seule la racine de l'adresse est nécessaire."
-#: club/models.py:427 com/models.py:82 com/models.py:309 core/models.py:957
+#: club/models.py:427 com/models.py:97 com/models.py:324 core/models.py:955
msgid "is moderated"
msgstr "est modéré"
-#: club/models.py:431 com/models.py:86 com/models.py:313
+#: club/models.py:431 com/models.py:101 com/models.py:328
msgid "moderator"
msgstr "modérateur"
@@ -1426,80 +1426,96 @@ msgstr "Hebdomadaire"
msgid "Call"
msgstr "Appel"
-#: com/models.py:67 com/models.py:174 com/models.py:248
+#: com/models.py:67 com/models.py:189 com/models.py:263
#: core/templates/core/macros.jinja:301 election/models.py:12
#: election/models.py:114 election/models.py:152 forum/models.py:256
#: forum/models.py:310 pedagogy/models.py:97
msgid "title"
msgstr "titre"
-#: com/models.py:68
+#: com/models.py:69
msgid "summary"
msgstr "résumé"
-#: com/models.py:69 com/models.py:249 trombi/models.py:188
+#: com/models.py:71
+msgid ""
+"A description of the event (what is the activity ? is there an associated "
+"clic ? is there a inscription form ?)"
+msgstr ""
+"Une description de l'évènement (quelle est l'activité ? Y a-t-il un clic "
+"associé ? Y-a-t'il un formulaire d'inscription ?)"
+
+#: com/models.py:76 com/models.py:264 trombi/models.py:188
msgid "content"
msgstr "contenu"
-#: com/models.py:71 core/models.py:1510 launderette/models.py:88
+#: com/models.py:79
+msgid "A more detailed and exhaustive description of the event."
+msgstr "Une description plus détaillée et exhaustive de l'évènement."
+
+#: com/models.py:82 core/models.py:1508 launderette/models.py:88
#: launderette/models.py:124 launderette/models.py:167
msgid "type"
msgstr "type"
-#: com/models.py:79 com/models.py:253 pedagogy/models.py:57
+#: com/models.py:89
+msgid "The club which organizes the event."
+msgstr "Le club qui organise l'évènement."
+
+#: com/models.py:94 com/models.py:268 pedagogy/models.py:57
#: pedagogy/models.py:200 trombi/models.py:178
msgid "author"
msgstr "auteur"
-#: com/models.py:153
+#: com/models.py:168
msgid "news_date"
msgstr "date de la nouvelle"
-#: com/models.py:156
+#: com/models.py:171
msgid "start_date"
msgstr "date de début"
-#: com/models.py:157
+#: com/models.py:172
msgid "end_date"
msgstr "date de fin"
-#: com/models.py:175
+#: com/models.py:190
msgid "intro"
msgstr "intro"
-#: com/models.py:176
+#: com/models.py:191
msgid "joke"
msgstr "blague"
-#: com/models.py:177
+#: com/models.py:192
msgid "protip"
msgstr "astuce"
-#: com/models.py:178
+#: com/models.py:193
msgid "conclusion"
msgstr "conclusion"
-#: com/models.py:179
+#: com/models.py:194
msgid "sent"
msgstr "envoyé"
-#: com/models.py:244
+#: com/models.py:259
msgid "weekmail"
msgstr "weekmail"
-#: com/models.py:262
+#: com/models.py:277
msgid "rank"
msgstr "rang"
-#: com/models.py:295 core/models.py:922 core/models.py:972
+#: com/models.py:310 core/models.py:920 core/models.py:970
msgid "file"
msgstr "fichier"
-#: com/models.py:307
+#: com/models.py:322
msgid "display time"
msgstr "temps d'affichage"
-#: com/models.py:338
+#: com/models.py:353
msgid "Begin date should be before end date"
msgstr "La date de début doit être avant celle de fin"
@@ -1690,15 +1706,15 @@ msgstr "Éditer (sera soumise de nouveau à la modération)"
msgid "Edit news"
msgstr "Éditer la nouvelle"
-#: com/templates/com/news_edit.jinja:39
+#: com/templates/com/news_edit.jinja:41
msgid "Notice: Information, election result - no date"
msgstr "Information, résultat d'élection - sans date"
-#: com/templates/com/news_edit.jinja:40
+#: com/templates/com/news_edit.jinja:42
msgid "Event: punctual event, associated with one date"
msgstr "Événement : événement ponctuel associé à une date"
-#: com/templates/com/news_edit.jinja:41
+#: com/templates/com/news_edit.jinja:44
msgid ""
"Weekly: recurrent event, associated with many dates (specify the first one, "
"and a deadline)"
@@ -1706,14 +1722,14 @@ msgstr ""
"Hebdomadaire : événement récurrent, associé à plusieurs dates (spécifier la "
"première, ainsi que la date de fin)"
-#: com/templates/com/news_edit.jinja:42
+#: com/templates/com/news_edit.jinja:50
msgid ""
-"Call: long time event, associated with a long date (election appliance, ...)"
+"Call: long time event, associated with a long date (like election appliance)"
msgstr ""
-"Appel : événement de longue durée, associé à une longue date (candidature, "
-"concours, ...)"
+"Appel : événement de longue durée, associé à une longue date (comme des "
+"candidatures à une élection)"
-#: com/templates/com/news_edit.jinja:56 com/templates/com/weekmail.jinja:10
+#: com/templates/com/news_edit.jinja:102 com/templates/com/weekmail.jinja:10
msgid "Preview"
msgstr "Prévisualiser"
@@ -1952,23 +1968,23 @@ msgstr "Jusqu'à"
msgid "Automoderation"
msgstr "Automodération"
-#: com/views.py:213 com/views.py:217 com/views.py:231
+#: com/views.py:213 com/views.py:217 com/views.py:229
msgid "This field is required."
msgstr "Ce champ est obligatoire."
-#: com/views.py:227
-msgid "You crazy? You can not finish an event before starting it."
-msgstr "T'es fou? Un événement ne peut pas finir avant même de commencer."
+#: com/views.py:226
+msgid "An event cannot end before its beginning."
+msgstr "Un évènement ne peut pas se finir avant d'avoir commencé."
-#: com/views.py:451
+#: com/views.py:449
msgid "Delete and save to regenerate"
msgstr "Supprimer et sauver pour régénérer"
-#: com/views.py:466
+#: com/views.py:464
msgid "Weekmail of the "
msgstr "Weekmail du "
-#: com/views.py:570
+#: com/views.py:568
msgid ""
"You must be a board member of the selected club to post in the Weekmail."
msgstr ""
@@ -2202,11 +2218,11 @@ msgstr "adresse des parents"
msgid "is subscriber viewable"
msgstr "profil visible par les cotisants"
-#: core/models.py:594
+#: core/models.py:592
msgid "A user with that username already exists"
msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
-#: core/models.py:761 core/templates/core/macros.jinja:80
+#: core/models.py:759 core/templates/core/macros.jinja:80
#: core/templates/core/macros.jinja:84 core/templates/core/macros.jinja:85
#: core/templates/core/user_detail.jinja:100
#: core/templates/core/user_detail.jinja:101
@@ -2226,101 +2242,101 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
msgid "Profile"
msgstr "Profil"
-#: core/models.py:872
+#: core/models.py:870
msgid "Visitor"
msgstr "Visiteur"
-#: core/models.py:879
+#: core/models.py:877
msgid "receive the Weekmail"
msgstr "recevoir le Weekmail"
-#: core/models.py:880
+#: core/models.py:878
msgid "show your stats to others"
msgstr "montrez vos statistiques aux autres"
-#: core/models.py:882
+#: core/models.py:880
msgid "get a notification for every click"
msgstr "avoir une notification pour chaque click"
-#: core/models.py:885
+#: core/models.py:883
msgid "get a notification for every refilling"
msgstr "avoir une notification pour chaque rechargement"
-#: core/models.py:911 sas/forms.py:81
+#: core/models.py:909 sas/forms.py:81
msgid "file name"
msgstr "nom du fichier"
-#: core/models.py:915 core/models.py:1268
+#: core/models.py:913 core/models.py:1266
msgid "parent"
msgstr "parent"
-#: core/models.py:929
+#: core/models.py:927
msgid "compressed file"
msgstr "version allégée"
-#: core/models.py:936
+#: core/models.py:934
msgid "thumbnail"
msgstr "miniature"
-#: core/models.py:944 core/models.py:961
+#: core/models.py:942 core/models.py:959
msgid "owner"
msgstr "propriétaire"
-#: core/models.py:948 core/models.py:1285
+#: core/models.py:946 core/models.py:1283
msgid "edit group"
msgstr "groupe d'édition"
-#: core/models.py:951 core/models.py:1288
+#: core/models.py:949 core/models.py:1286
msgid "view group"
msgstr "groupe de vue"
-#: core/models.py:953
+#: core/models.py:951
msgid "is folder"
msgstr "est un dossier"
-#: core/models.py:954
+#: core/models.py:952
msgid "mime type"
msgstr "type mime"
-#: core/models.py:955
+#: core/models.py:953
msgid "size"
msgstr "taille"
-#: core/models.py:966
+#: core/models.py:964
msgid "asked for removal"
msgstr "retrait demandé"
-#: core/models.py:968
+#: core/models.py:966
msgid "is in the SAS"
msgstr "est dans le SAS"
-#: core/models.py:1037
+#: core/models.py:1035
msgid "Character '/' not authorized in name"
msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier"
-#: core/models.py:1039 core/models.py:1043
+#: core/models.py:1037 core/models.py:1041
msgid "Loop in folder tree"
msgstr "Boucle dans l'arborescence des dossiers"
-#: core/models.py:1046
+#: core/models.py:1044
msgid "You can not make a file be a children of a non folder file"
msgstr ""
"Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas "
"un dossier"
-#: core/models.py:1057
+#: core/models.py:1055
msgid "Duplicate file"
msgstr "Un fichier de ce nom existe déjà"
-#: core/models.py:1074
+#: core/models.py:1072
msgid "You must provide a file"
msgstr "Vous devez fournir un fichier"
-#: core/models.py:1251
+#: core/models.py:1249
msgid "page unix name"
msgstr "nom unix de la page"
-#: core/models.py:1257
+#: core/models.py:1255
msgid ""
"Enter a valid page name. This value may contain only unaccented letters, "
"numbers and ./+/-/_ characters."
@@ -2328,55 +2344,55 @@ msgstr ""
"Entrez un nom de page correct. Uniquement des lettres non accentuées, "
"numéros, et ./+/-/_"
-#: core/models.py:1275
+#: core/models.py:1273
msgid "page name"
msgstr "nom de la page"
-#: core/models.py:1280
+#: core/models.py:1278
msgid "owner group"
msgstr "groupe propriétaire"
-#: core/models.py:1293
+#: core/models.py:1291
msgid "lock user"
msgstr "utilisateur bloquant"
-#: core/models.py:1300
+#: core/models.py:1298
msgid "lock_timeout"
msgstr "décompte du déblocage"
-#: core/models.py:1350
+#: core/models.py:1348
msgid "Duplicate page"
msgstr "Une page de ce nom existe déjà"
-#: core/models.py:1353
+#: core/models.py:1351
msgid "Loop in page tree"
msgstr "Boucle dans l'arborescence des pages"
-#: core/models.py:1464
+#: core/models.py:1462
msgid "revision"
msgstr "révision"
-#: core/models.py:1465
+#: core/models.py:1463
msgid "page title"
msgstr "titre de la page"
-#: core/models.py:1466
+#: core/models.py:1464
msgid "page content"
msgstr "contenu de la page"
-#: core/models.py:1507
+#: core/models.py:1505
msgid "url"
msgstr "url"
-#: core/models.py:1508
+#: core/models.py:1506
msgid "param"
msgstr "param"
-#: core/models.py:1513
+#: core/models.py:1511
msgid "viewed"
msgstr "vue"
-#: core/models.py:1571
+#: core/models.py:1569
msgid "operation type"
msgstr "type d'opération"
@@ -4208,7 +4224,8 @@ msgstr "Nouveau type de produit"
#: counter/templates/counter/product_type_list.jinja:25
msgid "Product types are in the same order on this page and on the eboutic."
-msgstr "Les types de produit sont dans le même ordre sur cette page et sur l'eboutic."
+msgstr ""
+"Les types de produit sont dans le même ordre sur cette page et sur l'eboutic."
#: counter/templates/counter/product_type_list.jinja:28
msgid ""
@@ -4218,7 +4235,7 @@ msgstr ""
"Vous pouvez les réorganiser ici. Les changements seront alors immédiatement "
"appliqués globalement."
-#: counter/templates/counter/product_type_list.jinja:58
+#: counter/templates/counter/product_type_list.jinja:61
msgid "There are no product types in this website."
msgstr "Il n'y a pas de types de produit dans ce site web."
From 6ca641ab7fa1e0f788440c1b421931cb6c2cbd2f Mon Sep 17 00:00:00 2001
From: imperosol
Date: Thu, 19 Dec 2024 10:32:02 +0100
Subject: [PATCH 11/17] fix: N+1 queries on page version list page
---
core/templates/core/macros_pages.jinja | 23 ++++++++++++-----------
1 file changed, 12 insertions(+), 11 deletions(-)
diff --git a/core/templates/core/macros_pages.jinja b/core/templates/core/macros_pages.jinja
index c18bfe31..79228ae7 100644
--- a/core/templates/core/macros_pages.jinja
+++ b/core/templates/core/macros_pages.jinja
@@ -3,17 +3,18 @@
{% macro page_history(page) %}
{% trans page_name=page.name %}You're seeing the history of page "{{ page_name }}"{% endtrans %}
- {% for r in (page.revisions.all()|sort(attribute='date', reverse=True)) %}
- {% if loop.index < 2 %}
- - {% trans %}last{% endtrans %} -
- {{ user_profile_link(page.revisions.last().author) }} -
- {{ page.revisions.last().date|localtime|date(DATETIME_FORMAT) }} {{ page.revisions.last().date|localtime|time(DATETIME_FORMAT) }}
- {% else %}
- - {{ r.revision }} -
- {{ user_profile_link(r.author) }} -
- {{ r.date|localtime|date(DATETIME_FORMAT) }} {{ r.date|localtime|time(DATETIME_FORMAT) }}
- {% endif %}
- {% endfor %}
+ {% set page_name = page.get_full_name() %}
+ {%- for rev in page.revisions.order_by("-date").select_related("author") -%}
+ -
+ {% if loop.first %}
+ {% trans %}last{% endtrans %}
+ {% else %}
+ {{ rev.revision }}
+ {% endif %}
+ {{ user_profile_link(rev.author) }} -
+ {{ rev.date|localtime|date(DATETIME_FORMAT) }} {{ rev.date|localtime|time(DATETIME_FORMAT) }}
+
+ {%- endfor -%}
{% endmacro %}
From 8c660e9856f737e908a087ef34eb7fbc4b454cc5 Mon Sep 17 00:00:00 2001
From: imperosol
Date: Wed, 20 Nov 2024 17:10:57 +0100
Subject: [PATCH 12/17] Make `core.User` inherit from `AbstractUser` instead of
`AbstractBaseUser`
---
accounting/tests.py | 13 ++-
accounting/views.py | 20 ++---
club/models.py | 27 +++---
com/models.py | 26 +++---
com/tests.py | 6 +-
com/views.py | 52 ++++++-----
core/management/commands/populate.py | 10 +--
core/management/commands/populate_more.py | 6 +-
..._options_user_user_permissions_and_more.py | 82 ++++++++++++++++++
core/models.py | 86 ++++---------------
core/tests/test_core.py | 4 +-
core/tests/test_files.py | 8 +-
core/views/files.py | 26 +++---
core/views/forms.py | 4 +-
core/views/group.py | 4 +-
pedagogy/tests/test_api.py | 6 +-
pedagogy/views.py | 27 +++---
rootplace/tests.py | 8 +-
sas/tests/test_api.py | 6 +-
sas/tests/test_views.py | 6 +-
20 files changed, 215 insertions(+), 212 deletions(-)
create mode 100644 core/migrations/0040_alter_user_options_user_user_permissions_and_more.py
diff --git a/accounting/tests.py b/accounting/tests.py
index c66558e0..1140acc7 100644
--- a/accounting/tests.py
+++ b/accounting/tests.py
@@ -216,7 +216,7 @@ class TestOperation(TestCase):
self.journal.operations.filter(target_label="Le fantome du jour").exists()
)
- def test__operation_simple_accounting(self):
+ def test_operation_simple_accounting(self):
sat = SimplifiedAccountingType.objects.all().first()
response = self.client.post(
reverse("accounting:op_new", args=[self.journal.id]),
@@ -237,15 +237,14 @@ class TestOperation(TestCase):
"done": False,
},
)
- self.assertFalse(response.status_code == 403)
- self.assertTrue(self.journal.operations.filter(amount=23).exists())
+ assert response.status_code != 403
+ assert self.journal.operations.filter(amount=23).exists()
response_get = self.client.get(
reverse("accounting:journal_details", args=[self.journal.id])
)
- self.assertTrue(
- "Le fantome de l'aurore | " in str(response_get.content)
- )
- self.assertTrue(
+ assert "Le fantome de l'aurore | " in str(response_get.content)
+
+ assert (
self.journal.operations.filter(amount=23)
.values("accounting_type")
.first()["accounting_type"]
diff --git a/accounting/views.py b/accounting/views.py
index 928dc009..ce0ae45b 100644
--- a/accounting/views.py
+++ b/accounting/views.py
@@ -215,17 +215,14 @@ class JournalTabsMixin(TabedViewMixin):
return _("Journal")
def get_list_of_tabs(self):
- tab_list = []
- tab_list.append(
+ return [
{
"url": reverse(
"accounting:journal_details", kwargs={"j_id": self.object.id}
),
"slug": "journal",
"name": _("Journal"),
- }
- )
- tab_list.append(
+ },
{
"url": reverse(
"accounting:journal_nature_statement",
@@ -233,9 +230,7 @@ class JournalTabsMixin(TabedViewMixin):
),
"slug": "nature_statement",
"name": _("Statement by nature"),
- }
- )
- tab_list.append(
+ },
{
"url": reverse(
"accounting:journal_person_statement",
@@ -243,9 +238,7 @@ class JournalTabsMixin(TabedViewMixin):
),
"slug": "person_statement",
"name": _("Statement by person"),
- }
- )
- tab_list.append(
+ },
{
"url": reverse(
"accounting:journal_accounting_statement",
@@ -253,9 +246,8 @@ class JournalTabsMixin(TabedViewMixin):
),
"slug": "accounting_statement",
"name": _("Accounting statement"),
- }
- )
- return tab_list
+ },
+ ]
class JournalCreateView(CanCreateMixin, CreateView):
diff --git a/club/models.py b/club/models.py
index 573fd176..5300057d 100644
--- a/club/models.py
+++ b/club/models.py
@@ -31,14 +31,14 @@ from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import RegexValidator, validate_email
from django.db import models, transaction
-from django.db.models import Q
+from django.db.models import Exists, OuterRef, Q
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.timezone import localdate
from django.utils.translation import gettext_lazy as _
-from core.models import Group, MetaGroup, Notification, Page, RealGroup, SithFile, User
+from core.models import Group, MetaGroup, Notification, Page, SithFile, User
# Create your models here.
@@ -438,19 +438,18 @@ class Mailing(models.Model):
def save(self, *args, **kwargs):
if not self.is_moderated:
- for user in (
- RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
- .first()
- .users.all()
+ unread_notif_subquery = Notification.objects.filter(
+ user=OuterRef("pk"), type="MAILING_MODERATION", viewed=False
+ )
+ for user in User.objects.filter(
+ ~Exists(unread_notif_subquery),
+ groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID],
):
- if not user.notifications.filter(
- type="MAILING_MODERATION", viewed=False
- ).exists():
- Notification(
- user=user,
- url=reverse("com:mailing_admin"),
- type="MAILING_MODERATION",
- ).save(*args, **kwargs)
+ Notification(
+ user=user,
+ url=reverse("com:mailing_admin"),
+ type="MAILING_MODERATION",
+ ).save(*args, **kwargs)
super().save(*args, **kwargs)
def clean(self):
diff --git a/com/models.py b/com/models.py
index cf2cb961..f3076174 100644
--- a/com/models.py
+++ b/com/models.py
@@ -34,7 +34,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from club.models import Club
-from core.models import Notification, Preferences, RealGroup, User
+from core.models import Notification, Preferences, User
class Sith(models.Model):
@@ -108,17 +108,15 @@ class News(models.Model):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
- for u in (
- RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
- .first()
- .users.all()
+ for user in User.objects.filter(
+ groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID]
):
- Notification(
- user=u,
+ Notification.objects.create(
+ user=user,
url=reverse("com:news_admin_list"),
type="NEWS_MODERATION",
param="1",
- ).save()
+ )
def get_absolute_url(self):
return reverse("com:news_detail", kwargs={"news_id": self.id})
@@ -336,16 +334,14 @@ class Poster(models.Model):
def save(self, *args, **kwargs):
if not self.is_moderated:
- for u in (
- RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
- .first()
- .users.all()
+ for user in User.objects.filter(
+ groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID]
):
- Notification(
- user=u,
+ Notification.objects.create(
+ user=user,
url=reverse("com:poster_moderate_list"),
type="POSTER_MODERATION",
- ).save()
+ )
return super().save(*args, **kwargs)
def clean(self, *args, **kwargs):
diff --git a/com/tests.py b/com/tests.py
index 1c39fa36..399eb0e8 100644
--- a/com/tests.py
+++ b/com/tests.py
@@ -23,7 +23,7 @@ from django.utils.translation import gettext as _
from club.models import Club, Membership
from com.models import News, Poster, Sith, Weekmail, WeekmailArticle
-from core.models import AnonymousUser, RealGroup, User
+from core.models import AnonymousUser, Group, User
@pytest.fixture()
@@ -49,9 +49,7 @@ class TestCom(TestCase):
@classmethod
def setUpTestData(cls):
cls.skia = User.objects.get(username="skia")
- cls.com_group = RealGroup.objects.filter(
- id=settings.SITH_GROUP_COM_ADMIN_ID
- ).first()
+ cls.com_group = Group.objects.get(id=settings.SITH_GROUP_COM_ADMIN_ID)
cls.skia.groups.set([cls.com_group])
def setUp(self):
diff --git a/com/views.py b/com/views.py
index c3835605..d4136d20 100644
--- a/com/views.py
+++ b/com/views.py
@@ -28,7 +28,7 @@ from smtplib import SMTPRecipientsRefused
from django import forms
from django.conf import settings
from django.core.exceptions import PermissionDenied, ValidationError
-from django.db.models import Max
+from django.db.models import Exists, Max, OuterRef
from django.forms.models import modelform_factory
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
@@ -42,7 +42,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView
from club.models import Club, Mailing
from com.models import News, NewsDate, Poster, Screen, Sith, Weekmail, WeekmailArticle
-from core.models import Notification, RealGroup, User
+from core.models import Notification, User
from core.views import (
CanCreateMixin,
CanEditMixin,
@@ -278,21 +278,18 @@ class NewsEditView(CanEditMixin, UpdateView):
else:
self.object.is_moderated = False
self.object.save()
- for u in (
- RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
- .first()
- .users.all()
+ unread_notif_subquery = Notification.objects.filter(
+ user=OuterRef("pk"), type="NEWS_MODERATION", viewed=False
+ )
+ for user in User.objects.filter(
+ ~Exists(unread_notif_subquery),
+ groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID],
):
- if not u.notifications.filter(
- type="NEWS_MODERATION", viewed=False
- ).exists():
- Notification(
- user=u,
- url=reverse(
- "com:news_detail", kwargs={"news_id": self.object.id}
- ),
- type="NEWS_MODERATION",
- ).save()
+ Notification.objects.create(
+ user=user,
+ url=self.object.get_absolute_url(),
+ type="NEWS_MODERATION",
+ )
return super().form_valid(form)
@@ -323,19 +320,18 @@ class NewsCreateView(CanCreateMixin, CreateView):
self.object.is_moderated = True
self.object.save()
else:
- for u in (
- RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
- .first()
- .users.all()
+ unread_notif_subquery = Notification.objects.filter(
+ user=OuterRef("pk"), type="NEWS_MODERATION", viewed=False
+ )
+ for user in User.objects.filter(
+ ~Exists(unread_notif_subquery),
+ groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID],
):
- if not u.notifications.filter(
- type="NEWS_MODERATION", viewed=False
- ).exists():
- Notification(
- user=u,
- url=reverse("com:news_admin_list"),
- type="NEWS_MODERATION",
- ).save()
+ Notification.objects.create(
+ user=user,
+ url=reverse("com:news_admin_list"),
+ type="NEWS_MODERATION",
+ )
return super().form_valid(form)
diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py
index 7098101a..5770c715 100644
--- a/core/management/commands/populate.py
+++ b/core/management/commands/populate.py
@@ -261,19 +261,19 @@ class Command(BaseCommand):
User.groups.through.objects.bulk_create(
[
User.groups.through(
- realgroup_id=settings.SITH_GROUP_COUNTER_ADMIN_ID, user=counter
+ group_id=settings.SITH_GROUP_COUNTER_ADMIN_ID, user=counter
),
User.groups.through(
- realgroup_id=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID, user=comptable
+ group_id=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID, user=comptable
),
User.groups.through(
- realgroup_id=settings.SITH_GROUP_COM_ADMIN_ID, user=comunity
+ group_id=settings.SITH_GROUP_COM_ADMIN_ID, user=comunity
),
User.groups.through(
- realgroup_id=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID, user=tutu
+ group_id=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID, user=tutu
),
User.groups.through(
- realgroup_id=settings.SITH_GROUP_SAS_ADMIN_ID, user=skia
+ group_id=settings.SITH_GROUP_SAS_ADMIN_ID, user=skia
),
]
)
diff --git a/core/management/commands/populate_more.py b/core/management/commands/populate_more.py
index eaac58c0..f8ac1cef 100644
--- a/core/management/commands/populate_more.py
+++ b/core/management/commands/populate_more.py
@@ -11,7 +11,7 @@ from django.utils.timezone import localdate, make_aware, now
from faker import Faker
from club.models import Club, Membership
-from core.models import RealGroup, User
+from core.models import Group, User
from counter.models import (
Counter,
Customer,
@@ -225,9 +225,7 @@ class Command(BaseCommand):
ae = Club.objects.get(unix_name="ae")
other_clubs = random.sample(list(Club.objects.all()), k=3)
groups = list(
- RealGroup.objects.filter(
- name__in=["Subscribers", "Old subscribers", "Public"]
- )
+ Group.objects.filter(name__in=["Subscribers", "Old subscribers", "Public"])
)
counters = list(
Counter.objects.filter(name__in=["Foyer", "MDE", "La Gommette", "Eboutic"])
diff --git a/core/migrations/0040_alter_user_options_user_user_permissions_and_more.py b/core/migrations/0040_alter_user_options_user_user_permissions_and_more.py
new file mode 100644
index 00000000..43e4911c
--- /dev/null
+++ b/core/migrations/0040_alter_user_options_user_user_permissions_and_more.py
@@ -0,0 +1,82 @@
+# Generated by Django 4.2.16 on 2024-11-20 16:22
+
+import django.contrib.auth.validators
+import django.utils.timezone
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("auth", "0012_alter_user_first_name_max_length"),
+ ("core", "0039_alter_user_managers"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="user",
+ options={"verbose_name": "user", "verbose_name_plural": "users"},
+ ),
+ migrations.AddField(
+ model_name="user",
+ name="user_permissions",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Specific permissions for this user.",
+ related_name="user_set",
+ related_query_name="user",
+ to="auth.permission",
+ verbose_name="user permissions",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="user",
+ name="date_joined",
+ field=models.DateTimeField(
+ default=django.utils.timezone.now, verbose_name="date joined"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="user",
+ name="is_superuser",
+ field=models.BooleanField(
+ default=False,
+ help_text="Designates that this user has all permissions without explicitly assigning them.",
+ verbose_name="superuser status",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="user",
+ name="username",
+ field=models.CharField(
+ error_messages={"unique": "A user with that username already exists."},
+ help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
+ max_length=150,
+ unique=True,
+ validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
+ verbose_name="username",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="user",
+ name="groups",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
+ related_name="user_set",
+ related_query_name="user",
+ to="auth.group",
+ verbose_name="groups",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="user",
+ name="groups",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
+ related_name="users",
+ to="core.group",
+ verbose_name="groups",
+ ),
+ ),
+ ]
diff --git a/core/models.py b/core/models.py
index 2d578d6d..48355b67 100644
--- a/core/models.py
+++ b/core/models.py
@@ -30,19 +30,13 @@ import string
import unicodedata
from datetime import timedelta
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Optional, Self
+from typing import TYPE_CHECKING, Optional, Self
from django.conf import settings
-from django.contrib.auth.models import AbstractBaseUser, UserManager
-from django.contrib.auth.models import (
- AnonymousUser as AuthAnonymousUser,
-)
-from django.contrib.auth.models import (
- Group as AuthGroup,
-)
-from django.contrib.auth.models import (
- GroupManager as AuthGroupManager,
-)
+from django.contrib.auth.models import AbstractUser, UserManager
+from django.contrib.auth.models import AnonymousUser as AuthAnonymousUser
+from django.contrib.auth.models import Group as AuthGroup
+from django.contrib.auth.models import GroupManager as AuthGroupManager
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core import validators
from django.core.cache import cache
@@ -242,7 +236,7 @@ class CustomUserManager(UserManager.from_queryset(UserQuerySet)):
pass
-class User(AbstractBaseUser):
+class User(AbstractUser):
"""Defines the base user class, useable in every app.
This is almost the same as the auth module AbstractUser since it inherits from it,
@@ -253,51 +247,22 @@ class User(AbstractBaseUser):
Required fields: email, first_name, last_name, date_of_birth
"""
- username = models.CharField(
- _("username"),
- max_length=254,
- unique=True,
- help_text=_(
- "Required. 254 characters or fewer. Letters, digits and ./+/-/_ only."
- ),
- validators=[
- validators.RegexValidator(
- r"^[\w.+-]+$",
- _(
- "Enter a valid username. This value may contain only "
- "letters, numbers "
- "and ./+/-/_ characters."
- ),
- )
- ],
- error_messages={"unique": _("A user with that username already exists.")},
- )
first_name = models.CharField(_("first name"), max_length=64)
last_name = models.CharField(_("last name"), max_length=64)
email = models.EmailField(_("email address"), unique=True)
date_of_birth = models.DateField(_("date of birth"), blank=True, null=True)
nick_name = models.CharField(_("nick name"), max_length=64, null=True, blank=True)
- is_staff = models.BooleanField(
- _("staff status"),
- default=False,
- help_text=_("Designates whether the user can log into this admin site."),
- )
- is_active = models.BooleanField(
- _("active"),
- default=True,
- help_text=_(
- "Designates whether this user should be treated as active. "
- "Unselect this instead of deleting accounts."
- ),
- )
- date_joined = models.DateField(_("date joined"), auto_now_add=True)
last_update = models.DateTimeField(_("last update"), auto_now=True)
- is_superuser = models.BooleanField(
- _("superuser"),
- default=False,
- help_text=_("Designates whether this user is a superuser. "),
+ groups = models.ManyToManyField(
+ Group,
+ verbose_name=_("groups"),
+ help_text=_(
+ "The groups this user belongs to. A user will get all permissions "
+ "granted to each of their groups."
+ ),
+ related_name="users",
+ blank=True,
)
- groups = models.ManyToManyField(RealGroup, related_name="users", blank=True)
home = models.OneToOneField(
"SithFile",
related_name="home_of",
@@ -401,8 +366,6 @@ class User(AbstractBaseUser):
objects = CustomUserManager()
- USERNAME_FIELD = "username"
-
def __str__(self):
return self.get_display_name()
@@ -422,12 +385,6 @@ class User(AbstractBaseUser):
settings.BASE_DIR / f"core/static/core/img/promo_{self.promo}.png"
).exists()
- def has_module_perms(self, package_name: str) -> bool:
- return self.is_active
-
- def has_perm(self, perm: str, obj: Any = None) -> bool:
- return self.is_active and self.is_superuser
-
@cached_property
def was_subscribed(self) -> bool:
return self.subscriptions.exists()
@@ -599,11 +556,6 @@ class User(AbstractBaseUser):
"date_of_birth": self.date_of_birth,
}
- def get_full_name(self):
- """Returns the first_name plus the last_name, with a space in between."""
- full_name = "%s %s" % (self.first_name, self.last_name)
- return full_name.strip()
-
def get_short_name(self):
"""Returns the short name for the user."""
if self.nick_name:
@@ -982,13 +934,11 @@ class SithFile(models.Model):
if copy_rights:
self.copy_rights()
if self.is_in_sas:
- for u in (
- RealGroup.objects.filter(id=settings.SITH_GROUP_SAS_ADMIN_ID)
- .first()
- .users.all()
+ for user in User.objects.filter(
+ groups__id__in=[settings.SITH_GROUP_SAS_ADMIN_ID]
):
Notification(
- user=u,
+ user=user,
url=reverse("sas:moderation"),
type="SAS_MODERATION",
param="1",
diff --git a/core/tests/test_core.py b/core/tests/test_core.py
index 9b70e886..a33a8705 100644
--- a/core/tests/test_core.py
+++ b/core/tests/test_core.py
@@ -118,7 +118,9 @@ class TestUserRegistration:
response = client.post(reverse("core:register"), valid_payload)
assert response.status_code == 200
- error_html = "Un objet User avec ce champ Adresse email existe déjà."
+ error_html = (
+ "Un objet Utilisateur avec ce champ Adresse email existe déjà."
+ )
assertInHTML(error_html, str(response.content.decode()))
def test_register_fail_with_not_existing_email(
diff --git a/core/tests/test_files.py b/core/tests/test_files.py
index 1f39fcd8..998ceab5 100644
--- a/core/tests/test_files.py
+++ b/core/tests/test_files.py
@@ -14,7 +14,7 @@ from PIL import Image
from pytest_django.asserts import assertNumQueries
from core.baker_recipes import board_user, old_subscriber_user, subscriber_user
-from core.models import Group, RealGroup, SithFile, User
+from core.models import Group, SithFile, User
from sas.models import Picture
from sith import settings
@@ -26,12 +26,10 @@ class TestImageAccess:
[
lambda: baker.make(User, is_superuser=True),
lambda: baker.make(
- User,
- groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)],
+ User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
),
lambda: baker.make(
- User,
- groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_COM_ADMIN_ID)],
+ User, groups=[Group.objects.get(pk=settings.SITH_GROUP_COM_ADMIN_ID)]
),
],
)
diff --git a/core/views/files.py b/core/views/files.py
index 0d083a84..f8539080 100644
--- a/core/views/files.py
+++ b/core/views/files.py
@@ -21,6 +21,7 @@ from wsgiref.util import FileWrapper
from django import forms
from django.conf import settings
from django.core.exceptions import PermissionDenied
+from django.db.models import Exists, OuterRef
from django.forms.models import modelform_factory
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
@@ -31,7 +32,7 @@ from django.views.generic import DetailView, ListView
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import DeleteView, FormMixin, UpdateView
-from core.models import Notification, RealGroup, SithFile, User
+from core.models import Notification, SithFile, User
from core.views import (
AllowFragment,
CanEditMixin,
@@ -159,19 +160,18 @@ class AddFilesForm(forms.Form):
% {"file_name": f, "msg": repr(e)},
)
if notif:
- for u in (
- RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
- .first()
- .users.all()
+ unread_notif_subquery = Notification.objects.filter(
+ user=OuterRef("pk"), type="FILE_MODERATION", viewed=False
+ )
+ for user in User.objects.filter(
+ ~Exists(unread_notif_subquery),
+ groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID],
):
- if not u.notifications.filter(
- type="FILE_MODERATION", viewed=False
- ).exists():
- Notification(
- user=u,
- url=reverse("core:file_moderation"),
- type="FILE_MODERATION",
- ).save()
+ Notification.objects.create(
+ user=user,
+ url=reverse("core:file_moderation"),
+ type="FILE_MODERATION",
+ )
class FileListView(ListView):
diff --git a/core/views/forms.py b/core/views/forms.py
index de01f7aa..ea9c27f0 100644
--- a/core/views/forms.py
+++ b/core/views/forms.py
@@ -167,9 +167,7 @@ class RegisteringForm(UserCreationForm):
class Meta:
model = User
fields = ("first_name", "last_name", "email")
- field_classes = {
- "email": AntiSpamEmailField,
- }
+ field_classes = {"email": AntiSpamEmailField}
class UserProfileForm(forms.ModelForm):
diff --git a/core/views/group.py b/core/views/group.py
index abb0097f..b6e77b54 100644
--- a/core/views/group.py
+++ b/core/views/group.py
@@ -23,9 +23,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView
from core.models import RealGroup, User
from core.views import CanCreateMixin, CanEditMixin, DetailFormView
-from core.views.widgets.select import (
- AutoCompleteSelectMultipleUser,
-)
+from core.views.widgets.select import AutoCompleteSelectMultipleUser
# Forms
diff --git a/pedagogy/tests/test_api.py b/pedagogy/tests/test_api.py
index b8fb90b4..cbb99c18 100644
--- a/pedagogy/tests/test_api.py
+++ b/pedagogy/tests/test_api.py
@@ -8,7 +8,7 @@ from model_bakery import baker
from model_bakery.recipe import Recipe
from core.baker_recipes import subscriber_user
-from core.models import RealGroup, User
+from core.models import Group, User
from pedagogy.models import UV
@@ -80,9 +80,7 @@ class TestUVSearch(TestCase):
subscriber_user.make(),
baker.make(
User,
- groups=[
- RealGroup.objects.get(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
- ],
+ groups=[Group.objects.get(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)],
),
):
# users that have right
diff --git a/pedagogy/views.py b/pedagogy/views.py
index ca2c712e..99dd8168 100644
--- a/pedagogy/views.py
+++ b/pedagogy/views.py
@@ -24,6 +24,7 @@
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
+from django.db.models import Exists, OuterRef
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.views.generic import (
@@ -34,7 +35,7 @@ from django.views.generic import (
UpdateView,
)
-from core.models import Notification, RealGroup
+from core.models import Notification, User
from core.views import (
CanCreateMixin,
CanEditPropMixin,
@@ -156,21 +157,19 @@ class UVCommentReportCreateView(CanCreateMixin, CreateView):
def form_valid(self, form):
resp = super().form_valid(form)
-
# Send a message to moderation admins
- for user in (
- RealGroup.objects.filter(id=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
- .first()
- .users.all()
+ unread_notif_subquery = Notification.objects.filter(
+ user=OuterRef("pk"), type="PEDAGOGY_MODERATION", viewed=False
+ )
+ for user in User.objects.filter(
+ ~Exists(unread_notif_subquery),
+ groups__id__in=[settings.SITH_GROUP_PEDAGOGY_ADMIN_ID],
):
- if not user.notifications.filter(
- type="PEDAGOGY_MODERATION", viewed=False
- ).exists():
- Notification(
- user=user,
- url=reverse("pedagogy:moderation"),
- type="PEDAGOGY_MODERATION",
- ).save()
+ Notification.objects.create(
+ user=user,
+ url=reverse("pedagogy:moderation"),
+ type="PEDAGOGY_MODERATION",
+ )
return resp
diff --git a/rootplace/tests.py b/rootplace/tests.py
index 0d0f1542..a2bbee81 100644
--- a/rootplace/tests.py
+++ b/rootplace/tests.py
@@ -19,7 +19,7 @@ from django.urls import reverse
from django.utils.timezone import localtime, now
from club.models import Club
-from core.models import RealGroup, User
+from core.models import Group, User
from counter.models import Counter, Customer, Product, Refilling, Selling
from subscription.models import Subscription
@@ -50,9 +50,9 @@ class TestMergeUser(TestCase):
self.to_keep.address = "Jerusalem"
self.to_delete.parent_address = "Rome"
self.to_delete.address = "Rome"
- subscribers = RealGroup.objects.get(name="Subscribers")
- mde_admin = RealGroup.objects.get(name="MDE admin")
- sas_admin = RealGroup.objects.get(name="SAS admin")
+ subscribers = Group.objects.get(name="Subscribers")
+ mde_admin = Group.objects.get(name="MDE admin")
+ sas_admin = Group.objects.get(name="SAS admin")
self.to_keep.groups.add(subscribers.id)
self.to_delete.groups.add(mde_admin.id)
self.to_keep.groups.add(sas_admin.id)
diff --git a/sas/tests/test_api.py b/sas/tests/test_api.py
index 733838d2..9b24688b 100644
--- a/sas/tests/test_api.py
+++ b/sas/tests/test_api.py
@@ -7,7 +7,7 @@ from model_bakery import baker
from model_bakery.recipe import Recipe
from core.baker_recipes import old_subscriber_user, subscriber_user
-from core.models import RealGroup, SithFile, User
+from core.models import Group, SithFile, User
from sas.baker_recipes import picture_recipe
from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest
@@ -155,7 +155,7 @@ class TestPictureRelation(TestSas):
def test_delete_relation_with_authorized_users(self):
"""Test that deletion works as intended when called by an authorized user."""
relation: PeoplePictureRelation = self.user_a.pictures.first()
- sas_admin_group = RealGroup.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
+ sas_admin_group = Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
sas_admin = baker.make(User, groups=[sas_admin_group])
root = baker.make(User, is_superuser=True)
for user in sas_admin, root, self.user_a:
@@ -189,7 +189,7 @@ class TestPictureModeration(TestSas):
def setUpTestData(cls):
super().setUpTestData()
cls.sas_admin = baker.make(
- User, groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
+ User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
)
cls.picture = Picture.objects.filter(parent=cls.album_a)[0]
cls.picture.is_moderated = False
diff --git a/sas/tests/test_views.py b/sas/tests/test_views.py
index 2a73b90b..ff8dd21d 100644
--- a/sas/tests/test_views.py
+++ b/sas/tests/test_views.py
@@ -23,7 +23,7 @@ from model_bakery import baker
from pytest_django.asserts import assertInHTML, assertRedirects
from core.baker_recipes import old_subscriber_user, subscriber_user
-from core.models import RealGroup, User
+from core.models import Group, User
from sas.baker_recipes import picture_recipe
from sas.models import Album, Picture
@@ -38,7 +38,7 @@ from sas.models import Album, Picture
old_subscriber_user.make,
lambda: baker.make(User, is_superuser=True),
lambda: baker.make(
- User, groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
+ User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
),
lambda: baker.make(User),
],
@@ -80,7 +80,7 @@ class TestSasModeration(TestCase):
cls.to_moderate.is_moderated = False
cls.to_moderate.save()
cls.moderator = baker.make(
- User, groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
+ User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
)
cls.simple_user = subscriber_user.make()
From 7e9071a533dffdbb6e8303c141d38f1853f5727a Mon Sep 17 00:00:00 2001
From: imperosol
Date: Wed, 20 Nov 2024 17:25:39 +0100
Subject: [PATCH 13/17] optimize `User.is_subscribed` and `User.was_subscribed`
---
core/models.py | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/core/models.py b/core/models.py
index 48355b67..347e9bd4 100644
--- a/core/models.py
+++ b/core/models.py
@@ -387,14 +387,21 @@ class User(AbstractUser):
@cached_property
def was_subscribed(self) -> bool:
+ if "is_subscribed" in self.__dict__ and self.is_subscribed:
+ # if the user is currently subscribed, he is an old subscriber too
+ # if the property has already been cached, avoid another request
+ return True
return self.subscriptions.exists()
@cached_property
def is_subscribed(self) -> bool:
- s = self.subscriptions.filter(
+ if "was_subscribed" in self.__dict__ and not self.was_subscribed:
+ # if the user never subscribed, he cannot be a subscriber now.
+ # if the property has already been cached, avoid another request
+ return False
+ return self.subscriptions.filter(
subscription_start__lte=timezone.now(), subscription_end__gte=timezone.now()
- )
- return s.exists()
+ ).exists()
@cached_property
def account_balance(self):
From 871ef60cf6f21e16ee8affbbc3d256f856e317c6 Mon Sep 17 00:00:00 2001
From: imperosol
Date: Wed, 20 Nov 2024 18:35:55 +0100
Subject: [PATCH 14/17] remove obsolete RunPython operations
---
club/migrations/0010_auto_20170912_2028.py | 20 -----------
.../0013_customer_recorded_products.py | 33 -------------------
.../migrations/0002_auto_20190827_2251.py | 23 +++----------
3 files changed, 4 insertions(+), 72 deletions(-)
diff --git a/club/migrations/0010_auto_20170912_2028.py b/club/migrations/0010_auto_20170912_2028.py
index d6e28063..aa49fc4b 100644
--- a/club/migrations/0010_auto_20170912_2028.py
+++ b/club/migrations/0010_auto_20170912_2028.py
@@ -3,19 +3,6 @@ from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
-from club.models import Club
-from core.operations import PsqlRunOnly
-
-
-def generate_club_pages(apps, schema_editor):
- def recursive_generate_club_page(club):
- club.make_page()
- for child in Club.objects.filter(parent=club).all():
- recursive_generate_club_page(child)
-
- for club in Club.objects.filter(parent=None).all():
- recursive_generate_club_page(club)
-
class Migration(migrations.Migration):
dependencies = [("core", "0024_auto_20170906_1317"), ("club", "0010_club_logo")]
@@ -48,11 +35,4 @@ class Migration(migrations.Migration):
null=True,
),
),
- PsqlRunOnly(
- "SET CONSTRAINTS ALL IMMEDIATE", reverse_sql=migrations.RunSQL.noop
- ),
- migrations.RunPython(generate_club_pages),
- PsqlRunOnly(
- migrations.RunSQL.noop, reverse_sql="SET CONSTRAINTS ALL IMMEDIATE"
- ),
]
diff --git a/counter/migrations/0013_customer_recorded_products.py b/counter/migrations/0013_customer_recorded_products.py
index f2a48ba5..72825d16 100644
--- a/counter/migrations/0013_customer_recorded_products.py
+++ b/counter/migrations/0013_customer_recorded_products.py
@@ -1,38 +1,6 @@
from __future__ import unicode_literals
-from django.conf import settings
from django.db import migrations, models
-from django.utils.translation import gettext_lazy as _
-
-from core.models import User
-from counter.models import Counter, Customer, Product, Selling
-
-
-def balance_ecocups(apps, schema_editor):
- for customer in Customer.objects.all():
- customer.recorded_products = 0
- for selling in customer.buyings.filter(
- product__id__in=[settings.SITH_ECOCUP_CONS, settings.SITH_ECOCUP_DECO]
- ).all():
- if selling.product.is_record_product:
- customer.recorded_products += selling.quantity
- elif selling.product.is_unrecord_product:
- customer.recorded_products -= selling.quantity
- if customer.recorded_products < -settings.SITH_ECOCUP_LIMIT:
- qt = -(customer.recorded_products + settings.SITH_ECOCUP_LIMIT)
- cons = Product.objects.get(id=settings.SITH_ECOCUP_CONS)
- Selling(
- label=_("Ecocup regularization"),
- product=cons,
- unit_price=cons.selling_price,
- club=cons.club,
- counter=Counter.objects.filter(name="Foyer").first(),
- quantity=qt,
- seller=User.objects.get(id=0),
- customer=customer,
- ).save(allow_negative=True)
- customer.recorded_products += qt
- customer.save()
class Migration(migrations.Migration):
@@ -44,5 +12,4 @@ class Migration(migrations.Migration):
name="recorded_products",
field=models.IntegerField(verbose_name="recorded items", default=0),
),
- migrations.RunPython(balance_ecocups),
]
diff --git a/pedagogy/migrations/0002_auto_20190827_2251.py b/pedagogy/migrations/0002_auto_20190827_2251.py
index 2b1f4b36..7fbe05a8 100644
--- a/pedagogy/migrations/0002_auto_20190827_2251.py
+++ b/pedagogy/migrations/0002_auto_20190827_2251.py
@@ -25,26 +25,11 @@ from __future__ import unicode_literals
from django.db import migrations
-from core.models import User
-
-
-def remove_multiples_comments_from_same_user(apps, schema_editor):
- for user in User.objects.exclude(uv_comments=None).prefetch_related("uv_comments"):
- for uv in user.uv_comments.values("uv").distinct():
- last = (
- user.uv_comments.filter(uv__id=uv["uv"])
- .order_by("-publish_date")
- .first()
- )
- user.uv_comments.filter(uv__id=uv["uv"]).exclude(pk=last.pk).delete()
-
class Migration(migrations.Migration):
dependencies = [("pedagogy", "0001_initial")]
- operations = [
- migrations.RunPython(
- remove_multiples_comments_from_same_user,
- reverse_code=migrations.RunPython.noop,
- )
- ]
+ # This migration contained just a RunPython operation
+ # Which has since been removed.
+ # The migration itself is kept in order not to break the migration tree
+ operations = []
From a7b1406e06c0559454a2e6393c048a07d6f4f9a5 Mon Sep 17 00:00:00 2001
From: imperosol
Date: Thu, 19 Dec 2024 10:53:11 +0100
Subject: [PATCH 15/17] post-rebase fix
---
counter/tests/test_product.py | 8 +-
counter/tests/test_product_type.py | 8 +-
locale/fr/LC_MESSAGES/django.po | 402 +++++++++++++----------------
3 files changed, 184 insertions(+), 234 deletions(-)
diff --git a/counter/tests/test_product.py b/counter/tests/test_product.py
index a5eb39c4..8daab2b1 100644
--- a/counter/tests/test_product.py
+++ b/counter/tests/test_product.py
@@ -13,7 +13,7 @@ from PIL import Image
from pytest_django.asserts import assertNumQueries
from core.baker_recipes import board_user, subscriber_user
-from core.models import RealGroup, User
+from core.models import Group, User
from counter.models import Product, ProductType
@@ -51,16 +51,14 @@ def test_resize_product_icon(model):
(
lambda: baker.make(
User,
- groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
+ groups=[Group.objects.get(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
),
200,
),
(
lambda: baker.make(
User,
- groups=[
- RealGroup.objects.get(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
- ],
+ groups=[Group.objects.get(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)],
),
200,
),
diff --git a/counter/tests/test_product_type.py b/counter/tests/test_product_type.py
index 7976ef85..16dc40dd 100644
--- a/counter/tests/test_product_type.py
+++ b/counter/tests/test_product_type.py
@@ -6,7 +6,7 @@ from model_bakery import baker, seq
from ninja_extra.testing import TestClient
from core.baker_recipes import board_user, subscriber_user
-from core.models import RealGroup, User
+from core.models import Group, User
from counter.api import ProductTypeController
from counter.models import ProductType
@@ -70,16 +70,14 @@ def test_move_above_product_type(product_types: list[ProductType]):
(
lambda: baker.make(
User,
- groups=[RealGroup.objects.get(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
+ groups=[Group.objects.get(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)],
),
200,
),
(
lambda: baker.make(
User,
- groups=[
- RealGroup.objects.get(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
- ],
+ groups=[Group.objects.get(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)],
),
200,
),
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index ae6ed43a..015e3dd7 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-12-18 15:53+0100\n"
+"POT-Creation-Date: 2024-12-19 10:43+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal \n"
@@ -17,8 +17,8 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: accounting/models.py:62 accounting/models.py:101 accounting/models.py:132
-#: accounting/models.py:190 club/models.py:55 com/models.py:289
-#: com/models.py:308 counter/models.py:299 counter/models.py:330
+#: accounting/models.py:190 club/models.py:55 com/models.py:287
+#: com/models.py:306 counter/models.py:299 counter/models.py:330
#: counter/models.py:481 forum/models.py:60 launderette/models.py:29
#: launderette/models.py:80 launderette/models.py:116
msgid "name"
@@ -40,7 +40,7 @@ msgstr "code postal"
msgid "country"
msgstr "pays"
-#: accounting/models.py:67 core/models.py:391
+#: accounting/models.py:67 core/models.py:356
msgid "phone"
msgstr "téléphone"
@@ -65,7 +65,7 @@ msgid "account number"
msgstr "numéro de compte"
#: accounting/models.py:107 accounting/models.py:136 club/models.py:345
-#: com/models.py:87 com/models.py:274 com/models.py:314 counter/models.py:359
+#: com/models.py:87 com/models.py:272 com/models.py:312 counter/models.py:359
#: counter/models.py:483 trombi/models.py:209
msgid "club"
msgstr "club"
@@ -126,8 +126,8 @@ msgstr "numéro"
msgid "journal"
msgstr "classeur"
-#: accounting/models.py:256 core/models.py:954 core/models.py:1465
-#: core/models.py:1510 core/models.py:1539 core/models.py:1563
+#: accounting/models.py:256 core/models.py:913 core/models.py:1422
+#: core/models.py:1467 core/models.py:1496 core/models.py:1520
#: counter/models.py:694 counter/models.py:798 counter/models.py:1002
#: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312
#: forum/models.py:413
@@ -165,7 +165,7 @@ msgid "accounting type"
msgstr "type comptable"
#: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460
-#: accounting/models.py:492 core/models.py:1538 core/models.py:1564
+#: accounting/models.py:492 core/models.py:1495 core/models.py:1521
#: counter/models.py:764
msgid "label"
msgstr "étiquette"
@@ -174,7 +174,7 @@ msgstr "étiquette"
msgid "target type"
msgstr "type de cible"
-#: accounting/models.py:303 club/models.py:505
+#: accounting/models.py:303 club/models.py:504
#: club/templates/club/club_members.jinja:17
#: club/templates/club/club_old_members.jinja:8
#: club/templates/club/mailing.jinja:41
@@ -218,7 +218,7 @@ msgstr "Compte"
msgid "Company"
msgstr "Entreprise"
-#: accounting/models.py:307 core/models.py:338 sith/settings.py:423
+#: accounting/models.py:307 core/models.py:303 sith/settings.py:423
msgid "Other"
msgstr "Autre"
@@ -279,14 +279,14 @@ msgstr "type de mouvement"
#: accounting/models.py:433
#: accounting/templates/accounting/journal_statement_nature.jinja:9
#: accounting/templates/accounting/journal_statement_person.jinja:12
-#: accounting/views.py:574
+#: accounting/views.py:566
msgid "Credit"
msgstr "Crédit"
#: accounting/models.py:434
#: accounting/templates/accounting/journal_statement_nature.jinja:28
#: accounting/templates/accounting/journal_statement_person.jinja:40
-#: accounting/views.py:574
+#: accounting/views.py:566
msgid "Debit"
msgstr "Débit"
@@ -782,7 +782,7 @@ msgstr "Sauver"
#: accounting/templates/accounting/refound_account.jinja:4
#: accounting/templates/accounting/refound_account.jinja:9
-#: accounting/views.py:892
+#: accounting/views.py:884
msgid "Refound account"
msgstr "Remboursement de compte"
@@ -803,87 +803,87 @@ msgstr "Types simplifiés"
msgid "New simplified type"
msgstr "Nouveau type simplifié"
-#: accounting/views.py:215 accounting/views.py:225 accounting/views.py:549
+#: accounting/views.py:215 accounting/views.py:224 accounting/views.py:541
msgid "Journal"
msgstr "Classeur"
-#: accounting/views.py:235
+#: accounting/views.py:232
msgid "Statement by nature"
msgstr "Bilan par nature"
-#: accounting/views.py:245
+#: accounting/views.py:240
msgid "Statement by person"
msgstr "Bilan par personne"
-#: accounting/views.py:255
+#: accounting/views.py:248
msgid "Accounting statement"
msgstr "Bilan comptable"
-#: accounting/views.py:369
+#: accounting/views.py:361
msgid "Link this operation to the target account"
msgstr "Lier cette opération au compte cible"
-#: accounting/views.py:399
+#: accounting/views.py:391
msgid "The target must be set."
msgstr "La cible doit être indiquée."
-#: accounting/views.py:414
+#: accounting/views.py:406
msgid "The amount must be set."
msgstr "Le montant doit être indiqué."
-#: accounting/views.py:543 accounting/views.py:549
+#: accounting/views.py:535 accounting/views.py:541
msgid "Operation"
msgstr "Opération"
-#: accounting/views.py:558
+#: accounting/views.py:550
msgid "Financial proof: "
msgstr "Justificatif de libellé : "
-#: accounting/views.py:561
+#: accounting/views.py:553
#, python-format
msgid "Club: %(club_name)s"
msgstr "Club : %(club_name)s"
-#: accounting/views.py:566
+#: accounting/views.py:558
#, python-format
msgid "Label: %(op_label)s"
msgstr "Libellé : %(op_label)s"
-#: accounting/views.py:569
+#: accounting/views.py:561
#, python-format
msgid "Date: %(date)s"
msgstr "Date : %(date)s"
-#: accounting/views.py:577
+#: accounting/views.py:569
#, python-format
msgid "Amount: %(amount).2f €"
msgstr "Montant : %(amount).2f €"
-#: accounting/views.py:592
+#: accounting/views.py:584
msgid "Debtor"
msgstr "Débiteur"
-#: accounting/views.py:592
+#: accounting/views.py:584
msgid "Creditor"
msgstr "Créditeur"
-#: accounting/views.py:597
+#: accounting/views.py:589
msgid "Comment:"
msgstr "Commentaire :"
-#: accounting/views.py:622
+#: accounting/views.py:614
msgid "Signature:"
msgstr "Signature :"
-#: accounting/views.py:686
+#: accounting/views.py:678
msgid "General statement"
msgstr "Bilan général"
-#: accounting/views.py:693
+#: accounting/views.py:685
msgid "No label operations"
msgstr "Opérations sans étiquette"
-#: accounting/views.py:846
+#: accounting/views.py:838
msgid "Refound this account"
msgstr "Rembourser ce compte"
@@ -910,7 +910,7 @@ msgstr ""
msgid "Users to add"
msgstr "Utilisateurs à ajouter"
-#: club/forms.py:55 club/forms.py:181 core/views/group.py:42
+#: club/forms.py:55 club/forms.py:181 core/views/group.py:40
msgid "Search users to add (one or more)."
msgstr "Recherche les utilisateurs à ajouter (un ou plus)."
@@ -1025,11 +1025,11 @@ msgstr "actif"
msgid "short description"
msgstr "description courte"
-#: club/models.py:81 core/models.py:393
+#: club/models.py:81 core/models.py:358
msgid "address"
msgstr "Adresse"
-#: club/models.py:98 core/models.py:304
+#: club/models.py:98 core/models.py:269
msgid "home"
msgstr "home"
@@ -1046,20 +1046,20 @@ msgstr "Un club avec ce nom UNIX existe déjà."
#: launderette/models.py:130 launderette/models.py:184 sas/models.py:273
#: trombi/models.py:205
msgid "user"
-msgstr "nom d'utilisateur"
+msgstr "utilisateur"
-#: club/models.py:354 core/models.py:357 election/models.py:178
+#: club/models.py:354 core/models.py:322 election/models.py:178
#: election/models.py:212 trombi/models.py:210
msgid "role"
msgstr "rôle"
-#: club/models.py:359 core/models.py:90 counter/models.py:300
+#: club/models.py:359 core/models.py:84 counter/models.py:300
#: counter/models.py:331 election/models.py:13 election/models.py:115
#: election/models.py:188 forum/models.py:61 forum/models.py:245
msgid "description"
msgstr "description"
-#: club/models.py:415 club/models.py:511
+#: club/models.py:415 club/models.py:510
msgid "Email address"
msgstr "Adresse email"
@@ -1068,31 +1068,31 @@ msgid "Enter a valid address. Only the root of the address is needed."
msgstr ""
"Entrez une adresse valide. Seule la racine de l'adresse est nécessaire."
-#: club/models.py:427 com/models.py:97 com/models.py:324 core/models.py:955
+#: club/models.py:427 com/models.py:97 com/models.py:322 core/models.py:914
msgid "is moderated"
msgstr "est modéré"
-#: club/models.py:431 com/models.py:101 com/models.py:328
+#: club/models.py:431 com/models.py:101 com/models.py:326
msgid "moderator"
msgstr "modérateur"
-#: club/models.py:458
+#: club/models.py:457
msgid "This mailing list already exists."
msgstr "Cette liste de diffusion existe déjà."
-#: club/models.py:497 club/templates/club/mailing.jinja:23
+#: club/models.py:496 club/templates/club/mailing.jinja:23
msgid "Mailing"
msgstr "Liste de diffusion"
-#: club/models.py:521
+#: club/models.py:520
msgid "At least user or email is required"
msgstr "Au moins un utilisateur ou un email est nécessaire"
-#: club/models.py:529 club/tests.py:770
+#: club/models.py:528 club/tests.py:770
msgid "This email is already suscribed in this mailing"
msgstr "Cet email est déjà abonné à cette mailing"
-#: club/models.py:557
+#: club/models.py:556
msgid "Unregistered user"
msgstr "Utilisateur non enregistré"
@@ -1146,7 +1146,7 @@ msgid "There are no members in this club."
msgstr "Il n'y a pas de membres dans ce club."
#: club/templates/club/club_members.jinja:80
-#: core/templates/core/file_detail.jinja:19 core/views/forms.py:307
+#: core/templates/core/file_detail.jinja:19 core/views/forms.py:305
#: launderette/views.py:208 trombi/templates/trombi/detail.jinja:19
msgid "Add"
msgstr "Ajouter"
@@ -1426,7 +1426,7 @@ msgstr "Hebdomadaire"
msgid "Call"
msgstr "Appel"
-#: com/models.py:67 com/models.py:189 com/models.py:263
+#: com/models.py:67 com/models.py:187 com/models.py:261
#: core/templates/core/macros.jinja:301 election/models.py:12
#: election/models.py:114 election/models.py:152 forum/models.py:256
#: forum/models.py:310 pedagogy/models.py:97
@@ -1445,7 +1445,7 @@ msgstr ""
"Une description de l'évènement (quelle est l'activité ? Y a-t-il un clic "
"associé ? Y-a-t'il un formulaire d'inscription ?)"
-#: com/models.py:76 com/models.py:264 trombi/models.py:188
+#: com/models.py:76 com/models.py:262 trombi/models.py:188
msgid "content"
msgstr "contenu"
@@ -1453,7 +1453,7 @@ msgstr "contenu"
msgid "A more detailed and exhaustive description of the event."
msgstr "Une description plus détaillée et exhaustive de l'évènement."
-#: com/models.py:82 core/models.py:1508 launderette/models.py:88
+#: com/models.py:82 core/models.py:1465 launderette/models.py:88
#: launderette/models.py:124 launderette/models.py:167
msgid "type"
msgstr "type"
@@ -1462,60 +1462,60 @@ msgstr "type"
msgid "The club which organizes the event."
msgstr "Le club qui organise l'évènement."
-#: com/models.py:94 com/models.py:268 pedagogy/models.py:57
+#: com/models.py:94 com/models.py:266 pedagogy/models.py:57
#: pedagogy/models.py:200 trombi/models.py:178
msgid "author"
msgstr "auteur"
-#: com/models.py:168
+#: com/models.py:166
msgid "news_date"
msgstr "date de la nouvelle"
-#: com/models.py:171
+#: com/models.py:169
msgid "start_date"
msgstr "date de début"
-#: com/models.py:172
+#: com/models.py:170
msgid "end_date"
msgstr "date de fin"
-#: com/models.py:190
+#: com/models.py:188
msgid "intro"
msgstr "intro"
-#: com/models.py:191
+#: com/models.py:189
msgid "joke"
msgstr "blague"
-#: com/models.py:192
+#: com/models.py:190
msgid "protip"
msgstr "astuce"
-#: com/models.py:193
+#: com/models.py:191
msgid "conclusion"
msgstr "conclusion"
-#: com/models.py:194
+#: com/models.py:192
msgid "sent"
msgstr "envoyé"
-#: com/models.py:259
+#: com/models.py:257
msgid "weekmail"
msgstr "weekmail"
-#: com/models.py:277
+#: com/models.py:275
msgid "rank"
msgstr "rang"
-#: com/models.py:310 core/models.py:920 core/models.py:970
+#: com/models.py:308 core/models.py:879 core/models.py:929
msgid "file"
msgstr "fichier"
-#: com/models.py:322
+#: com/models.py:320
msgid "display time"
msgstr "temps d'affichage"
-#: com/models.py:353
+#: com/models.py:349
msgid "Begin date should be before end date"
msgstr "La date de début doit être avant celle de fin"
@@ -1766,7 +1766,7 @@ msgstr "Anniversaires"
msgid "%(age)s year old"
msgstr "%(age)s ans"
-#: com/templates/com/news_list.jinja:156 com/tests.py:103 com/tests.py:113
+#: com/templates/com/news_list.jinja:156 com/tests.py:101 com/tests.py:111
msgid "You need an up to date subscription to access this content"
msgstr "Votre cotisation doit être à jour pour accéder à cette section"
@@ -1976,253 +1976,211 @@ msgstr "Ce champ est obligatoire."
msgid "An event cannot end before its beginning."
msgstr "Un évènement ne peut pas se finir avant d'avoir commencé."
-#: com/views.py:449
+#: com/views.py:445
msgid "Delete and save to regenerate"
msgstr "Supprimer et sauver pour régénérer"
-#: com/views.py:464
+#: com/views.py:460
msgid "Weekmail of the "
msgstr "Weekmail du "
-#: com/views.py:568
+#: com/views.py:564
msgid ""
"You must be a board member of the selected club to post in the Weekmail."
msgstr ""
"Vous devez êtres un membre du bureau du club sélectionné pour poster dans le "
"Weekmail."
-#: core/models.py:85
+#: core/models.py:79
msgid "meta group status"
msgstr "status du meta-groupe"
-#: core/models.py:87
+#: core/models.py:81
msgid "Whether a group is a meta group or not"
msgstr "Si un groupe est un meta-groupe ou pas"
-#: core/models.py:173
+#: core/models.py:167
#, python-format
msgid "%(value)s is not a valid promo (between 0 and %(end)s)"
msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)"
-#: core/models.py:257
-msgid "username"
-msgstr "nom d'utilisateur"
-
-#: core/models.py:261
-msgid "Required. 254 characters or fewer. Letters, digits and ./+/-/_ only."
-msgstr ""
-"Requis. Pas plus de 254 caractères. Uniquement des lettres, numéros, et ./"
-"+/-/_"
-
-#: core/models.py:267
-msgid ""
-"Enter a valid username. This value may contain only letters, numbers and ./"
-"+/-/_ characters."
-msgstr ""
-"Entrez un nom d'utilisateur correct. Uniquement des lettres, numéros, et ./"
-"+/-/_"
-
-#: core/models.py:273
-msgid "A user with that username already exists."
-msgstr "Un utilisateur de ce nom existe déjà"
-
-#: core/models.py:275
+#: core/models.py:250
msgid "first name"
msgstr "Prénom"
-#: core/models.py:276
+#: core/models.py:251
msgid "last name"
msgstr "Nom"
-#: core/models.py:277
+#: core/models.py:252
msgid "email address"
msgstr "adresse email"
-#: core/models.py:278
+#: core/models.py:253
msgid "date of birth"
msgstr "date de naissance"
-#: core/models.py:279
+#: core/models.py:254
msgid "nick name"
msgstr "surnom"
-#: core/models.py:281
-msgid "staff status"
-msgstr "status \"staff\""
-
-#: core/models.py:283
-msgid "Designates whether the user can log into this admin site."
-msgstr "Est-ce que l'utilisateur peut se logger à la partie admin du site."
-
-#: core/models.py:286
-msgid "active"
-msgstr "actif"
-
-#: core/models.py:289
-msgid ""
-"Designates whether this user should be treated as active. Unselect this "
-"instead of deleting accounts."
-msgstr ""
-"Est-ce que l'utilisateur doit être traité comme actif. Désélectionnez au "
-"lieu de supprimer les comptes."
-
-#: core/models.py:293
-msgid "date joined"
-msgstr "date d'inscription"
-
-#: core/models.py:294
+#: core/models.py:255
msgid "last update"
msgstr "dernière mise à jour"
-#: core/models.py:296
-msgid "superuser"
-msgstr "super-utilisateur"
+#: core/models.py:258
+msgid "groups"
+msgstr "groupes"
-#: core/models.py:298
-msgid "Designates whether this user is a superuser. "
-msgstr "Est-ce que l'utilisateur est super-utilisateur."
+#: core/models.py:260
+msgid ""
+"The groups this user belongs to. A user will get all permissions granted to "
+"each of their groups."
+msgstr ""
+"Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes les "
+"permissions de chacun de ses groupes."
-#: core/models.py:312
+#: core/models.py:277
msgid "profile"
msgstr "profil"
-#: core/models.py:320
+#: core/models.py:285
msgid "avatar"
msgstr "avatar"
-#: core/models.py:328
+#: core/models.py:293
msgid "scrub"
msgstr "blouse"
-#: core/models.py:334
+#: core/models.py:299
msgid "sex"
msgstr "Genre"
-#: core/models.py:338
+#: core/models.py:303
msgid "Man"
msgstr "Homme"
-#: core/models.py:338
+#: core/models.py:303
msgid "Woman"
msgstr "Femme"
-#: core/models.py:340
+#: core/models.py:305
msgid "pronouns"
msgstr "pronoms"
-#: core/models.py:342
+#: core/models.py:307
msgid "tshirt size"
msgstr "taille de t-shirt"
-#: core/models.py:345
+#: core/models.py:310
msgid "-"
msgstr "-"
-#: core/models.py:346
+#: core/models.py:311
msgid "XS"
msgstr "XS"
-#: core/models.py:347
+#: core/models.py:312
msgid "S"
msgstr "S"
-#: core/models.py:348
+#: core/models.py:313
msgid "M"
msgstr "M"
-#: core/models.py:349
+#: core/models.py:314
msgid "L"
msgstr "L"
-#: core/models.py:350
+#: core/models.py:315
msgid "XL"
msgstr "XL"
-#: core/models.py:351
+#: core/models.py:316
msgid "XXL"
msgstr "XXL"
-#: core/models.py:352
+#: core/models.py:317
msgid "XXXL"
msgstr "XXXL"
-#: core/models.py:360
+#: core/models.py:325
msgid "Student"
msgstr "Étudiant"
-#: core/models.py:361
+#: core/models.py:326
msgid "Administrative agent"
msgstr "Personnel administratif"
-#: core/models.py:362
+#: core/models.py:327
msgid "Teacher"
msgstr "Enseignant"
-#: core/models.py:363
+#: core/models.py:328
msgid "Agent"
msgstr "Personnel"
-#: core/models.py:364
+#: core/models.py:329
msgid "Doctor"
msgstr "Doctorant"
-#: core/models.py:365
+#: core/models.py:330
msgid "Former student"
msgstr "Ancien étudiant"
-#: core/models.py:366
+#: core/models.py:331
msgid "Service"
msgstr "Service"
-#: core/models.py:372
+#: core/models.py:337
msgid "department"
msgstr "département"
-#: core/models.py:379
+#: core/models.py:344
msgid "dpt option"
msgstr "Filière"
-#: core/models.py:381 pedagogy/models.py:70 pedagogy/models.py:294
+#: core/models.py:346 pedagogy/models.py:70 pedagogy/models.py:294
msgid "semester"
msgstr "semestre"
-#: core/models.py:382
+#: core/models.py:347
msgid "quote"
msgstr "citation"
-#: core/models.py:383
+#: core/models.py:348
msgid "school"
msgstr "école"
-#: core/models.py:385
+#: core/models.py:350
msgid "promo"
msgstr "promo"
-#: core/models.py:388
+#: core/models.py:353
msgid "forum signature"
msgstr "signature du forum"
-#: core/models.py:390
+#: core/models.py:355
msgid "second email address"
msgstr "adresse email secondaire"
-#: core/models.py:392
+#: core/models.py:357
msgid "parent phone"
msgstr "téléphone des parents"
-#: core/models.py:395
+#: core/models.py:360
msgid "parent address"
msgstr "adresse des parents"
-#: core/models.py:398
+#: core/models.py:363
msgid "is subscriber viewable"
msgstr "profil visible par les cotisants"
-#: core/models.py:592
+#: core/models.py:556
msgid "A user with that username already exists"
msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
-#: core/models.py:759 core/templates/core/macros.jinja:80
+#: core/models.py:718 core/templates/core/macros.jinja:80
#: core/templates/core/macros.jinja:84 core/templates/core/macros.jinja:85
#: core/templates/core/user_detail.jinja:100
#: core/templates/core/user_detail.jinja:101
@@ -2242,101 +2200,101 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà"
msgid "Profile"
msgstr "Profil"
-#: core/models.py:870
+#: core/models.py:829
msgid "Visitor"
msgstr "Visiteur"
-#: core/models.py:877
+#: core/models.py:836
msgid "receive the Weekmail"
msgstr "recevoir le Weekmail"
-#: core/models.py:878
+#: core/models.py:837
msgid "show your stats to others"
msgstr "montrez vos statistiques aux autres"
-#: core/models.py:880
+#: core/models.py:839
msgid "get a notification for every click"
msgstr "avoir une notification pour chaque click"
-#: core/models.py:883
+#: core/models.py:842
msgid "get a notification for every refilling"
msgstr "avoir une notification pour chaque rechargement"
-#: core/models.py:909 sas/forms.py:81
+#: core/models.py:868 sas/forms.py:81
msgid "file name"
msgstr "nom du fichier"
-#: core/models.py:913 core/models.py:1266
+#: core/models.py:872 core/models.py:1223
msgid "parent"
msgstr "parent"
-#: core/models.py:927
+#: core/models.py:886
msgid "compressed file"
msgstr "version allégée"
-#: core/models.py:934
+#: core/models.py:893
msgid "thumbnail"
msgstr "miniature"
-#: core/models.py:942 core/models.py:959
+#: core/models.py:901 core/models.py:918
msgid "owner"
msgstr "propriétaire"
-#: core/models.py:946 core/models.py:1283
+#: core/models.py:905 core/models.py:1240
msgid "edit group"
msgstr "groupe d'édition"
-#: core/models.py:949 core/models.py:1286
+#: core/models.py:908 core/models.py:1243
msgid "view group"
msgstr "groupe de vue"
-#: core/models.py:951
+#: core/models.py:910
msgid "is folder"
msgstr "est un dossier"
-#: core/models.py:952
+#: core/models.py:911
msgid "mime type"
msgstr "type mime"
-#: core/models.py:953
+#: core/models.py:912
msgid "size"
msgstr "taille"
-#: core/models.py:964
+#: core/models.py:923
msgid "asked for removal"
msgstr "retrait demandé"
-#: core/models.py:966
+#: core/models.py:925
msgid "is in the SAS"
msgstr "est dans le SAS"
-#: core/models.py:1035
+#: core/models.py:992
msgid "Character '/' not authorized in name"
msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier"
-#: core/models.py:1037 core/models.py:1041
+#: core/models.py:994 core/models.py:998
msgid "Loop in folder tree"
msgstr "Boucle dans l'arborescence des dossiers"
-#: core/models.py:1044
+#: core/models.py:1001
msgid "You can not make a file be a children of a non folder file"
msgstr ""
"Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas "
"un dossier"
-#: core/models.py:1055
+#: core/models.py:1012
msgid "Duplicate file"
msgstr "Un fichier de ce nom existe déjà"
-#: core/models.py:1072
+#: core/models.py:1029
msgid "You must provide a file"
msgstr "Vous devez fournir un fichier"
-#: core/models.py:1249
+#: core/models.py:1206
msgid "page unix name"
msgstr "nom unix de la page"
-#: core/models.py:1255
+#: core/models.py:1212
msgid ""
"Enter a valid page name. This value may contain only unaccented letters, "
"numbers and ./+/-/_ characters."
@@ -2344,55 +2302,55 @@ msgstr ""
"Entrez un nom de page correct. Uniquement des lettres non accentuées, "
"numéros, et ./+/-/_"
-#: core/models.py:1273
+#: core/models.py:1230
msgid "page name"
msgstr "nom de la page"
-#: core/models.py:1278
+#: core/models.py:1235
msgid "owner group"
msgstr "groupe propriétaire"
-#: core/models.py:1291
+#: core/models.py:1248
msgid "lock user"
msgstr "utilisateur bloquant"
-#: core/models.py:1298
+#: core/models.py:1255
msgid "lock_timeout"
msgstr "décompte du déblocage"
-#: core/models.py:1348
+#: core/models.py:1305
msgid "Duplicate page"
msgstr "Une page de ce nom existe déjà"
-#: core/models.py:1351
+#: core/models.py:1308
msgid "Loop in page tree"
msgstr "Boucle dans l'arborescence des pages"
-#: core/models.py:1462
+#: core/models.py:1419
msgid "revision"
msgstr "révision"
-#: core/models.py:1463
+#: core/models.py:1420
msgid "page title"
msgstr "titre de la page"
-#: core/models.py:1464
+#: core/models.py:1421
msgid "page content"
msgstr "contenu de la page"
-#: core/models.py:1505
+#: core/models.py:1462
msgid "url"
msgstr "url"
-#: core/models.py:1506
+#: core/models.py:1463
msgid "param"
msgstr "param"
-#: core/models.py:1511
+#: core/models.py:1468
msgid "viewed"
msgstr "vue"
-#: core/models.py:1569
+#: core/models.py:1526
msgid "operation type"
msgstr "type d'opération"
@@ -2543,7 +2501,7 @@ msgid "Launderette"
msgstr "Laverie"
#: core/templates/core/base/navbar.jinja:28 core/templates/core/file.jinja:24
-#: core/views/files.py:121
+#: core/views/files.py:122
msgid "Files"
msgstr "Fichiers"
@@ -3508,16 +3466,16 @@ msgid_plural "%(nb_days)d days, %(remainder)s"
msgstr[0] ""
msgstr[1] ""
-#: core/views/files.py:118
+#: core/views/files.py:119
msgid "Add a new folder"
msgstr "Ajouter un nouveau dossier"
-#: core/views/files.py:138
+#: core/views/files.py:139
#, python-format
msgid "Error creating folder %(folder_name)s: %(msg)s"
msgstr "Erreur de création du dossier %(folder_name)s : %(msg)s"
-#: core/views/files.py:158 core/views/forms.py:272 core/views/forms.py:279
+#: core/views/files.py:159 core/views/forms.py:270 core/views/forms.py:277
#: sas/forms.py:60
#, python-format
msgid "Error uploading file %(file_name)s: %(msg)s"
@@ -3539,7 +3497,7 @@ msgstr "Choisir un utilisateur"
msgid "Username, email, or account number"
msgstr "Nom d'utilisateur, email, ou numéro de compte AE"
-#: core/views/forms.py:222
+#: core/views/forms.py:220
msgid ""
"Profile: you need to be visible on the picture, in order to be recognized (e."
"g. by the barmen)"
@@ -3547,53 +3505,53 @@ msgstr ""
"Photo de profil: vous devez être visible sur la photo afin d'être reconnu "
"(par exemple par les barmen)"
-#: core/views/forms.py:227
+#: core/views/forms.py:225
msgid "Avatar: used on the forum"
msgstr "Avatar : utilisé sur le forum"
-#: core/views/forms.py:231
+#: core/views/forms.py:229
msgid "Scrub: let other know how your scrub looks like!"
msgstr "Blouse : montrez aux autres à quoi ressemble votre blouse !"
-#: core/views/forms.py:283
+#: core/views/forms.py:281
msgid "Bad image format, only jpeg, png, webp and gif are accepted"
msgstr "Mauvais format d'image, seuls les jpeg, png, webp et gif sont acceptés"
-#: core/views/forms.py:304
+#: core/views/forms.py:302
msgid "Godfather / Godmother"
msgstr "Parrain / Marraine"
-#: core/views/forms.py:305
+#: core/views/forms.py:303
msgid "Godchild"
msgstr "Fillot / Fillote"
-#: core/views/forms.py:310 counter/forms.py:78 trombi/views.py:151
+#: core/views/forms.py:308 counter/forms.py:78 trombi/views.py:151
msgid "Select user"
msgstr "Choisir un utilisateur"
-#: core/views/forms.py:324
+#: core/views/forms.py:322
msgid "This user does not exist"
msgstr "Cet utilisateur n'existe pas"
-#: core/views/forms.py:326
+#: core/views/forms.py:324
msgid "You cannot be related to yourself"
msgstr "Vous ne pouvez pas être relié à vous-même"
-#: core/views/forms.py:338
+#: core/views/forms.py:336
#, python-format
msgid "%s is already your godfather"
msgstr "%s est déjà votre parrain/marraine"
-#: core/views/forms.py:344
+#: core/views/forms.py:342
#, python-format
msgid "%s is already your godchild"
msgstr "%s est déjà votre fillot/fillote"
-#: core/views/group.py:41
+#: core/views/group.py:39
msgid "Users to add to group"
msgstr "Utilisateurs à ajouter au groupe"
-#: core/views/group.py:50
+#: core/views/group.py:48
msgid "Users to remove from group"
msgstr "Utilisateurs à retirer du groupe"
@@ -3649,10 +3607,6 @@ msgstr "Votre compte AE a été vidé"
msgid "Clearing of your AE account"
msgstr "Vidange de votre compte AE"
-#: counter/migrations/0013_customer_recorded_products.py:25
-msgid "Ecocup regularization"
-msgstr "Régularization des ecocups"
-
#: counter/models.py:92
msgid "account id"
msgstr "numéro de compte"
From 87b619794d29c075573e5af77f198f4ce3885aba Mon Sep 17 00:00:00 2001
From: Sli
Date: Thu, 19 Dec 2024 18:57:50 +0100
Subject: [PATCH 16/17] Fix groups displayed on user profile group edition
---
core/views/forms.py | 11 +++++++----
core/views/user.py | 6 ++----
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/core/views/forms.py b/core/views/forms.py
index ea9c27f0..88963789 100644
--- a/core/views/forms.py
+++ b/core/views/forms.py
@@ -44,7 +44,7 @@ from phonenumber_field.widgets import RegionalPhoneNumberWidget
from PIL import Image
from antispam.forms import AntiSpamEmailField
-from core.models import Gift, Page, SithFile, User
+from core.models import Gift, Page, RealGroup, SithFile, User
from core.utils import resize_image
from core.views.widgets.select import (
AutoCompleteSelect,
@@ -285,15 +285,18 @@ class UserProfileForm(forms.ModelForm):
self._post_clean()
-class UserPropForm(forms.ModelForm):
+class UserRealGroupForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"
+ groups = forms.ModelChoiceField(
+ RealGroup.objects.all(),
+ widget=CheckboxSelectMultiple,
+ )
+
class Meta:
model = User
fields = ["groups"]
- help_texts = {"groups": "Which groups this user belongs to"}
- widgets = {"groups": CheckboxSelectMultiple}
class UserGodfathersForm(forms.Form):
diff --git a/core/views/user.py b/core/views/user.py
index 9f724fca..7b6c146b 100644
--- a/core/views/user.py
+++ b/core/views/user.py
@@ -35,7 +35,6 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.db.models import DateField, QuerySet
from django.db.models.functions import Trunc
-from django.forms import CheckboxSelectMultiple
from django.forms.models import modelform_factory
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
@@ -69,6 +68,7 @@ from core.views.forms import (
RegisteringForm,
UserGodfathersForm,
UserProfileForm,
+ UserRealGroupForm,
)
from counter.models import Refilling, Selling
from counter.views.student_card import StudentCardFormView
@@ -583,9 +583,7 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
model = User
pk_url_kwarg = "user_id"
template_name = "core/user_group.jinja"
- form_class = modelform_factory(
- User, fields=["groups"], widgets={"groups": CheckboxSelectMultiple}
- )
+ form_class = UserRealGroupForm
context_object_name = "profile"
current_tab = "groups"
From 9f3a10ca71391e0d93c271e8e45011dafc02acca Mon Sep 17 00:00:00 2001
From: imperosol
Date: Fri, 20 Dec 2024 11:00:57 +0100
Subject: [PATCH 17/17] fix user groups form
---
core/views/forms.py | 7 ++++---
core/views/user.py | 4 ++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/core/views/forms.py b/core/views/forms.py
index 88963789..8a998ab0 100644
--- a/core/views/forms.py
+++ b/core/views/forms.py
@@ -285,13 +285,14 @@ class UserProfileForm(forms.ModelForm):
self._post_clean()
-class UserRealGroupForm(forms.ModelForm):
+class UserGroupsForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"
- groups = forms.ModelChoiceField(
- RealGroup.objects.all(),
+ groups = forms.ModelMultipleChoiceField(
+ queryset=RealGroup.objects.all(),
widget=CheckboxSelectMultiple,
+ label=_("Groups"),
)
class Meta:
diff --git a/core/views/user.py b/core/views/user.py
index 7b6c146b..264a8dd6 100644
--- a/core/views/user.py
+++ b/core/views/user.py
@@ -67,8 +67,8 @@ from core.views.forms import (
LoginForm,
RegisteringForm,
UserGodfathersForm,
+ UserGroupsForm,
UserProfileForm,
- UserRealGroupForm,
)
from counter.models import Refilling, Selling
from counter.views.student_card import StudentCardFormView
@@ -583,7 +583,7 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
model = User
pk_url_kwarg = "user_id"
template_name = "core/user_group.jinja"
- form_class = UserRealGroupForm
+ form_class = UserGroupsForm
context_object_name = "profile"
current_tab = "groups"