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
This commit is contained in:
Antoine Bartuccio 2024-11-14 16:17:10 +01:00
parent 1da45fdffc
commit b81cf49d0a
8 changed files with 196 additions and 97 deletions

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

@ -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

@ -0,0 +1,25 @@
<div id="student_card_form">
<h3>{% trans %}Add a student card{% endtrans %}</h3>
<form
hx-trigger="submit"
hx-post="{{ action }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
>
{% csrf_token %}
{{ form.as_p() }}
<button>{% trans %}Go{% endtrans %}</button>
</form>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if student_cards %}
<ul>
{% for card in student_cards %}
<li>{{ card.uid }}</li>
{% endfor %}
</ul>
{% else %}
{% trans %}No card registered{% endtrans %}
{% endif %}
</div>

View File

@ -29,26 +29,15 @@
{{ 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 %} <div
<li>{{ card.uid }}</li> hx-get="{{ url('counter:add_student_card_fragment', counter_id=counter.id, customer_id=customer.pk) }}"
{% endfor %} hx-trigger="load"
</ul> hx-swap="outerHTML"
{% else %} >
{% trans %}No card registered{% endtrans %} <div aria-busy="true" style="min-height: 100px;"></div>
</div>
{% endif %} {% endif %}
</div> </div>

View File

@ -168,6 +168,7 @@ class TestStudentCard(TestCase):
cls.root = User.objects.get(username="root") cls.root = User.objects.get(username="root")
cls.counter = Counter.objects.get(id=2) cls.counter = Counter.objects.get(id=2)
cls.ae_counter = Counter.objects.get(name="AE")
def setUp(self): def setUp(self):
# Auto login on counter # Auto login on counter
@ -191,94 +192,144 @@ class TestStudentCard(TestCase):
# 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_fragment",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, 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 # Test card with only numbers
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card_fragment",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, 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 # Test card with only letters
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:click", "counter:add_student_card_fragment",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, 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): 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_fragment",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"counter_id": self.counter.id,
"customer_id": self.sli.customer.pk,
},
), ),
{"student_card_uid": "8B90734A802A8", "action": "add_student_card"}, {"uid": "8B90734A802A8"},
)
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_fragment",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, 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( 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_fragment",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, 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( 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_fragment",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"counter_id": self.counter.id,
"customer_id": self.sli.customer.pk,
},
), ),
{"student_card_uid": "8b90734a802a9f", "action": "add_student_card"}, {"uid": "8b90734a802a9f"},
)
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_fragment",
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, kwargs={
"counter_id": self.counter.id,
"customer_id": self.sli.customer.pk,
},
), ),
{"student_card_uid": " ", "action": "add_student_card"}, {"uid": " "},
) )
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.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): def test_delete_student_card_with_owner(self):
self.client.force_login(self.sli) self.client.force_login(self.sli)
self.client.post( self.client.post(

View File

@ -52,7 +52,11 @@ 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,
StudentCardFormFragmentView,
StudentCardFormView,
)
urlpatterns = [ urlpatterns = [
path("<int:counter_id>/", CounterMain.as_view(), name="details"), path("<int:counter_id>/", CounterMain.as_view(), name="details"),
@ -77,6 +81,11 @@ urlpatterns = [
StudentCardFormView.as_view(), StudentCardFormView.as_view(),
name="add_student_card", name="add_student_card",
), ),
path(
"customer/<int:customer_id>/card/add/counter/<int:counter_id>/",
StudentCardFormFragmentView.as_view(),
name="add_student_card_fragment",
),
path( path(
"customer/<int:customer_id>/card/delete/<int:card_id>/", "customer/<int:customer_id>/card/delete/<int:card_id>/",
StudentCardDeleteView.as_view(), StudentCardDeleteView.as_view(),

View File

@ -27,8 +27,8 @@ 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
if TYPE_CHECKING: if TYPE_CHECKING:
@ -134,7 +134,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 +145,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 +281,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 +411,7 @@ 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()
return kwargs return kwargs

View File

@ -18,9 +18,9 @@ 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.views import CanEditMixin from core.views import AllowFragment, CanEditMixin
from counter.forms import StudentCardForm from counter.forms import StudentCardForm
from counter.models import Customer, StudentCard from counter.models import Counter, Customer, StudentCard
class StudentCardDeleteView(DeleteView, CanEditMixin): class StudentCardDeleteView(DeleteView, CanEditMixin):
@ -40,7 +40,7 @@ class StudentCardDeleteView(DeleteView, CanEditMixin):
) )
class StudentCardFormView(FormView): class StudentCardFormView(AllowFragment, FormView):
"""Add a new student card.""" """Add a new student card."""
form_class = StudentCardForm form_class = StudentCardForm
@ -62,3 +62,52 @@ class StudentCardFormView(FormView):
return reverse_lazy( return reverse_lazy(
"core:user_prefs", kwargs={"user_id": self.customer.user.pk} "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,
},
)