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) {
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):
"""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"

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

View File

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

View File

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

View File

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

View File

@ -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,
},
)