From b81cf49d0aca043c576405983de9d865556b04bc Mon Sep 17 00:00:00 2001 From: Sli Date: Thu, 14 Nov 2024 16:17:10 +0100 Subject: [PATCH 1/6] Remove student card creation from CounterClick view and use fragment instead Intercept htmx on submit requests, this allows auto submit from nfc fields Fix super call with parameters Add loading wheel on student card form for counter_click.jinja --- core/static/bundled/utils/web-components.ts | 11 +- counter/forms.py | 12 +- .../counter/add_student_card_fragment.jinja | 25 ++++ counter/templates/counter/counter_click.jinja | 27 ++-- counter/tests/test_customer.py | 125 ++++++++++++------ counter/urls.py | 11 +- counter/views/click.py | 27 +--- counter/views/student_card.py | 55 +++++++- 8 files changed, 196 insertions(+), 97 deletions(-) create mode 100644 counter/templates/counter/add_student_card_fragment.jinja diff --git a/core/static/bundled/utils/web-components.ts b/core/static/bundled/utils/web-components.ts index 8bec98f9..c2b089c5 100644 --- a/core/static/bundled/utils/web-components.ts +++ b/core/static/bundled/utils/web-components.ts @@ -6,7 +6,16 @@ **/ export function registerComponent(name: string, options?: ElementDefinitionOptions) { return (component: CustomElementConstructor) => { - window.customElements.define(name, component, options); + try { + window.customElements.define(name, component, options); + } catch (e) { + if (e instanceof DOMException) { + // biome-ignore lint/suspicious/noConsole: it's handy to troobleshot + console.warn(e.message); + return; + } + throw e; + } }; } diff --git a/counter/forms.py b/counter/forms.py index 84a92512..91e7c3dc 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -45,9 +45,7 @@ class BillingInfoForm(forms.ModelForm): class StudentCardForm(forms.ModelForm): - """Form for adding student cards - Only used for user profile since CounterClick is to complicated. - """ + """Form for adding student cards""" class Meta: model = StudentCard @@ -114,14 +112,6 @@ class GetUserForm(forms.Form): return cleaned_data -class NFCCardForm(forms.Form): - student_card_uid = forms.CharField( - max_length=StudentCard.UID_SIZE, - required=False, - widget=NFCTextInput, - ) - - class RefillForm(forms.ModelForm): error_css_class = "error" required_css_class = "required" diff --git a/counter/templates/counter/add_student_card_fragment.jinja b/counter/templates/counter/add_student_card_fragment.jinja new file mode 100644 index 00000000..aa2150e1 --- /dev/null +++ b/counter/templates/counter/add_student_card_fragment.jinja @@ -0,0 +1,25 @@ +
+

{% trans %}Add a student card{% endtrans %}

+
+ {% csrf_token %} + {{ form.as_p() }} + + +
+
{% trans %}Registered cards{% endtrans %}
+ {% if student_cards %} + + + {% else %} + {% trans %}No card registered{% endtrans %} + {% endif %} +
diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index cb6bb9cf..323a5905 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -29,26 +29,15 @@ {{ user_mini_profile(customer.user) }} {{ user_subscription(customer.user) }}

{% trans %}Amount: {% endtrans %}{{ customer.amount }} €

-
- {% csrf_token %} - - {% trans %}Add a student card{% endtrans %} - {{ student_card_input.student_card_uid }} - {% if request.session['not_valid_student_card_uid'] %} -

{% trans %}This is not a valid student card UID{% endtrans %}

- {% endif %} - -
-
{% trans %}Registered cards{% endtrans %}
- {% if student_cards %} - - {% else %} - {% trans %}No card registered{% endtrans %} + {% if counter.type == 'BAR' %} +
+
+
{% endif %} diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py index 2d7e1c60..dd7a5ed6 100644 --- a/counter/tests/test_customer.py +++ b/counter/tests/test_customer.py @@ -168,6 +168,7 @@ class TestStudentCard(TestCase): cls.root = User.objects.get(username="root") cls.counter = Counter.objects.get(id=2) + cls.ae_counter = Counter.objects.get(name="AE") def setUp(self): # Auto login on counter @@ -191,94 +192,144 @@ class TestStudentCard(TestCase): # Test card with mixed letters and numbers response = self.client.post( reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + "counter:add_student_card_fragment", + kwargs={ + "counter_id": self.counter.id, + "customer_id": self.sli.customer.pk, + }, ), - {"student_card_uid": "8B90734A802A8F", "action": "add_student_card"}, + {"uid": "8B90734A802A8F"}, ) - self.assertContains(response, text="8B90734A802A8F") + assert response.status_code == 302 + self.assertContains(self.client.get(response.url), text="8B90734A802A8F") # Test card with only numbers response = self.client.post( reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + "counter:add_student_card_fragment", + kwargs={ + "counter_id": self.counter.id, + "customer_id": self.sli.customer.pk, + }, ), - {"student_card_uid": "04786547890123", "action": "add_student_card"}, + {"uid": "04786547890123"}, ) - self.assertContains(response, text="04786547890123") + assert response.status_code == 302 + self.assertContains(self.client.get(response.url), text="04786547890123") # Test card with only letters response = self.client.post( reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + "counter:add_student_card_fragment", + kwargs={ + "counter_id": self.counter.id, + "customer_id": self.sli.customer.pk, + }, ), - {"student_card_uid": "ABCAAAFAAFAAAB", "action": "add_student_card"}, + {"uid": "ABCAAAFAAFAAAB"}, ) - self.assertContains(response, text="ABCAAAFAAFAAAB") + assert response.status_code == 302 + self.assertContains(self.client.get(response.url), text="ABCAAAFAAFAAAB") def test_add_student_card_from_counter_fail(self): # UID too short response = self.client.post( reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + "counter:add_student_card_fragment", + kwargs={ + "counter_id": self.counter.id, + "customer_id": self.sli.customer.pk, + }, ), - {"student_card_uid": "8B90734A802A8", "action": "add_student_card"}, - ) - self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" + {"uid": "8B90734A802A8"}, ) + self.assertContains(response, text="Cet UID est invalide") # UID too long response = self.client.post( reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + "counter:add_student_card_fragment", + kwargs={ + "counter_id": self.counter.id, + "customer_id": self.sli.customer.pk, + }, ), - {"student_card_uid": "8B90734A802A8FA", "action": "add_student_card"}, + {"uid": "8B90734A802A8FA"}, ) + self.assertContains(response, text="Cet UID est invalide") self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" + response, + text="Assurez-vous que cette valeur comporte au plus 14 caractères (actuellement 15).", ) # Test with already existing card response = self.client.post( reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + "counter:add_student_card_fragment", + kwargs={ + "counter_id": self.counter.id, + "customer_id": self.sli.customer.pk, + }, ), - {"student_card_uid": "9A89B82018B0A0", "action": "add_student_card"}, + {"uid": "9A89B82018B0A0"}, ) + self.assertContains(response, text="Cet UID est invalide") self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" + response, text="Un objet Student card avec ce champ Uid existe déjà." ) # Test with lowercase response = self.client.post( reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + "counter:add_student_card_fragment", + kwargs={ + "counter_id": self.counter.id, + "customer_id": self.sli.customer.pk, + }, ), - {"student_card_uid": "8b90734a802a9f", "action": "add_student_card"}, - ) - self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" + {"uid": "8b90734a802a9f"}, ) + self.assertContains(response, text="Cet UID est invalide") # Test with white spaces response = self.client.post( reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + "counter:add_student_card_fragment", + kwargs={ + "counter_id": self.counter.id, + "customer_id": self.sli.customer.pk, + }, ), - {"student_card_uid": " ", "action": "add_student_card"}, + {"uid": " "}, ) - self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" + self.assertContains(response, text="Cet UID est invalide") + self.assertContains(response, text="Ce champ est obligatoire.") + + def test_add_student_card_from_counter_unauthorized(self): + # Send to a counter where you aren't logged in + self.client.post( + reverse("counter:logout", args=[self.counter.id]), + {"user_id": self.krophil.id}, ) + def send_valid_request(client, counter_id): + return client.post( + reverse( + "counter:add_student_card_fragment", + kwargs={ + "counter_id": counter_id, + "customer_id": self.sli.customer.pk, + }, + ), + {"uid": "8B90734A802A8F"}, + ) + + assert send_valid_request(self.client, self.counter.id).status_code == 403 + + # Send to a non bar counter + self.client.force_login(self.skia) + assert send_valid_request(self.client, self.ae_counter.id) + def test_delete_student_card_with_owner(self): self.client.force_login(self.sli) self.client.post( diff --git a/counter/urls.py b/counter/urls.py index d5247478..ab93b586 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -52,7 +52,11 @@ from counter.views.home import ( CounterMain, ) from counter.views.invoice import InvoiceCallView -from counter.views.student_card import StudentCardDeleteView, StudentCardFormView +from counter.views.student_card import ( + StudentCardDeleteView, + StudentCardFormFragmentView, + StudentCardFormView, +) urlpatterns = [ path("/", CounterMain.as_view(), name="details"), @@ -77,6 +81,11 @@ urlpatterns = [ StudentCardFormView.as_view(), name="add_student_card", ), + path( + "customer//card/add/counter//", + StudentCardFormFragmentView.as_view(), + name="add_student_card_fragment", + ), path( "customer//card/delete//", StudentCardDeleteView.as_view(), diff --git a/counter/views/click.py b/counter/views/click.py index 88875fe4..1bdc4b3c 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -27,8 +27,8 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView from core.views import CanViewMixin -from counter.forms import NFCCardForm, RefillForm -from counter.models import Counter, Customer, Product, Selling, StudentCard +from counter.forms import RefillForm +from counter.models import Counter, Customer, Product, Selling from counter.views.mixins import CounterTabsMixin if TYPE_CHECKING: @@ -134,7 +134,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): request.session["too_young"] = False request.session["not_allowed"] = False request.session["no_age"] = False - request.session["not_valid_student_card_uid"] = False if self.object.type != "BAR": self.operator = request.user elif self.customer_is_barman(): @@ -146,8 +145,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): action = parse_qs(request.body.decode()).get("action", [""])[0] if action == "add_product": self.add_product(request) - elif action == "add_student_card": - self.add_student_card(request) elif action == "del_product": self.del_product(request) elif action == "refill": @@ -284,23 +281,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): request.session.modified = True return True - def add_student_card(self, request): - """Add a new student card on the customer account.""" - uid = str(request.POST["student_card_uid"]) - if not StudentCard.is_valid(uid): - request.session["not_valid_student_card_uid"] = True - return False - - if not ( - self.object.type == "BAR" - and "counter_token" in request.session - and request.session["counter_token"] == self.object.token - and self.object.is_open - ): - raise PermissionDenied - StudentCard(customer=self.customer, uid=uid).save() - return True - def del_product(self, request): """Delete a product from the basket.""" pid = parse_qs(request.body.decode())["product_id"][0] @@ -431,10 +411,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): product ) kwargs["customer"] = self.customer - kwargs["student_cards"] = self.customer.student_cards.all() - kwargs["student_card_input"] = NFCCardForm() kwargs["basket_total"] = self.sum_basket(self.request) kwargs["refill_form"] = self.refill_form or RefillForm() - kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE kwargs["barmens_can_refill"] = self.object.can_refill() return kwargs diff --git a/counter/views/student_card.py b/counter/views/student_card.py index b39cc519..a79fa8fd 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -18,9 +18,9 @@ from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy from django.views.generic.edit import DeleteView, FormView -from core.views import CanEditMixin +from core.views import AllowFragment, CanEditMixin from counter.forms import StudentCardForm -from counter.models import Customer, StudentCard +from counter.models import Counter, Customer, StudentCard class StudentCardDeleteView(DeleteView, CanEditMixin): @@ -40,7 +40,7 @@ class StudentCardDeleteView(DeleteView, CanEditMixin): ) -class StudentCardFormView(FormView): +class StudentCardFormView(AllowFragment, FormView): """Add a new student card.""" form_class = StudentCardForm @@ -62,3 +62,52 @@ class StudentCardFormView(FormView): return reverse_lazy( "core:user_prefs", kwargs={"user_id": self.customer.user.pk} ) + + +class StudentCardFormFragmentView(FormView): + """ + Add a new student card from a counter + This is a fragment only view which integrates with counter_click.jinja + """ + + form_class = StudentCardForm + template_name = "counter/add_student_card_fragment.jinja" + + def dispatch(self, request, *args, **kwargs): + self.counter = get_object_or_404( + Counter.objects.annotate_is_open(), pk=kwargs["counter_id"] + ) + self.customer = get_object_or_404( + Customer.objects.prefetch_related("student_cards"), pk=kwargs["customer_id"] + ) + if not ( + self.counter.type == "BAR" + and "counter_token" in request.session + and request.session["counter_token"] == self.counter.token + and self.counter.is_open + ): + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form): + data = form.clean() + res = super().form_valid(form) + StudentCard(customer=self.customer, uid=data["uid"]).save() + return res + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["counter"] = self.counter + context["customer"] = self.customer + context["action"] = self.request.path + context["student_cards"] = self.customer.student_cards.all() + return context + + def get_success_url(self, **kwargs): + return reverse_lazy( + "counter:add_student_card_fragment", + kwargs={ + "customer_id": self.customer.pk, + "counter_id": self.counter.id, + }, + ) From d4b9c3afb1b83d3e0c143fadb590197b3150599e Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 7 Dec 2024 17:28:34 +0100 Subject: [PATCH 2/6] Make StudentCardFormView fragment only --- core/templates/core/user_preferences.jinja | 37 +-- core/views/user.py | 4 - counter/templates/counter/counter_click.jinja | 2 +- .../create_student_card.jinja} | 10 +- counter/tests/test_customer.py | 280 ++++++++++++------ counter/urls.py | 6 - counter/utils.py | 12 +- counter/views/student_card.py | 71 ++--- 8 files changed, 234 insertions(+), 188 deletions(-) rename counter/templates/counter/{add_student_card_fragment.jinja => fragments/create_student_card.jinja} (61%) diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index 0cf4bd57..38749fde 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -38,32 +38,17 @@ {% if profile.customer %}

{% trans %}Student cards{% endtrans %}

- {% if profile.customer.student_cards.exists() %} - - {% else %} - {% trans %}No student card registered.{% endtrans %} -

- {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually - add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} -

- {% endif %} - -
- {% csrf_token %} - {{ student_card_form.as_p() }} - -
+

+ {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually + add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} +

+
+
+
{% endif %} {% endblock %} \ No newline at end of file diff --git a/core/views/user.py b/core/views/user.py index e9694a92..5a797620 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -70,7 +70,6 @@ from core.views.forms import ( UserGodfathersForm, UserProfileForm, ) -from counter.forms import StudentCardForm from counter.models import Refilling, Selling from eboutic.models import Invoice from subscription.models import Subscription @@ -576,9 +575,6 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): hasattr(self.object, "trombi_user") and self.request.user.trombi_user.trombi ): kwargs["trombi_form"] = UserTrombiForm() - - if hasattr(self.object, "customer"): - kwargs["student_card_form"] = StudentCardForm() return kwargs diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 323a5905..9c95292e 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -32,7 +32,7 @@ {% if counter.type == 'BAR' %}
diff --git a/counter/templates/counter/add_student_card_fragment.jinja b/counter/templates/counter/fragments/create_student_card.jinja similarity index 61% rename from counter/templates/counter/add_student_card_fragment.jinja rename to counter/templates/counter/fragments/create_student_card.jinja index aa2150e1..7cd05ba9 100644 --- a/counter/templates/counter/add_student_card_fragment.jinja +++ b/counter/templates/counter/fragments/create_student_card.jinja @@ -1,7 +1,6 @@

{% trans %}Add a student card{% endtrans %}

{% for card in student_cards %} -
  • {{ card.uid }}
  • +
  • + {{ card.uid }} + + {% trans %}Delete{% endtrans %} + +
  • {% endfor %} {% else %} - {% trans %}No card registered{% endtrans %} + {% trans %}No student card registered.{% endtrans %} {% endif %}
    diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py index dd7a5ed6..f7e599e6 100644 --- a/counter/tests/test_customer.py +++ b/counter/tests/test_customer.py @@ -1,15 +1,27 @@ import json import string +from datetime import timedelta import pytest +from django.conf import settings +from django.contrib.auth.base_user import make_password from django.test import Client, TestCase from django.urls import reverse +from django.utils.timezone import now from model_bakery import baker -from core.baker_recipes import subscriber_user +from club.models import Membership +from core.baker_recipes import board_user, subscriber_user from core.models import User from counter.baker_recipes import refill_recipe, sale_recipe -from counter.models import BillingInfo, Counter, Customer, Refilling, Selling +from counter.models import ( + BillingInfo, + Counter, + Customer, + Refilling, + Selling, + StudentCard, +) @pytest.mark.django_db @@ -162,43 +174,65 @@ class TestStudentCard(TestCase): @classmethod def setUpTestData(cls): - cls.krophil = User.objects.get(username="krophil") - cls.sli = User.objects.get(username="sli") - cls.skia = User.objects.get(username="skia") - cls.root = User.objects.get(username="root") + cls.customer = subscriber_user.make() + cls.customer.save() + cls.barmen = subscriber_user.make(password=make_password("plop")) + cls.board_admin = board_user.make() + cls.club_admin = baker.make(User) + cls.root = baker.make(User, is_superuser=True) + cls.subscriber = subscriber_user.make() - cls.counter = Counter.objects.get(id=2) - cls.ae_counter = Counter.objects.get(name="AE") + cls.counter = baker.make(Counter, type="BAR") + cls.counter.sellers.add(cls.barmen) + + cls.club_counter = baker.make(Counter) + baker.make( + Membership, + start_date=now() - timedelta(days=30), + club=cls.club_counter.club, + role=settings.SITH_CLUB_ROLES_ID["Board member"], + user=cls.club_admin, + ) + + cls.valid_card = baker.make( + StudentCard, customer=cls.customer.customer, uid="8A89B82018B0A0" + ) def setUp(self): # Auto login on counter self.client.post( reverse("counter:login", args=[self.counter.id]), - {"username": "krophil", "password": "plop"}, + {"username": self.barmen.username, "password": "plop"}, ) def test_search_user_with_student_card(self): response = self.client.post( reverse("counter:details", args=[self.counter.id]), - {"code": "9A89B82018B0A0"}, + {"code": self.valid_card.uid}, ) assert response.url == reverse( "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + kwargs={"counter_id": self.counter.id, "user_id": self.customer.id}, ) def test_add_student_card_from_counter(self): # Test card with mixed letters and numbers response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8B90734A802A8F"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) assert response.status_code == 302 self.assertContains(self.client.get(response.url), text="8B90734A802A8F") @@ -206,13 +240,19 @@ class TestStudentCard(TestCase): # Test card with only numbers response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "04786547890123"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) assert response.status_code == 302 self.assertContains(self.client.get(response.url), text="04786547890123") @@ -220,13 +260,19 @@ class TestStudentCard(TestCase): # Test card with only letters response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "ABCAAAFAAFAAAB"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) assert response.status_code == 302 self.assertContains(self.client.get(response.url), text="ABCAAAFAAFAAAB") @@ -235,26 +281,38 @@ class TestStudentCard(TestCase): # UID too short response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8B90734A802A8"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) self.assertContains(response, text="Cet UID est invalide") # UID too long response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8B90734A802A8FA"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) self.assertContains(response, text="Cet UID est invalide") self.assertContains( @@ -265,13 +323,19 @@ class TestStudentCard(TestCase): # Test with already existing card response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, + }, + ), + {"uid": self.valid_card.uid}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, }, ), - {"uid": "9A89B82018B0A0"}, ) self.assertContains(response, text="Cet UID est invalide") self.assertContains( @@ -281,26 +345,38 @@ class TestStudentCard(TestCase): # Test with lowercase response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8b90734a802a9f"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) self.assertContains(response, text="Cet UID est invalide") # Test with white spaces response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": " "}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) self.assertContains(response, text="Cet UID est invalide") self.assertContains(response, text="Ce champ est obligatoire.") @@ -309,52 +385,58 @@ class TestStudentCard(TestCase): # Send to a counter where you aren't logged in self.client.post( reverse("counter:logout", args=[self.counter.id]), - {"user_id": self.krophil.id}, + {"user_id": self.barmen.id}, ) def send_valid_request(client, counter_id): return client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": counter_id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8B90734A802A8F"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": counter_id, + "user_id": self.customer.customer.pk, + }, + ), ) assert send_valid_request(self.client, self.counter.id).status_code == 403 # Send to a non bar counter - self.client.force_login(self.skia) - assert send_valid_request(self.client, self.ae_counter.id) + self.client.force_login(self.club_admin) + assert send_valid_request(self.client, self.club_counter.id).status_code == 403 def test_delete_student_card_with_owner(self): - self.client.force_login(self.sli) + self.client.force_login(self.customer) self.client.post( reverse( "counter:delete_student_card", kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, + "customer_id": self.customer.customer.pk, + "card_id": self.customer.customer.student_cards.first().id, }, ) ) - assert not self.sli.customer.student_cards.exists() + assert not self.customer.customer.student_cards.exists() def test_delete_student_card_with_board_member(self): - self.client.force_login(self.skia) + self.client.force_login(self.board_admin) self.client.post( reverse( "counter:delete_student_card", kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, + "customer_id": self.customer.customer.pk, + "card_id": self.customer.customer.student_cards.first().id, }, ) ) - assert not self.sli.customer.student_cards.exists() + assert not self.customer.customer.student_cards.exists() def test_delete_student_card_with_root(self): self.client.force_login(self.root) @@ -362,100 +444,107 @@ class TestStudentCard(TestCase): reverse( "counter:delete_student_card", kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, + "customer_id": self.customer.customer.pk, + "card_id": self.customer.customer.student_cards.first().id, }, ) ) - assert not self.sli.customer.student_cards.exists() + assert not self.customer.customer.student_cards.exists() def test_delete_student_card_fail(self): - self.client.force_login(self.krophil) + self.client.force_login(self.subscriber) response = self.client.post( reverse( "counter:delete_student_card", kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, + "customer_id": self.customer.customer.pk, + "card_id": self.customer.customer.student_cards.first().id, }, ) ) assert response.status_code == 403 - assert self.sli.customer.student_cards.exists() + assert self.customer.customer.student_cards.exists() def test_add_student_card_from_user_preferences(self): # Test with owner of the card - self.client.force_login(self.sli) - self.client.post( + self.client.force_login(self.customer) + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8F"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="8B90734A802A8F") # Test with board member - self.client.force_login(self.skia) - self.client.post( + self.client.force_login(self.board_admin) + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8A"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="8B90734A802A8A") # Test card with only numbers - self.client.post( + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "04786547890123"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="04786547890123") # Test card with only letters - self.client.post( + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "ABCAAAFAAFAAAB"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="ABCAAAFAAFAAAB") # Test with root self.client.force_login(self.root) - self.client.post( + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8B"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="8B90734A802A8B") def test_add_student_card_from_user_preferences_fail(self): - self.client.force_login(self.sli) + self.client.force_login(self.customer) # UID too short response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8"}, ) @@ -465,7 +554,8 @@ class TestStudentCard(TestCase): # UID too long response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8FA"}, ) @@ -474,9 +564,10 @@ class TestStudentCard(TestCase): # Test with already existing card response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), - {"uid": "9A89B82018B0A0"}, + {"uid": self.valid_card.uid}, ) self.assertContains( response, text="Un objet Student card avec ce champ Uid existe déjà." @@ -485,7 +576,8 @@ class TestStudentCard(TestCase): # Test with lowercase response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8b90734a802a9f"}, ) @@ -494,17 +586,19 @@ class TestStudentCard(TestCase): # Test with white spaces response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": " " * 14}, ) self.assertContains(response, text="Cet UID est invalide") # Test with unauthorized user - self.client.force_login(self.krophil) + self.client.force_login(self.subscriber) response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8F"}, ) diff --git a/counter/urls.py b/counter/urls.py index ab93b586..e196894f 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -54,7 +54,6 @@ from counter.views.home import ( from counter.views.invoice import InvoiceCallView from counter.views.student_card import ( StudentCardDeleteView, - StudentCardFormFragmentView, StudentCardFormView, ) @@ -81,11 +80,6 @@ urlpatterns = [ StudentCardFormView.as_view(), name="add_student_card", ), - path( - "customer//card/add/counter//", - StudentCardFormFragmentView.as_view(), - name="add_student_card_fragment", - ), path( "customer//card/delete//", StudentCardDeleteView.as_view(), diff --git a/counter/utils.py b/counter/utils.py index 2b9b6fd6..499b2d8e 100644 --- a/counter/utils.py +++ b/counter/utils.py @@ -22,14 +22,22 @@ def is_logged_in_counter(request: HttpRequest) -> bool: to the counter) - The current session has a counter token associated with it. - A counter with this token exists. + - The counter is open """ referer_ok = ( "HTTP_REFERER" in request.META and resolve(urlparse(request.META["HTTP_REFERER"]).path).app_name == "counter" ) - return ( + has_token = ( (referer_ok or request.resolver_match.app_name == "counter") and "counter_token" in request.session and request.session["counter_token"] - and Counter.objects.filter(token=request.session["counter_token"]).exists() + ) + if not has_token: + return False + + return ( + Counter.objects.annotate_is_open() + .filter(token=request.session["counter_token"], is_open=True) + .exists() ) diff --git a/counter/views/student_card.py b/counter/views/student_card.py index a79fa8fd..882069da 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -13,14 +13,17 @@ # # + from django.core.exceptions import PermissionDenied +from django.http import HttpRequest from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy from django.views.generic.edit import DeleteView, FormView -from core.views import AllowFragment, CanEditMixin +from core.views import CanEditMixin from counter.forms import StudentCardForm -from counter.models import Counter, Customer, StudentCard +from counter.models import Customer, StudentCard +from counter.utils import is_logged_in_counter class StudentCardDeleteView(DeleteView, CanEditMixin): @@ -40,16 +43,22 @@ class StudentCardDeleteView(DeleteView, CanEditMixin): ) -class StudentCardFormView(AllowFragment, FormView): - """Add a new student card.""" +class StudentCardFormView(FormView): + """Add a new student card. This is a fragment view !""" form_class = StudentCardForm - template_name = "core/create.jinja" + template_name = "counter/fragments/create_student_card.jinja" - def dispatch(self, request, *args, **kwargs): - self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) - if not StudentCard.can_create(self.customer, request.user): + def dispatch(self, request: HttpRequest, *args, **kwargs): + self.customer = get_object_or_404( + Customer.objects.prefetch_related("student_cards"), pk=kwargs["customer_id"] + ) + + if not is_logged_in_counter(request) and not StudentCard.can_create( + self.customer, request.user + ): raise PermissionDenied + return super().dispatch(request, *args, **kwargs) def form_valid(self, form): @@ -58,56 +67,12 @@ class StudentCardFormView(AllowFragment, FormView): StudentCard(customer=self.customer, uid=data["uid"]).save() return res - def get_success_url(self, **kwargs): - return reverse_lazy( - "core:user_prefs", kwargs={"user_id": self.customer.user.pk} - ) - - -class StudentCardFormFragmentView(FormView): - """ - Add a new student card from a counter - This is a fragment only view which integrates with counter_click.jinja - """ - - form_class = StudentCardForm - template_name = "counter/add_student_card_fragment.jinja" - - def dispatch(self, request, *args, **kwargs): - self.counter = get_object_or_404( - Counter.objects.annotate_is_open(), pk=kwargs["counter_id"] - ) - self.customer = get_object_or_404( - Customer.objects.prefetch_related("student_cards"), pk=kwargs["customer_id"] - ) - if not ( - self.counter.type == "BAR" - and "counter_token" in request.session - and request.session["counter_token"] == self.counter.token - and self.counter.is_open - ): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - def form_valid(self, form): - data = form.clean() - res = super().form_valid(form) - StudentCard(customer=self.customer, uid=data["uid"]).save() - return res - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["counter"] = self.counter context["customer"] = self.customer context["action"] = self.request.path context["student_cards"] = self.customer.student_cards.all() return context def get_success_url(self, **kwargs): - return reverse_lazy( - "counter:add_student_card_fragment", - kwargs={ - "customer_id": self.customer.pk, - "counter_id": self.counter.id, - }, - ) + return self.request.path From 2f613607afd534b93996fcf866b31fdfd3e4d672 Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 7 Dec 2024 22:48:18 +0100 Subject: [PATCH 3/6] Update number of queries in test_num_queries --- sas/tests/test_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sas/tests/test_api.py b/sas/tests/test_api.py index ebc33638..733838d2 100644 --- a/sas/tests/test_api.py +++ b/sas/tests/test_api.py @@ -1,10 +1,10 @@ from django.conf import settings +from django.core.cache import cache from django.db import transaction from django.test import TestCase from django.urls import reverse from model_bakery import baker from model_bakery.recipe import Recipe -from pytest_django.asserts import assertNumQueries from core.baker_recipes import old_subscriber_user, subscriber_user from core.models import RealGroup, SithFile, User @@ -128,9 +128,11 @@ class TestPictureSearch(TestSas): def test_num_queries(self): """Test that the number of queries is stable.""" self.client.force_login(subscriber_user.make()) - with assertNumQueries(5): + cache.clear() + with self.assertNumQueries(7): + # 2 requests to create the session # 1 request to fetch the user from the db - # 2 requests to check the user permissions + # 2 requests to check the user permissions, depends on the db engine # 1 request to fetch the pictures # 1 request to count the total number of items in the pagination self.client.get(self.url) From 66d2dc74e79618db5b8c5199723a0190229c8583 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 8 Dec 2024 00:32:28 +0100 Subject: [PATCH 4/6] Pre-fetch forms for student card --- core/templates/core/user_preferences.jinja | 30 +- core/views/user.py | 5 + counter/templates/counter/counter_click.jinja | 231 ++++----- counter/views/click.py | 4 + counter/views/student_card.py | 35 +- locale/fr/LC_MESSAGES/django.po | 455 +++++++++--------- 6 files changed, 391 insertions(+), 369 deletions(-) diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index 38749fde..d70371a2 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -35,20 +35,20 @@ {% endif %} - {% if profile.customer %} -

    {% trans %}Student cards{% endtrans %}

    + {% if student_card %} + {% with + form=student_card.form, + action=student_card.context.action, + customer=student_card.context.customer, + student_cards=student_card.context.student_cards + %} + {% include student_card.template %} + {% endwith %} -

    - {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually - add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} -

    -
    -
    -
    - {% endif %} -
    +

    + {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually + add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} +

    +{% endif %} + {% endblock %} \ No newline at end of file diff --git a/core/views/user.py b/core/views/user.py index 5a797620..83916fb1 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -71,6 +71,7 @@ from core.views.forms import ( UserProfileForm, ) from counter.models import Refilling, Selling +from counter.views.student_card import StudentCardFormView from eboutic.models import Invoice from subscription.models import Subscription from trombi.views import UserTrombiForm @@ -575,6 +576,10 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): hasattr(self.object, "trombi_user") and self.request.user.trombi_user.trombi ): kwargs["trombi_form"] = UserTrombiForm() + if hasattr(self.object, "customer"): + kwargs["student_card"] = StudentCardFormView.get_template_data( + self.request, self.object.customer + ).as_dict() return kwargs diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 9c95292e..ed89b100 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -31,130 +31,131 @@

    {% trans %}Amount: {% endtrans %}{{ customer.amount }} €

    {% if counter.type == 'BAR' %} -
    -
    -
    - {% endif %} - + {% with + form=student_card.form, + action=student_card.context.action, + customer=student_card.context.customer, + student_cards=student_card.context.student_cards + %} + {% include student_card.template %} + {% endwith %} +{% endif %} + -
    -
    {% trans %}Selling{% endtrans %}
    -
    - {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} +
    +
    {% trans %}Selling{% endtrans %}
    +
    + {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} {# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #} - - {% csrf_token %} - - - - - +
    + {% csrf_token %} + + + + +
    - -

    {% trans %}Basket: {% endtrans %}

    - -
      - -
    -

    - Total: - - -

    - -
    - {% csrf_token %} - - -
    -
    - {% csrf_token %} - - -
    + +

    {% trans %}Basket: {% endtrans %}

    -
    -
      - {% for category in categories.keys() -%} -
    • {{ category }}
    • - {%- endfor %} -
    - {% for category in categories.keys() -%} -
    -
    {{ category }}
    - {% for p in categories[category] -%} -
    - {% csrf_token %} - - - -
    - {%- endfor %} +
      + +
    +

    + Total: + + +

    + +
    + {% csrf_token %} + + +
    +
    + {% csrf_token %} + + +
    +
    + {% if (counter.type == 'BAR' and barmens_can_refill) %} +
    {% trans %}Refilling{% endtrans %}
    +
    +
    + {% csrf_token %} + {{ refill_form.as_p() }} + + +
    +
    + {% endif %} +
    + +
    +
      + {% for category in categories.keys() -%} +
    • {{ category }}
    • + {%- endfor %} +
    + {% for category in categories.keys() -%} +
    +
    {{ category }}
    + {% for p in categories[category] -%} +
    + {% csrf_token %} + + + +
    {%- endfor %}
    -
    + {%- endfor %} +
    +
    {% endblock content %} {% block script %} diff --git a/counter/views/click.py b/counter/views/click.py index 1bdc4b3c..c0845aaf 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -30,6 +30,7 @@ from core.views import CanViewMixin from counter.forms import RefillForm from counter.models import Counter, Customer, Product, Selling from counter.views.mixins import CounterTabsMixin +from counter.views.student_card import StudentCardFormView if TYPE_CHECKING: from core.models import User @@ -414,4 +415,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): kwargs["basket_total"] = self.sum_basket(self.request) kwargs["refill_form"] = self.refill_form or RefillForm() kwargs["barmens_can_refill"] = self.object.can_refill() + kwargs["student_card"] = StudentCardFormView.get_template_data( + self.request, self.customer + ).as_dict() return kwargs diff --git a/counter/views/student_card.py b/counter/views/student_card.py index 882069da..fb919ce2 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -14,6 +14,9 @@ # +from dataclasses import asdict, dataclass +from typing import Any + from django.core.exceptions import PermissionDenied from django.http import HttpRequest from django.shortcuts import get_object_or_404 @@ -26,6 +29,16 @@ from counter.models import Customer, StudentCard from counter.utils import is_logged_in_counter +@dataclass +class StudentCardTemplateData: + form: StudentCardForm + template: str + context: dict[str, Any] + + def as_dict(self) -> dict[str, Any]: + return asdict(self) + + class StudentCardDeleteView(DeleteView, CanEditMixin): """View used to delete a card from a user.""" @@ -49,6 +62,23 @@ class StudentCardFormView(FormView): form_class = StudentCardForm template_name = "counter/fragments/create_student_card.jinja" + @classmethod + def get_template_data( + cls, request: HttpRequest, customer: Customer + ) -> StudentCardTemplateData: + """Get necessary data to pre-render the fragment""" + return StudentCardTemplateData( + form=cls.form_class(), + template=cls.template_name, + context={ + "action": reverse_lazy( + "counter:add_student_card", kwargs={"customer_id": customer.pk} + ), + "customer": customer, + "student_cards": customer.student_cards.all(), + }, + ) + def dispatch(self, request: HttpRequest, *args, **kwargs): self.customer = get_object_or_404( Customer.objects.prefetch_related("student_cards"), pk=kwargs["customer_id"] @@ -69,9 +99,8 @@ class StudentCardFormView(FormView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["customer"] = self.customer - context["action"] = self.request.path - context["student_cards"] = self.customer.student_cards.all() + data = self.get_template_data(self.request, self.customer) + context.update(data.context) return context def get_success_url(self, **kwargs): diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 0a7ce5fe..9fd39f03 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-11-29 18:04+0100\n" +"POT-Creation-Date: 2024-12-08 00:29+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -218,7 +218,7 @@ msgstr "Compte" msgid "Company" msgstr "Entreprise" -#: accounting/models.py:307 core/models.py:338 sith/settings.py:421 +#: accounting/models.py:307 core/models.py:338 sith/settings.py:423 msgid "Other" msgstr "Autre" @@ -369,7 +369,7 @@ msgstr "Compte en banque : " #: core/templates/core/user_clubs.jinja:34 #: core/templates/core/user_clubs.jinja:63 #: core/templates/core/user_edit.jinja:62 -#: core/templates/core/user_preferences.jinja:48 +#: counter/templates/counter/fragments/create_student_card.jinja:21 #: counter/templates/counter/last_ops.jinja:35 #: counter/templates/counter/last_ops.jinja:65 #: election/templates/election/election_detail.jinja:191 @@ -517,7 +517,7 @@ msgid "Effective amount" msgstr "Montant effectif" #: accounting/templates/accounting/club_account_details.jinja:36 -#: sith/settings.py:467 +#: sith/settings.py:469 msgid "Closed" msgstr "Fermé" @@ -650,8 +650,8 @@ msgid "Done" msgstr "Effectuées" #: accounting/templates/accounting/journal_details.jinja:41 -#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:955 -#: pedagogy/templates/pedagogy/moderation.jinja:13 +#: counter/templates/counter/cash_summary_list.jinja:37 +#: counter/views/cash.py:87 pedagogy/templates/pedagogy/moderation.jinja:13 #: pedagogy/templates/pedagogy/uv_detail.jinja:142 #: trombi/templates/trombi/comment.jinja:4 #: trombi/templates/trombi/comment.jinja:8 @@ -771,7 +771,6 @@ msgstr "Opération liée : " #: core/templates/core/user_godfathers_tree.jinja:85 #: core/templates/core/user_preferences.jinja:18 #: core/templates/core/user_preferences.jinja:27 -#: core/templates/core/user_preferences.jinja:65 #: counter/templates/counter/cash_register_summary.jinja:28 #: forum/templates/forum/reply.jinja:39 #: subscription/templates/subscription/fragments/creation_form.jinja:9 @@ -951,11 +950,11 @@ msgstr "Une action est requise" msgid "You must specify at least an user or an email address" msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" -#: club/forms.py:149 counter/forms.py:203 +#: club/forms.py:149 counter/forms.py:193 msgid "Begin date" msgstr "Date de début" -#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:206 +#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:196 #: election/views.py:170 subscription/forms.py:21 msgid "End date" msgstr "Date de fin" @@ -963,15 +962,16 @@ msgstr "Date de fin" #: club/forms.py:156 club/templates/club/club_sellings.jinja:49 #: core/templates/core/user_account_detail.jinja:17 #: core/templates/core/user_account_detail.jinja:56 -#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:137 +#: counter/templates/counter/cash_summary_list.jinja:33 +#: counter/views/mixins.py:58 msgid "Counter" msgstr "Comptoir" -#: club/forms.py:163 counter/views.py:683 +#: club/forms.py:163 counter/views/mixins.py:94 msgid "Products" msgstr "Produits" -#: club/forms.py:168 counter/views.py:688 +#: club/forms.py:168 counter/views/mixins.py:99 msgid "Archived products" msgstr "Produits archivés" @@ -1334,7 +1334,7 @@ msgid "No mailing list existing for this club" msgstr "Aucune mailing liste n'existe pour ce club" #: club/templates/club/mailing.jinja:72 -#: subscription/templates/subscription/subscription.jinja:39 +#: subscription/templates/subscription/subscription.jinja:38 msgid "New member" msgstr "Nouveau membre" @@ -1426,7 +1426,8 @@ msgstr "Hebdomadaire" msgid "Call" msgstr "Appel" -#: com/models.py:67 com/models.py:174 com/models.py:248 election/models.py:12 +#: com/models.py:67 com/models.py:174 com/models.py:248 +#: 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" @@ -1835,6 +1836,7 @@ msgid "Articles in no weekmail yet" msgstr "Articles dans aucun weekmail" #: com/templates/com/weekmail.jinja:20 com/templates/com/weekmail.jinja:49 +#: core/templates/core/macros.jinja:301 msgid "Content" msgstr "Contenu" @@ -2505,7 +2507,7 @@ msgstr "Photos" #: eboutic/templates/eboutic/eboutic_main.jinja:22 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:16 #: eboutic/templates/eboutic/eboutic_payment_result.jinja:4 -#: sith/settings.py:420 sith/settings.py:428 +#: sith/settings.py:422 sith/settings.py:430 msgid "Eboutic" msgstr "Eboutic" @@ -2583,7 +2585,7 @@ msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/file_delete_confirm.jinja:46 -#: counter/templates/counter/counter_click.jinja:121 +#: counter/templates/counter/counter_click.jinja:111 #: sas/templates/sas/ask_picture_removal.jinja:20 msgid "Cancel" msgstr "Annuler" @@ -3042,11 +3044,11 @@ msgid "Eboutic invoices" msgstr "Facture eboutic" #: core/templates/core/user_account.jinja:54 -#: core/templates/core/user_tools.jinja:58 counter/views.py:708 +#: core/templates/core/user_tools.jinja:58 counter/views/mixins.py:119 msgid "Etickets" msgstr "Etickets" -#: core/templates/core/user_account.jinja:69 core/views/user.py:638 +#: core/templates/core/user_account.jinja:69 core/views/user.py:639 msgid "User has no account" msgstr "L'utilisateur n'a pas de compte" @@ -3137,7 +3139,7 @@ msgstr "Non cotisant" #: core/templates/core/user_detail.jinja:162 #: subscription/templates/subscription/subscription.jinja:6 -#: subscription/templates/subscription/subscription.jinja:37 +#: subscription/templates/subscription/subscription.jinja:36 msgid "New subscription" msgstr "Nouvelle cotisation" @@ -3295,19 +3297,11 @@ msgstr "Vous avez déjà choisi ce Trombi: %(trombi)s." msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" -#: core/templates/core/user_preferences.jinja:39 -msgid "Student cards" -msgstr "Cartes étudiante" - -#: core/templates/core/user_preferences.jinja:54 -msgid "No student card registered." -msgstr "Aucune carte étudiante enregistrée." - -#: core/templates/core/user_preferences.jinja:56 +#: core/templates/core/user_preferences.jinja:49 msgid "" "You can add a card by asking at a counter or add it yourself here. If you " "want to manually\n" -" add a student card yourself, you'll need a NFC reader. We store " +" add a student card yourself, you'll need a NFC reader. We store " "the UID of the card which is 14 characters long." msgstr "" "Vous pouvez ajouter une carte en demandant à un comptoir ou en l'ajoutant " @@ -3377,8 +3371,8 @@ msgstr "Cotisations" msgid "Subscription stats" msgstr "Statistiques de cotisation" -#: core/templates/core/user_tools.jinja:48 counter/forms.py:176 -#: counter/views.py:678 +#: core/templates/core/user_tools.jinja:48 counter/forms.py:166 +#: counter/views/mixins.py:89 msgid "Counters" msgstr "Comptoirs" @@ -3395,12 +3389,13 @@ msgid "Product types management" msgstr "Gestion des types de produit" #: core/templates/core/user_tools.jinja:56 -#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:698 +#: counter/templates/counter/cash_summary_list.jinja:23 +#: counter/views/mixins.py:109 msgid "Cash register summaries" msgstr "Relevés de caisse" #: core/templates/core/user_tools.jinja:57 -#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:703 +#: counter/templates/counter/invoices_call.jinja:4 counter/views/mixins.py:114 msgid "Invoices call" msgstr "Appels à facture" @@ -3548,7 +3543,7 @@ msgstr "Parrain / Marraine" msgid "Godchild" msgstr "Fillot / Fillote" -#: core/views/forms.py:310 counter/forms.py:82 trombi/views.py:151 +#: core/views/forms.py:310 counter/forms.py:80 trombi/views.py:151 msgid "Select user" msgstr "Choisir un utilisateur" @@ -3601,15 +3596,15 @@ msgstr "Galaxie" msgid "counter" msgstr "comptoir" -#: counter/forms.py:63 +#: counter/forms.py:61 msgid "This UID is invalid" msgstr "Cet UID est invalide" -#: counter/forms.py:111 +#: counter/forms.py:109 msgid "User not found" msgstr "Utilisateur non trouvé" -#: counter/management/commands/dump_accounts.py:141 +#: counter/management/commands/dump_accounts.py:148 msgid "Your AE account has been emptied" msgstr "Votre compte AE a été vidé" @@ -3637,7 +3632,7 @@ msgstr "client" msgid "customers" msgstr "clients" -#: counter/models.py:110 counter/views.py:261 +#: counter/models.py:110 counter/views/click.py:66 msgid "Not enough money" msgstr "Solde insuffisant" @@ -3777,8 +3772,8 @@ msgstr "quantité" msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:797 sith/settings.py:413 sith/settings.py:418 -#: sith/settings.py:438 +#: counter/models.py:797 sith/settings.py:415 sith/settings.py:420 +#: sith/settings.py:440 msgid "Credit card" msgstr "Carte bancaire" @@ -3910,7 +3905,8 @@ msgstr "Liste des relevés de caisse" msgid "Theoric sums" msgstr "Sommes théoriques" -#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:956 +#: counter/templates/counter/cash_summary_list.jinja:36 +#: counter/views/cash.py:88 msgid "Emptied" msgstr "Coffre vidé" @@ -3922,17 +3918,14 @@ msgstr "oui" msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." -#: counter/templates/counter/counter_click.jinja:35 -msgid "Add a student card" -msgstr "Ajouter une carte étudiante" +#: counter/templates/counter/counter_click.jinja:46 +#: launderette/templates/launderette/launderette_admin.jinja:8 +msgid "Selling" +msgstr "Vente" -#: counter/templates/counter/counter_click.jinja:38 -msgid "This is not a valid student card UID" -msgstr "Ce n'est pas un UID de carte étudiante valide" - -#: counter/templates/counter/counter_click.jinja:40 -#: counter/templates/counter/counter_click.jinja:67 -#: counter/templates/counter/counter_click.jinja:132 +#: counter/templates/counter/counter_click.jinja:57 +#: counter/templates/counter/counter_click.jinja:122 +#: counter/templates/counter/fragments/create_student_card.jinja:10 #: counter/templates/counter/invoices_call.jinja:16 #: launderette/templates/launderette/launderette_admin.jinja:35 #: launderette/templates/launderette/launderette_click.jinja:13 @@ -3941,29 +3934,16 @@ msgstr "Ce n'est pas un UID de carte étudiante valide" msgid "Go" msgstr "Valider" -#: counter/templates/counter/counter_click.jinja:42 -msgid "Registered cards" -msgstr "Cartes enregistrées" - -#: counter/templates/counter/counter_click.jinja:51 -msgid "No card registered" -msgstr "Aucune carte enregistrée" - -#: counter/templates/counter/counter_click.jinja:56 -#: launderette/templates/launderette/launderette_admin.jinja:8 -msgid "Selling" -msgstr "Vente" - -#: counter/templates/counter/counter_click.jinja:74 +#: counter/templates/counter/counter_click.jinja:64 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:19 msgid "Basket: " msgstr "Panier : " -#: counter/templates/counter/counter_click.jinja:115 +#: counter/templates/counter/counter_click.jinja:105 msgid "Finish" msgstr "Terminer" -#: counter/templates/counter/counter_click.jinja:125 +#: counter/templates/counter/counter_click.jinja:115 #: counter/templates/counter/refilling_list.jinja:9 msgid "Refilling" msgstr "Rechargement" @@ -4047,6 +4027,18 @@ msgstr "Nouveau eticket" msgid "There is no eticket in this website." msgstr "Il n'y a pas de eticket sur ce site web." +#: counter/templates/counter/fragments/create_student_card.jinja:2 +msgid "Add a student card" +msgstr "Ajouter une carte étudiante" + +#: counter/templates/counter/fragments/create_student_card.jinja:13 +msgid "Registered cards" +msgstr "Cartes enregistrées" + +#: counter/templates/counter/fragments/create_student_card.jinja:27 +msgid "No student card registered." +msgstr "Aucune carte étudiante enregistrée." + #: counter/templates/counter/invoices_call.jinja:8 #, python-format msgid "Invoices call for %(date)s" @@ -4219,104 +4211,104 @@ msgstr "Temps" msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" -#: counter/views.py:147 -msgid "Cash summary" -msgstr "Relevé de caisse" - -#: counter/views.py:156 -msgid "Last operations" -msgstr "Dernières opérations" - -#: counter/views.py:203 -msgid "Bad credentials" -msgstr "Mauvais identifiants" - -#: counter/views.py:205 -msgid "User is not barman" -msgstr "L'utilisateur n'est pas barman." - -#: counter/views.py:210 -msgid "Bad location, someone is already logged in somewhere else" -msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" - -#: counter/views.py:252 -msgid "Too young for that product" -msgstr "Trop jeune pour ce produit" - -#: counter/views.py:255 -msgid "Not allowed for that product" -msgstr "Non autorisé pour ce produit" - -#: counter/views.py:258 -msgid "No date of birth provided" -msgstr "Pas de date de naissance renseignée" - -#: counter/views.py:546 -msgid "You have not enough money to buy all the basket" -msgstr "Vous n'avez pas assez d'argent pour acheter le panier" - -#: counter/views.py:673 -msgid "Counter administration" -msgstr "Administration des comptoirs" - -#: counter/views.py:693 -msgid "Product types" -msgstr "Types de produit" - -#: counter/views.py:913 +#: counter/views/cash.py:45 msgid "10 cents" msgstr "10 centimes" -#: counter/views.py:914 +#: counter/views/cash.py:46 msgid "20 cents" msgstr "20 centimes" -#: counter/views.py:915 +#: counter/views/cash.py:47 msgid "50 cents" msgstr "50 centimes" -#: counter/views.py:916 +#: counter/views/cash.py:48 msgid "1 euro" msgstr "1 €" -#: counter/views.py:917 +#: counter/views/cash.py:49 msgid "2 euros" msgstr "2 €" -#: counter/views.py:918 +#: counter/views/cash.py:50 msgid "5 euros" msgstr "5 €" -#: counter/views.py:919 +#: counter/views/cash.py:51 msgid "10 euros" msgstr "10 €" -#: counter/views.py:920 +#: counter/views/cash.py:52 msgid "20 euros" msgstr "20 €" -#: counter/views.py:921 +#: counter/views/cash.py:53 msgid "50 euros" msgstr "50 €" -#: counter/views.py:923 +#: counter/views/cash.py:55 msgid "100 euros" msgstr "100 €" -#: counter/views.py:926 counter/views.py:932 counter/views.py:938 -#: counter/views.py:944 counter/views.py:950 +#: counter/views/cash.py:58 counter/views/cash.py:64 counter/views/cash.py:70 +#: counter/views/cash.py:76 counter/views/cash.py:82 msgid "Check amount" msgstr "Montant du chèque" -#: counter/views.py:929 counter/views.py:935 counter/views.py:941 -#: counter/views.py:947 counter/views.py:953 +#: counter/views/cash.py:61 counter/views/cash.py:67 counter/views/cash.py:73 +#: counter/views/cash.py:79 counter/views/cash.py:85 msgid "Check quantity" msgstr "Nombre de chèque" -#: counter/views.py:1473 +#: counter/views/click.py:57 +msgid "Too young for that product" +msgstr "Trop jeune pour ce produit" + +#: counter/views/click.py:60 +msgid "Not allowed for that product" +msgstr "Non autorisé pour ce produit" + +#: counter/views/click.py:63 +msgid "No date of birth provided" +msgstr "Pas de date de naissance renseignée" + +#: counter/views/click.py:331 +msgid "You have not enough money to buy all the basket" +msgstr "Vous n'avez pas assez d'argent pour acheter le panier" + +#: counter/views/eticket.py:120 msgid "people(s)" msgstr "personne(s)" +#: counter/views/home.py:74 +msgid "Bad credentials" +msgstr "Mauvais identifiants" + +#: counter/views/home.py:76 +msgid "User is not barman" +msgstr "L'utilisateur n'est pas barman." + +#: counter/views/home.py:81 +msgid "Bad location, someone is already logged in somewhere else" +msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" + +#: counter/views/mixins.py:68 +msgid "Cash summary" +msgstr "Relevé de caisse" + +#: counter/views/mixins.py:77 +msgid "Last operations" +msgstr "Dernières opérations" + +#: counter/views/mixins.py:84 +msgid "Counter administration" +msgstr "Administration des comptoirs" + +#: counter/views/mixins.py:104 +msgid "Product types" +msgstr "Types de produit" + #: eboutic/forms.py:88 msgid "The request was badly formatted." msgstr "La requête a été mal formatée." @@ -4894,12 +4886,12 @@ msgid "Washing and drying" msgstr "Lavage et séchage" #: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:656 +#: sith/settings.py:658 msgid "Washing" msgstr "Lavage" #: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:656 +#: sith/settings.py:658 msgid "Drying" msgstr "Séchage" @@ -5414,380 +5406,380 @@ msgstr "Personne(s)" msgid "Identify users on pictures" msgstr "Identifiez les utilisateurs sur les photos" -#: sith/settings.py:254 sith/settings.py:475 +#: sith/settings.py:253 sith/settings.py:477 msgid "English" msgstr "Anglais" -#: sith/settings.py:254 sith/settings.py:474 +#: sith/settings.py:253 sith/settings.py:476 msgid "French" msgstr "Français" -#: sith/settings.py:394 +#: sith/settings.py:396 msgid "TC" msgstr "TC" -#: sith/settings.py:395 +#: sith/settings.py:397 msgid "IMSI" msgstr "IMSI" -#: sith/settings.py:396 +#: sith/settings.py:398 msgid "IMAP" msgstr "IMAP" -#: sith/settings.py:397 +#: sith/settings.py:399 msgid "INFO" msgstr "INFO" -#: sith/settings.py:398 +#: sith/settings.py:400 msgid "GI" msgstr "GI" -#: sith/settings.py:399 sith/settings.py:485 +#: sith/settings.py:401 sith/settings.py:487 msgid "E" msgstr "E" -#: sith/settings.py:400 +#: sith/settings.py:402 msgid "EE" msgstr "EE" -#: sith/settings.py:401 +#: sith/settings.py:403 msgid "GESC" msgstr "GESC" -#: sith/settings.py:402 +#: sith/settings.py:404 msgid "GMC" msgstr "GMC" -#: sith/settings.py:403 +#: sith/settings.py:405 msgid "MC" msgstr "MC" -#: sith/settings.py:404 +#: sith/settings.py:406 msgid "EDIM" msgstr "EDIM" -#: sith/settings.py:405 +#: sith/settings.py:407 msgid "Humanities" msgstr "Humanités" -#: sith/settings.py:406 +#: sith/settings.py:408 msgid "N/A" msgstr "N/A" -#: sith/settings.py:410 sith/settings.py:417 sith/settings.py:436 +#: sith/settings.py:412 sith/settings.py:419 sith/settings.py:438 msgid "Check" msgstr "Chèque" -#: sith/settings.py:411 sith/settings.py:419 sith/settings.py:437 +#: sith/settings.py:413 sith/settings.py:421 sith/settings.py:439 msgid "Cash" msgstr "Espèces" -#: sith/settings.py:412 +#: sith/settings.py:414 msgid "Transfert" msgstr "Virement" -#: sith/settings.py:425 +#: sith/settings.py:427 msgid "Belfort" msgstr "Belfort" -#: sith/settings.py:426 +#: sith/settings.py:428 msgid "Sevenans" msgstr "Sevenans" -#: sith/settings.py:427 +#: sith/settings.py:429 msgid "Montbéliard" msgstr "Montbéliard" -#: sith/settings.py:455 +#: sith/settings.py:457 msgid "Free" msgstr "Libre" -#: sith/settings.py:456 +#: sith/settings.py:458 msgid "CS" msgstr "CS" -#: sith/settings.py:457 +#: sith/settings.py:459 msgid "TM" msgstr "TM" -#: sith/settings.py:458 +#: sith/settings.py:460 msgid "OM" msgstr "OM" -#: sith/settings.py:459 +#: sith/settings.py:461 msgid "QC" msgstr "QC" -#: sith/settings.py:460 +#: sith/settings.py:462 msgid "EC" msgstr "EC" -#: sith/settings.py:461 +#: sith/settings.py:463 msgid "RN" msgstr "RN" -#: sith/settings.py:462 +#: sith/settings.py:464 msgid "ST" msgstr "ST" -#: sith/settings.py:463 +#: sith/settings.py:465 msgid "EXT" msgstr "EXT" -#: sith/settings.py:468 +#: sith/settings.py:470 msgid "Autumn" msgstr "Automne" -#: sith/settings.py:469 +#: sith/settings.py:471 msgid "Spring" msgstr "Printemps" -#: sith/settings.py:470 +#: sith/settings.py:472 msgid "Autumn and spring" msgstr "Automne et printemps" -#: sith/settings.py:476 +#: sith/settings.py:478 msgid "German" msgstr "Allemand" -#: sith/settings.py:477 +#: sith/settings.py:479 msgid "Spanish" msgstr "Espagnol" -#: sith/settings.py:481 +#: sith/settings.py:483 msgid "A" msgstr "A" -#: sith/settings.py:482 +#: sith/settings.py:484 msgid "B" msgstr "B" -#: sith/settings.py:483 +#: sith/settings.py:485 msgid "C" msgstr "C" -#: sith/settings.py:484 +#: sith/settings.py:486 msgid "D" msgstr "D" -#: sith/settings.py:486 +#: sith/settings.py:488 msgid "FX" msgstr "FX" -#: sith/settings.py:487 +#: sith/settings.py:489 msgid "F" msgstr "F" -#: sith/settings.py:488 +#: sith/settings.py:490 msgid "Abs" msgstr "Abs" -#: sith/settings.py:492 +#: sith/settings.py:494 msgid "Selling deletion" msgstr "Suppression de vente" -#: sith/settings.py:493 +#: sith/settings.py:495 msgid "Refilling deletion" msgstr "Suppression de rechargement" -#: sith/settings.py:537 +#: sith/settings.py:539 msgid "One semester" msgstr "Un semestre, 20 €" -#: sith/settings.py:538 +#: sith/settings.py:540 msgid "Two semesters" msgstr "Deux semestres, 35 €" -#: sith/settings.py:540 +#: sith/settings.py:542 msgid "Common core cursus" msgstr "Cursus tronc commun, 60 €" -#: sith/settings.py:544 +#: sith/settings.py:546 msgid "Branch cursus" msgstr "Cursus branche, 60 €" -#: sith/settings.py:545 +#: sith/settings.py:547 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:546 +#: sith/settings.py:548 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:547 +#: sith/settings.py:549 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:548 +#: sith/settings.py:550 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:549 +#: sith/settings.py:551 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:550 +#: sith/settings.py:552 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:551 +#: sith/settings.py:553 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 20 €" -#: sith/settings.py:553 +#: sith/settings.py:555 msgid "One semester Welcome Week" msgstr "Un semestre Welcome Week" -#: sith/settings.py:557 +#: sith/settings.py:559 msgid "One month for free" msgstr "Un mois gratuit" -#: sith/settings.py:558 +#: sith/settings.py:560 msgid "Two months for free" msgstr "Deux mois gratuits" -#: sith/settings.py:559 +#: sith/settings.py:561 msgid "Eurok's volunteer" msgstr "Bénévole Eurockéennes" -#: sith/settings.py:561 +#: sith/settings.py:563 msgid "Six weeks for free" msgstr "6 semaines gratuites" -#: sith/settings.py:565 +#: sith/settings.py:567 msgid "One day" msgstr "Un jour" -#: sith/settings.py:566 +#: sith/settings.py:568 msgid "GA staff member" msgstr "Membre staff GA (2 semaines), 1 €" -#: sith/settings.py:569 +#: sith/settings.py:571 msgid "One semester (-20%)" msgstr "Un semestre (-20%), 12 €" -#: sith/settings.py:574 +#: sith/settings.py:576 msgid "Two semesters (-20%)" msgstr "Deux semestres (-20%), 22 €" -#: sith/settings.py:579 +#: sith/settings.py:581 msgid "Common core cursus (-20%)" msgstr "Cursus tronc commun (-20%), 36 €" -#: sith/settings.py:584 +#: sith/settings.py:586 msgid "Branch cursus (-20%)" msgstr "Cursus branche (-20%), 36 €" -#: sith/settings.py:589 +#: sith/settings.py:591 msgid "Alternating cursus (-20%)" msgstr "Cursus alternant (-20%), 24 €" -#: sith/settings.py:595 +#: sith/settings.py:597 msgid "One year for free(CA offer)" msgstr "Une année offerte (Offre CA)" -#: sith/settings.py:615 +#: sith/settings.py:617 msgid "President" msgstr "Président⸱e" -#: sith/settings.py:616 +#: sith/settings.py:618 msgid "Vice-President" msgstr "Vice-Président⸱e" -#: sith/settings.py:617 +#: sith/settings.py:619 msgid "Treasurer" msgstr "Trésorier⸱e" -#: sith/settings.py:618 +#: sith/settings.py:620 msgid "Communication supervisor" msgstr "Responsable communication" -#: sith/settings.py:619 +#: sith/settings.py:621 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:620 +#: sith/settings.py:622 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:621 +#: sith/settings.py:623 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:622 +#: sith/settings.py:624 msgid "Active member" msgstr "Membre actif⸱ve" -#: sith/settings.py:623 +#: sith/settings.py:625 msgid "Curious" msgstr "Curieux⸱euse" -#: sith/settings.py:660 +#: sith/settings.py:662 msgid "A new poster needs to be moderated" msgstr "Une nouvelle affiche a besoin d'être modérée" -#: sith/settings.py:661 +#: sith/settings.py:663 msgid "A new mailing list needs to be moderated" msgstr "Une nouvelle mailing list a besoin d'être modérée" -#: sith/settings.py:664 +#: sith/settings.py:666 msgid "A new pedagogy comment has been signaled for moderation" msgstr "" "Un nouveau commentaire de la pédagogie a été signalé pour la modération" -#: sith/settings.py:666 +#: sith/settings.py:668 #, python-format msgid "There are %s fresh news to be moderated" msgstr "Il y a %s nouvelles toutes fraîches à modérer" -#: sith/settings.py:667 +#: sith/settings.py:669 msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:668 +#: sith/settings.py:670 #, python-format msgid "There are %s pictures to be moderated in the SAS" msgstr "Il y a %s photos à modérer dans le SAS" -#: sith/settings.py:669 +#: sith/settings.py:671 msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:670 +#: sith/settings.py:672 #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s€" -#: sith/settings.py:671 +#: sith/settings.py:673 #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:672 +#: sith/settings.py:674 msgid "You have a notification" msgstr "Vous avez une notification" -#: sith/settings.py:684 +#: sith/settings.py:686 msgid "Success!" msgstr "Succès !" -#: sith/settings.py:685 +#: sith/settings.py:687 msgid "Fail!" msgstr "Échec !" -#: sith/settings.py:686 +#: sith/settings.py:688 msgid "You successfully posted an article in the Weekmail" msgstr "Article posté avec succès dans le Weekmail" -#: sith/settings.py:687 +#: sith/settings.py:689 msgid "You successfully edited an article in the Weekmail" msgstr "Article édité avec succès dans le Weekmail" -#: sith/settings.py:688 +#: sith/settings.py:690 msgid "You successfully sent the Weekmail" msgstr "Weekmail envoyé avec succès" -#: sith/settings.py:696 +#: sith/settings.py:698 msgid "AE tee-shirt" msgstr "Tee-shirt AE" @@ -5828,27 +5820,14 @@ msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période" msgid "Subscription created for %(user)s" msgstr "Cotisation créée pour %(user)s" -#: subscription/templates/subscription/fragments/creation_success.jinja:8 -#, python-format -msgid "" -"%(user)s received its new %(type)s subscription. It will be active until " -"%(end)s included." -msgstr "" -"%(user)s a reçu sa nouvelle cotisaton %(type)s. Elle sert active jusqu'au " -"%(end)s inclu." - -#: subscription/templates/subscription/fragments/creation_success.jinja:16 +#: subscription/templates/subscription/fragments/creation_success.jinja:19 msgid "Go to user profile" msgstr "Voir le profil de l'utilisateur" -#: subscription/templates/subscription/fragments/creation_success.jinja:24 +#: subscription/templates/subscription/fragments/creation_success.jinja:27 msgid "Create another subscription" msgstr "Créer une nouvelle cotisation" -#: subscription/templates/subscription/subscription.jinja -msgid "Existing member" -msgstr "Membre existant" - #: subscription/templates/subscription/stats.jinja:27 msgid "Total subscriptions" msgstr "Cotisations totales" @@ -5857,6 +5836,10 @@ msgstr "Cotisations totales" msgid "Subscriptions by type" msgstr "Cotisations par type" +#: subscription/templates/subscription/subscription.jinja:38 +msgid "Existing member" +msgstr "Membre existant" + #: trombi/models.py:55 msgid "subscription deadline" msgstr "fin des inscriptions" From de7aa6f6a6e08bd790ca59a3e3461924f47eda17 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 8 Dec 2024 11:45:16 +0100 Subject: [PATCH 5/6] Create a generic form fragment renderer --- core/templates/core/user_preferences.jinja | 22 +- core/utils.py | 25 +- core/views/user.py | 4 +- counter/templates/counter/counter_click.jinja | 229 +++++++++--------- .../fragments/create_student_card.jinja | 2 +- counter/views/click.py | 4 +- counter/views/student_card.py | 22 +- 7 files changed, 150 insertions(+), 158 deletions(-) diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index d70371a2..722e7c44 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -36,19 +36,11 @@ {% if student_card %} - {% with - form=student_card.form, - action=student_card.context.action, - customer=student_card.context.customer, - student_cards=student_card.context.student_cards - %} - {% include student_card.template %} - {% endwith %} - -

    - {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually - add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} -

    -{% endif %} -
    + {{ student_card }} +

    + {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually + add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} +

    + {% endif %} +
    {% endblock %} \ No newline at end of file diff --git a/core/utils.py b/core/utils.py index 5b6191f6..cdd72fa6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -13,22 +13,41 @@ # # +from dataclasses import dataclass from datetime import date # Image utils from io import BytesIO -from typing import Optional +from typing import Any import PIL from django.conf import settings from django.core.files.base import ContentFile +from django.forms import BaseForm from django.http import HttpRequest +from django.template.loader import render_to_string +from django.utils.html import SafeString from django.utils.timezone import localdate from PIL import ExifTags from PIL.Image import Image, Resampling -def get_start_of_semester(today: Optional[date] = None) -> date: +@dataclass +class FormFragmentTemplateData[T: BaseForm]: + """Dataclass used to pre-render form fragments""" + + form: T + template: str + context: dict[str, Any] + + def render(self, request: HttpRequest) -> SafeString: + # Request is needed for csrf_tokens + return render_to_string( + self.template, context={"form": self.form, **self.context}, request=request + ) + + +def get_start_of_semester(today: date | None = None) -> date: """Return the date of the start of the semester of the given date. If no date is given, return the start date of the current semester. @@ -58,7 +77,7 @@ def get_start_of_semester(today: Optional[date] = None) -> date: return autumn.replace(year=autumn.year - 1) -def get_semester_code(d: Optional[date] = None) -> str: +def get_semester_code(d: date | None = None) -> str: """Return the semester code of the given date. If no date is given, return the semester code of the current semester. diff --git a/core/views/user.py b/core/views/user.py index 83916fb1..2c6b01fc 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -578,8 +578,8 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): kwargs["trombi_form"] = UserTrombiForm() if hasattr(self.object, "customer"): kwargs["student_card"] = StudentCardFormView.get_template_data( - self.request, self.object.customer - ).as_dict() + self.object.customer + ).render(self.request) return kwargs diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index ed89b100..7c36b01b 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -31,131 +31,124 @@

    {% trans %}Amount: {% endtrans %}{{ customer.amount }} €

    {% if counter.type == 'BAR' %} - {% with - form=student_card.form, - action=student_card.context.action, - customer=student_card.context.customer, - student_cards=student_card.context.student_cards - %} - {% include student_card.template %} - {% endwith %} -{% endif %} - + {{ student_card }} + {% endif %} + -
    -
    {% trans %}Selling{% endtrans %}
    -
    - {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} +
    +
    {% trans %}Selling{% endtrans %}
    +
    + {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} {# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #} -
    - {% csrf_token %} - - - - -
    - - -

    {% trans %}Basket: {% endtrans %}

    - -
      - -
    -

    - Total: - - -

    - -
    - {% csrf_token %} - - -
    -
    - {% csrf_token %} - - -
    -
    - {% if (counter.type == 'BAR' and barmens_can_refill) %} -
    {% trans %}Refilling{% endtrans %}
    -
    -
    - {% csrf_token %} - {{ refill_form.as_p() }} - - -
    -
    - {% endif %} -
    - -
    -
      - {% for category in categories.keys() -%} -
    • {{ category }}
    • - {%- endfor %} -
    - {% for category in categories.keys() -%} -
    -
    {{ category }}
    - {% for p in categories[category] -%} -
    + {% csrf_token %} - - - + + + +
    + + +

    {% trans %}Basket: {% endtrans %}

    + +
      + +
    +

    + Total: + + +

    + +
    + {% csrf_token %} + + +
    +
    + {% csrf_token %} + + +
    +
    + {% if (counter.type == 'BAR' and barmens_can_refill) %} +
    {% trans %}Refilling{% endtrans %}
    +
    +
    + {% csrf_token %} + {{ refill_form.as_p() }} + + +
    +
    + {% endif %} +
    + +
    +
      + {% for category in categories.keys() -%} +
    • {{ category }}
    • + {%- endfor %} +
    + {% for category in categories.keys() -%} +
    +
    {{ category }}
    + {% for p in categories[category] -%} +
    + {% csrf_token %} + + + +
    + {%- endfor %} +
    {%- endfor %}
    - {%- endfor %} -
    -
    + {% endblock content %} {% block script %} diff --git a/counter/templates/counter/fragments/create_student_card.jinja b/counter/templates/counter/fragments/create_student_card.jinja index 7cd05ba9..ab846c55 100644 --- a/counter/templates/counter/fragments/create_student_card.jinja +++ b/counter/templates/counter/fragments/create_student_card.jinja @@ -7,7 +7,7 @@ > {% csrf_token %} {{ form.as_p() }} - +
    {% trans %}Registered cards{% endtrans %}
    diff --git a/counter/views/click.py b/counter/views/click.py index c0845aaf..2fa9684d 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -416,6 +416,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): kwargs["refill_form"] = self.refill_form or RefillForm() kwargs["barmens_can_refill"] = self.object.can_refill() kwargs["student_card"] = StudentCardFormView.get_template_data( - self.request, self.customer - ).as_dict() + self.customer + ).render(self.request) return kwargs diff --git a/counter/views/student_card.py b/counter/views/student_card.py index fb919ce2..99f67316 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -14,31 +14,19 @@ # -from dataclasses import asdict, dataclass -from typing import Any - from django.core.exceptions import PermissionDenied from django.http import HttpRequest from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy from django.views.generic.edit import DeleteView, FormView +from core.utils import FormFragmentTemplateData from core.views import CanEditMixin from counter.forms import StudentCardForm from counter.models import Customer, StudentCard from counter.utils import is_logged_in_counter -@dataclass -class StudentCardTemplateData: - form: StudentCardForm - template: str - context: dict[str, Any] - - def as_dict(self) -> dict[str, Any]: - return asdict(self) - - class StudentCardDeleteView(DeleteView, CanEditMixin): """View used to delete a card from a user.""" @@ -64,10 +52,10 @@ class StudentCardFormView(FormView): @classmethod def get_template_data( - cls, request: HttpRequest, customer: Customer - ) -> StudentCardTemplateData: + cls, customer: Customer + ) -> FormFragmentTemplateData[form_class]: """Get necessary data to pre-render the fragment""" - return StudentCardTemplateData( + return FormFragmentTemplateData[cls.form_class]( form=cls.form_class(), template=cls.template_name, context={ @@ -99,7 +87,7 @@ class StudentCardFormView(FormView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - data = self.get_template_data(self.request, self.customer) + data = self.get_template_data(self.customer) context.update(data.context) return context From 29a5425259bc1fbf61175ca61fe43c8cd888256d Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 8 Dec 2024 12:31:35 +0100 Subject: [PATCH 6/6] Add spinner to student card form --- core/static/bundled/htmx-index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/static/bundled/htmx-index.js b/core/static/bundled/htmx-index.js index 56edea4a..474617ac 100644 --- a/core/static/bundled/htmx-index.js +++ b/core/static/bundled/htmx-index.js @@ -1,3 +1,11 @@ import htmx from "htmx.org"; +document.body.addEventListener("htmx:beforeRequest", (event) => { + event.target.ariaBusy = true; +}); + +document.body.addEventListener("htmx:afterRequest", (event) => { + event.originalTarget.ariaBusy = null; +}); + Object.assign(window, { htmx });