Merge pull request #921 from ae-utbm/counter-click

Casser counter click étape 1 : introduire des fragments
This commit is contained in:
Bartuccio Antoine 2024-12-08 13:49:08 +01:00 committed by GitHub
commit 902cafc5e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 608 additions and 439 deletions

View File

@ -1,3 +1,11 @@
import htmx from "htmx.org"; 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 }); Object.assign(window, { htmx });

View File

@ -6,7 +6,16 @@
**/ **/
export function registerComponent(name: string, options?: ElementDefinitionOptions) { export function registerComponent(name: string, options?: ElementDefinitionOptions) {
return (component: CustomElementConstructor) => { 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;
}
}; };
} }

View File

@ -35,35 +35,12 @@
{% endif %} {% endif %}
{% if profile.customer %} {% if student_card %}
<h3>{% trans %}Student cards{% endtrans %}</h3> {{ student_card }}
<p class="justify">
{% if profile.customer.student_cards.exists() %} {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually
<ul class="student-cards"> add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %}
{% for card in profile.customer.student_cards.all() %} </p>
<li>
{{ card.uid }}
&nbsp;-&nbsp;
<a href="{{ url('counter:delete_student_card', customer_id=profile.customer.pk, card_id=card.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<em class="no-cards">{% trans %}No student card registered.{% endtrans %}</em>
<p class="justify">
{% 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 %}
</p>
{% endif %}
<form class="form form-cards" action="{{ url('counter:add_student_card', customer_id=profile.customer.pk) }}"
method="post">
{% csrf_token %}
{{ student_card_form.as_p() }}
<input class="form-submit-btn" type="submit" value="{% trans %}Save{% endtrans %}" />
</form>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -13,22 +13,41 @@
# #
# #
from dataclasses import dataclass
from datetime import date from datetime import date
# Image utils # Image utils
from io import BytesIO from io import BytesIO
from typing import Optional from typing import Any
import PIL import PIL
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.forms import BaseForm
from django.http import HttpRequest 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 django.utils.timezone import localdate
from PIL import ExifTags from PIL import ExifTags
from PIL.Image import Image, Resampling 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. """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. 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) 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. """Return the semester code of the given date.
If no date is given, return the semester code of the current semester. If no date is given, return the semester code of the current semester.

View File

@ -70,8 +70,8 @@ from core.views.forms import (
UserGodfathersForm, UserGodfathersForm,
UserProfileForm, UserProfileForm,
) )
from counter.forms import StudentCardForm
from counter.models import Refilling, Selling from counter.models import Refilling, Selling
from counter.views.student_card import StudentCardFormView
from eboutic.models import Invoice from eboutic.models import Invoice
from subscription.models import Subscription from subscription.models import Subscription
from trombi.views import UserTrombiForm from trombi.views import UserTrombiForm
@ -576,9 +576,10 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
hasattr(self.object, "trombi_user") and self.request.user.trombi_user.trombi hasattr(self.object, "trombi_user") and self.request.user.trombi_user.trombi
): ):
kwargs["trombi_form"] = UserTrombiForm() kwargs["trombi_form"] = UserTrombiForm()
if hasattr(self.object, "customer"): if hasattr(self.object, "customer"):
kwargs["student_card_form"] = StudentCardForm() kwargs["student_card"] = StudentCardFormView.get_template_data(
self.object.customer
).render(self.request)
return kwargs return kwargs

View File

@ -45,9 +45,7 @@ class BillingInfoForm(forms.ModelForm):
class StudentCardForm(forms.ModelForm): class StudentCardForm(forms.ModelForm):
"""Form for adding student cards """Form for adding student cards"""
Only used for user profile since CounterClick is to complicated.
"""
class Meta: class Meta:
model = StudentCard model = StudentCard
@ -114,14 +112,6 @@ class GetUserForm(forms.Form):
return cleaned_data 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): class RefillForm(forms.ModelForm):
error_css_class = "error" error_css_class = "error"
required_css_class = "required" required_css_class = "required"

View File

@ -29,26 +29,9 @@
{{ user_mini_profile(customer.user) }} {{ user_mini_profile(customer.user) }}
{{ user_subscription(customer.user) }} {{ user_subscription(customer.user) }}
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p> <p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="add_student_card">
{% trans %}Add a student card{% endtrans %}
{{ student_card_input.student_card_uid }}
{% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if student_cards %}
<ul> {% if counter.type == 'BAR' %}
{% for card in student_cards %} {{ student_card }}
<li>{{ card.uid }}</li>
{% endfor %}
</ul>
{% else %}
{% trans %}No card registered{% endtrans %}
{% endif %} {% endif %}
</div> </div>

View File

@ -0,0 +1,29 @@
<div id="student_card_form">
<h3>{% trans %}Add a student card{% endtrans %}</h3>
<form
hx-post="{{ action }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
>
{% csrf_token %}
{{ form.as_p() }}
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if student_cards %}
<ul>
{% for card in student_cards %}
<li>
{{ card.uid }}
<a href="{{ url('counter:delete_student_card', customer_id=customer.pk, card_id=card.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<em class="no-cards">{% trans %}No student card registered.{% endtrans %}</em>
{% endif %}
</div>

View File

@ -1,15 +1,27 @@
import json import json
import string import string
from datetime import timedelta
import pytest import pytest
from django.conf import settings
from django.contrib.auth.base_user import make_password
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now
from model_bakery import baker 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 core.models import User
from counter.baker_recipes import refill_recipe, sale_recipe 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 @pytest.mark.django_db
@ -162,148 +174,269 @@ class TestStudentCard(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.krophil = User.objects.get(username="krophil") cls.customer = subscriber_user.make()
cls.sli = User.objects.get(username="sli") cls.customer.save()
cls.skia = User.objects.get(username="skia") cls.barmen = subscriber_user.make(password=make_password("plop"))
cls.root = User.objects.get(username="root") 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.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): def setUp(self):
# Auto login on counter # Auto login on counter
self.client.post( self.client.post(
reverse("counter:login", args=[self.counter.id]), 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): def test_search_user_with_student_card(self):
response = self.client.post( response = self.client.post(
reverse("counter:details", args=[self.counter.id]), reverse("counter:details", args=[self.counter.id]),
{"code": "9A89B82018B0A0"}, {"code": self.valid_card.uid},
) )
assert response.url == reverse( assert response.url == reverse(
"counter:click", "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): def test_add_student_card_from_counter(self):
# Test card with mixed letters and numbers # Test card with mixed letters and numbers
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"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,
},
), ),
{"student_card_uid": "8B90734A802A8F", "action": "add_student_card"},
) )
self.assertContains(response, text="8B90734A802A8F") assert response.status_code == 302
self.assertContains(self.client.get(response.url), text="8B90734A802A8F")
# Test card with only numbers # Test card with only numbers
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"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,
},
), ),
{"student_card_uid": "04786547890123", "action": "add_student_card"},
) )
self.assertContains(response, text="04786547890123") assert response.status_code == 302
self.assertContains(self.client.get(response.url), text="04786547890123")
# Test card with only letters # Test card with only letters
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"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,
},
), ),
{"student_card_uid": "ABCAAAFAAFAAAB", "action": "add_student_card"},
) )
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): def test_add_student_card_from_counter_fail(self):
# UID too short # UID too short
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"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,
},
), ),
{"student_card_uid": "8B90734A802A8", "action": "add_student_card"},
)
self.assertContains(
response, text="Ce n'est pas un UID de carte étudiante valide"
) )
self.assertContains(response, text="Cet UID est invalide")
# UID too long # UID too long
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"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,
},
), ),
{"student_card_uid": "8B90734A802A8FA", "action": "add_student_card"},
) )
self.assertContains(response, text="Cet UID est invalide")
self.assertContains( 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 # Test with already existing card
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"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,
},
), ),
{"student_card_uid": "9A89B82018B0A0", "action": "add_student_card"},
) )
self.assertContains(response, text="Cet UID est invalide")
self.assertContains( 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 # Test with lowercase
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"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,
},
), ),
{"student_card_uid": "8b90734a802a9f", "action": "add_student_card"},
)
self.assertContains(
response, text="Ce n'est pas un UID de carte étudiante valide"
) )
self.assertContains(response, text="Cet UID est invalide")
# Test with white spaces # Test with white spaces
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"customer_id": self.customer.customer.pk,
},
),
{"uid": " "},
HTTP_REFERER=reverse(
"counter:click",
kwargs={
"counter_id": self.counter.id,
"user_id": self.customer.customer.pk,
},
), ),
{"student_card_uid": " ", "action": "add_student_card"},
) )
self.assertContains( self.assertContains(response, text="Cet UID est invalide")
response, text="Ce n'est pas un UID de carte étudiante valide" 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.barmen.id},
) )
def send_valid_request(client, counter_id):
return client.post(
reverse(
"counter:add_student_card",
kwargs={
"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.club_admin)
assert send_valid_request(self.client, self.club_counter.id).status_code == 403
def test_delete_student_card_with_owner(self): def test_delete_student_card_with_owner(self):
self.client.force_login(self.sli) self.client.force_login(self.customer)
self.client.post( self.client.post(
reverse( reverse(
"counter:delete_student_card", "counter:delete_student_card",
kwargs={ kwargs={
"customer_id": self.sli.customer.pk, "customer_id": self.customer.customer.pk,
"card_id": self.sli.customer.student_cards.first().id, "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): 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( self.client.post(
reverse( reverse(
"counter:delete_student_card", "counter:delete_student_card",
kwargs={ kwargs={
"customer_id": self.sli.customer.pk, "customer_id": self.customer.customer.pk,
"card_id": self.sli.customer.student_cards.first().id, "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): def test_delete_student_card_with_root(self):
self.client.force_login(self.root) self.client.force_login(self.root)
@ -311,100 +444,107 @@ class TestStudentCard(TestCase):
reverse( reverse(
"counter:delete_student_card", "counter:delete_student_card",
kwargs={ kwargs={
"customer_id": self.sli.customer.pk, "customer_id": self.customer.customer.pk,
"card_id": self.sli.customer.student_cards.first().id, "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): def test_delete_student_card_fail(self):
self.client.force_login(self.krophil) self.client.force_login(self.subscriber)
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:delete_student_card", "counter:delete_student_card",
kwargs={ kwargs={
"customer_id": self.sli.customer.pk, "customer_id": self.customer.customer.pk,
"card_id": self.sli.customer.student_cards.first().id, "card_id": self.customer.customer.student_cards.first().id,
}, },
) )
) )
assert response.status_code == 403 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): def test_add_student_card_from_user_preferences(self):
# Test with owner of the card # Test with owner of the card
self.client.force_login(self.sli) self.client.force_login(self.customer)
self.client.post( response = self.client.post(
reverse( 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"}, {"uid": "8B90734A802A8F"},
) )
response = self.client.get( assert response.status_code == 302
reverse("core:user_prefs", kwargs={"user_id": self.sli.id})
) response = self.client.get(response.url)
self.assertContains(response, text="8B90734A802A8F") self.assertContains(response, text="8B90734A802A8F")
# Test with board member # Test with board member
self.client.force_login(self.skia) self.client.force_login(self.board_admin)
self.client.post( response = self.client.post(
reverse( 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"}, {"uid": "8B90734A802A8A"},
) )
response = self.client.get( assert response.status_code == 302
reverse("core:user_prefs", kwargs={"user_id": self.sli.id})
) response = self.client.get(response.url)
self.assertContains(response, text="8B90734A802A8A") self.assertContains(response, text="8B90734A802A8A")
# Test card with only numbers # Test card with only numbers
self.client.post( response = self.client.post(
reverse( 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"}, {"uid": "04786547890123"},
) )
response = self.client.get( assert response.status_code == 302
reverse("core:user_prefs", kwargs={"user_id": self.sli.id})
) response = self.client.get(response.url)
self.assertContains(response, text="04786547890123") self.assertContains(response, text="04786547890123")
# Test card with only letters # Test card with only letters
self.client.post( response = self.client.post(
reverse( 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"}, {"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") self.assertContains(response, text="ABCAAAFAAFAAAB")
# Test with root # Test with root
self.client.force_login(self.root) self.client.force_login(self.root)
self.client.post( response = self.client.post(
reverse( 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"}, {"uid": "8B90734A802A8B"},
) )
response = self.client.get( assert response.status_code == 302
reverse("core:user_prefs", kwargs={"user_id": self.sli.id})
) response = self.client.get(response.url)
self.assertContains(response, text="8B90734A802A8B") self.assertContains(response, text="8B90734A802A8B")
def test_add_student_card_from_user_preferences_fail(self): 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 # UID too short
response = self.client.post( response = self.client.post(
reverse( 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"}, {"uid": "8B90734A802A8"},
) )
@ -414,7 +554,8 @@ class TestStudentCard(TestCase):
# UID too long # UID too long
response = self.client.post( response = self.client.post(
reverse( 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"}, {"uid": "8B90734A802A8FA"},
) )
@ -423,9 +564,10 @@ class TestStudentCard(TestCase):
# Test with already existing card # Test with already existing card
response = self.client.post( response = self.client.post(
reverse( 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( self.assertContains(
response, text="Un objet Student card avec ce champ Uid existe déjà." response, text="Un objet Student card avec ce champ Uid existe déjà."
@ -434,7 +576,8 @@ class TestStudentCard(TestCase):
# Test with lowercase # Test with lowercase
response = self.client.post( response = self.client.post(
reverse( 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"}, {"uid": "8b90734a802a9f"},
) )
@ -443,17 +586,19 @@ class TestStudentCard(TestCase):
# Test with white spaces # Test with white spaces
response = self.client.post( response = self.client.post(
reverse( 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}, {"uid": " " * 14},
) )
self.assertContains(response, text="Cet UID est invalide") self.assertContains(response, text="Cet UID est invalide")
# Test with unauthorized user # Test with unauthorized user
self.client.force_login(self.krophil) self.client.force_login(self.subscriber)
response = self.client.post( response = self.client.post(
reverse( 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"}, {"uid": "8B90734A802A8F"},
) )

View File

@ -52,7 +52,10 @@ from counter.views.home import (
CounterMain, CounterMain,
) )
from counter.views.invoice import InvoiceCallView from counter.views.invoice import InvoiceCallView
from counter.views.student_card import StudentCardDeleteView, StudentCardFormView from counter.views.student_card import (
StudentCardDeleteView,
StudentCardFormView,
)
urlpatterns = [ urlpatterns = [
path("<int:counter_id>/", CounterMain.as_view(), name="details"), path("<int:counter_id>/", CounterMain.as_view(), name="details"),

View File

@ -22,14 +22,22 @@ def is_logged_in_counter(request: HttpRequest) -> bool:
to the counter) to the counter)
- The current session has a counter token associated with it. - The current session has a counter token associated with it.
- A counter with this token exists. - A counter with this token exists.
- The counter is open
""" """
referer_ok = ( referer_ok = (
"HTTP_REFERER" in request.META "HTTP_REFERER" in request.META
and resolve(urlparse(request.META["HTTP_REFERER"]).path).app_name == "counter" and resolve(urlparse(request.META["HTTP_REFERER"]).path).app_name == "counter"
) )
return ( has_token = (
(referer_ok or request.resolver_match.app_name == "counter") (referer_ok or request.resolver_match.app_name == "counter")
and "counter_token" in request.session and "counter_token" in request.session
and request.session["counter_token"] 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()
) )

View File

@ -27,9 +27,10 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView from django.views.generic import DetailView
from core.views import CanViewMixin from core.views import CanViewMixin
from counter.forms import NFCCardForm, RefillForm from counter.forms import RefillForm
from counter.models import Counter, Customer, Product, Selling, StudentCard from counter.models import Counter, Customer, Product, Selling
from counter.views.mixins import CounterTabsMixin from counter.views.mixins import CounterTabsMixin
from counter.views.student_card import StudentCardFormView
if TYPE_CHECKING: if TYPE_CHECKING:
from core.models import User from core.models import User
@ -134,7 +135,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session["too_young"] = False request.session["too_young"] = False
request.session["not_allowed"] = False request.session["not_allowed"] = False
request.session["no_age"] = False request.session["no_age"] = False
request.session["not_valid_student_card_uid"] = False
if self.object.type != "BAR": if self.object.type != "BAR":
self.operator = request.user self.operator = request.user
elif self.customer_is_barman(): elif self.customer_is_barman():
@ -146,8 +146,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
action = parse_qs(request.body.decode()).get("action", [""])[0] action = parse_qs(request.body.decode()).get("action", [""])[0]
if action == "add_product": if action == "add_product":
self.add_product(request) self.add_product(request)
elif action == "add_student_card":
self.add_student_card(request)
elif action == "del_product": elif action == "del_product":
self.del_product(request) self.del_product(request)
elif action == "refill": elif action == "refill":
@ -284,23 +282,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session.modified = True request.session.modified = True
return 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): def del_product(self, request):
"""Delete a product from the basket.""" """Delete a product from the basket."""
pid = parse_qs(request.body.decode())["product_id"][0] pid = parse_qs(request.body.decode())["product_id"][0]
@ -431,10 +412,10 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
product product
) )
kwargs["customer"] = self.customer 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["basket_total"] = self.sum_basket(self.request)
kwargs["refill_form"] = self.refill_form or RefillForm() 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() kwargs["barmens_can_refill"] = self.object.can_refill()
kwargs["student_card"] = StudentCardFormView.get_template_data(
self.customer
).render(self.request)
return kwargs return kwargs

View File

@ -13,14 +13,18 @@
# #
# #
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views.generic.edit import DeleteView, FormView from django.views.generic.edit import DeleteView, FormView
from core.utils import FormFragmentTemplateData
from core.views import CanEditMixin from core.views import CanEditMixin
from counter.forms import StudentCardForm from counter.forms import StudentCardForm
from counter.models import Customer, StudentCard from counter.models import Customer, StudentCard
from counter.utils import is_logged_in_counter
class StudentCardDeleteView(DeleteView, CanEditMixin): class StudentCardDeleteView(DeleteView, CanEditMixin):
@ -41,15 +45,38 @@ class StudentCardDeleteView(DeleteView, CanEditMixin):
class StudentCardFormView(FormView): class StudentCardFormView(FormView):
"""Add a new student card.""" """Add a new student card. This is a fragment view !"""
form_class = StudentCardForm form_class = StudentCardForm
template_name = "core/create.jinja" template_name = "counter/fragments/create_student_card.jinja"
def dispatch(self, request, *args, **kwargs): @classmethod
self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) def get_template_data(
if not StudentCard.can_create(self.customer, request.user): cls, customer: Customer
) -> FormFragmentTemplateData[form_class]:
"""Get necessary data to pre-render the fragment"""
return FormFragmentTemplateData[cls.form_class](
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"]
)
if not is_logged_in_counter(request) and not StudentCard.can_create(
self.customer, request.user
):
raise PermissionDenied raise PermissionDenied
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
@ -58,7 +85,11 @@ class StudentCardFormView(FormView):
StudentCard(customer=self.customer, uid=data["uid"]).save() StudentCard(customer=self.customer, uid=data["uid"]).save()
return res return res
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
data = self.get_template_data(self.customer)
context.update(data.context)
return context
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
return reverse_lazy( return self.request.path
"core:user_prefs", kwargs={"user_id": self.customer.user.pk}
)

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.db import transaction from django.db import transaction
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from model_bakery import baker from model_bakery import baker
from model_bakery.recipe import Recipe from model_bakery.recipe import Recipe
from pytest_django.asserts import assertNumQueries
from core.baker_recipes import old_subscriber_user, subscriber_user from core.baker_recipes import old_subscriber_user, subscriber_user
from core.models import RealGroup, SithFile, User from core.models import RealGroup, SithFile, User
@ -128,9 +128,11 @@ class TestPictureSearch(TestSas):
def test_num_queries(self): def test_num_queries(self):
"""Test that the number of queries is stable.""" """Test that the number of queries is stable."""
self.client.force_login(subscriber_user.make()) 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 # 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 fetch the pictures
# 1 request to count the total number of items in the pagination # 1 request to count the total number of items in the pagination
self.client.get(self.url) self.client.get(self.url)