Convert customer refill to a fragment view

This commit is contained in:
Antoine Bartuccio 2024-12-15 21:33:19 +01:00
parent 0f003870bb
commit e9361697f7
9 changed files with 148 additions and 52 deletions

View File

@ -21,11 +21,12 @@ from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema
from core.api_permissions import CanAccessLookup, CanView, IsRoot
from counter.models import Counter, Product
from core.api_permissions import CanAccessLookup, CanView, IsLoggedInCounter, IsRoot
from counter.models import Counter, Customer, Product
from counter.schemas import (
CounterFilterSchema,
CounterSchema,
CustomerBalance,
ProductSchema,
SimplifiedCounterSchema,
)
@ -60,6 +61,13 @@ class CounterController(ControllerBase):
return filters.filter(Counter.objects.all())
@api_controller("/customer")
class CustomerController(ControllerBase):
@route.get("/balance", response=CustomerBalance, permissions=[IsLoggedInCounter])
def get_balance(self, customer_id: int):
return self.get_object_or_exception(Customer, pk=customer_id)
@api_controller("/product")
class ProductController(ControllerBase):
@route.get(

View File

@ -4,7 +4,7 @@ from annotated_types import MinLen
from ninja import Field, FilterSchema, ModelSchema
from core.schemas import SimpleUserSchema
from counter.models import Counter, Product
from counter.models import Counter, Customer, Product
class CounterSchema(ModelSchema):
@ -16,6 +16,12 @@ class CounterSchema(ModelSchema):
fields = ["id", "name", "type", "club", "products"]
class CustomerBalance(ModelSchema):
class Meta:
model = Customer
fields = ["amount"]
class CounterFilterSchema(FilterSchema):
search: Annotated[str, MinLen(1)] = Field(None, q="name__icontains")

View File

@ -6,7 +6,7 @@
{% endblock %}
{% block additional_js %}
<script src="{{ static('counter/js/counter_click.js') }}" defer></script>
<script type="module" src="{{ static('bundled/counter/counter-click-index.ts') }}"></script>
{% endblock %}
{% block info_boxes %}
@ -28,7 +28,7 @@
<h5>{% trans %}Customer{% endtrans %}</h5>
{{ user_mini_profile(customer.user) }}
{{ user_subscription(customer.user) }}
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<p>{% trans %}Amount: {% endtrans %}<span x-text="customerBalance"></span> €</p>
{% if counter.type == 'BAR' %}
<h5>{% trans %}Student card{% endtrans %}</h3>
@ -105,16 +105,12 @@
<input type="submit" value="{% trans %}Cancel{% endtrans %}"/>
</form>
</div>
{% if (counter.type == 'BAR' and barmens_can_refill) %}
{% if refilling_fragment %}
<h5>{% trans %}Refilling{% endtrans %}</h5>
<div>
<form method="post"
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
{{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill">
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
<div
@htmx:after-request="updateBalance()"
>
{{ refilling_fragment }}
</div>
{% endif %}
</div>
@ -155,9 +151,6 @@
{% block script %}
{{ super() }}
<script>
const csrfToken = "{{ csrf_token }}";
const clickApiUrl = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}";
const sessionBasket = {{ request.session["basket"]|tojson }};
const products = {
{%- for p in products -%}
{{ p.id }}: {
@ -176,5 +169,14 @@
},
{%- endfor %}
];
window.addEventListener("DOMContentLoaded", () => {
loadCounter({
csrfToken: "{{ csrf_token }}",
clickApiUrl: "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}",
sessionBasket: {{ request.session["basket"]|tojson }},
customerBalance: {{ customer.amount }},
customerId: {{ customer.pk }},
});
});
</script>
{% endblock script %}

View File

@ -0,0 +1,9 @@
<form
hx-trigger="submit"
hx-post="{{ action }}"
hx-swap="outerHTML"
>
{% csrf_token %}
{{ form.as_p() }}
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>

View File

@ -67,17 +67,24 @@ class TestCounter(TestCase):
{"code": self.richard.customer.account_id, "counter_token": counter_token},
)
counter_url = response.get("location")
response = self.client.get(response.get("location"))
refill_url = reverse(
"counter:refilling_create",
kwargs={"customer_id": self.richard.customer.pk},
)
response = self.client.get(
response.get("location"),
)
assert ">Richard Batsbak</" in str(response.content)
self.client.post(
counter_url,
refill_url,
{
"action": "refill",
"amount": "5",
"payment_method": "CASH",
"bank": "OTHER",
},
HTTP_REFERER=counter_url,
)
self.client.post(counter_url, "action=code&code=BARB", content_type="text/xml")
self.client.post(
@ -110,15 +117,15 @@ class TestCounter(TestCase):
)
response = self.client.post(
counter_url,
refill_url,
{
"action": "refill",
"amount": "5",
"payment_method": "CASH",
"bank": "OTHER",
},
HTTP_REFERER=counter_url,
)
assert response.status_code == 200
assert response.status_code == 302
self.client.post(
reverse("counter:login", kwargs={"counter_id": self.foyer.id}),
@ -138,17 +145,23 @@ class TestCounter(TestCase):
{"code": self.richard.customer.account_id, "counter_token": counter_token},
)
counter_url = response.get("location")
refill_url = reverse(
"counter:refilling_create",
kwargs={
"customer_id": self.richard.customer.pk,
},
)
response = self.client.post(
counter_url,
refill_url,
{
"action": "refill",
"amount": "5",
"payment_method": "CASH",
"bank": "OTHER",
},
HTTP_REFERER=counter_url,
)
assert response.status_code == 200
assert response.status_code == 302
def test_annotate_has_barman_queryset(self):
"""Test if the custom queryset method `annotate_has_barman` works as intended."""

View File

@ -39,7 +39,7 @@ from counter.views.cash import (
CashSummaryListView,
CounterCashSummaryView,
)
from counter.views.click import CounterClick
from counter.views.click import CounterClick, RefillingCreateView
from counter.views.eticket import (
EticketCreateView,
EticketEditView,
@ -60,6 +60,11 @@ from counter.views.student_card import (
urlpatterns = [
path("<int:counter_id>/", CounterMain.as_view(), name="details"),
path("<int:counter_id>/click/<int:user_id>/", CounterClick.as_view(), name="click"),
path(
"refill/<int:customer_id>/",
RefillingCreateView.as_view(),
name="refilling_create",
),
path(
"<int:counter_id>/last_ops/",
CounterLastOperationsView.as_view(),

View File

@ -24,11 +24,13 @@ from django.http import Http404, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView
from django.views.generic import DetailView, FormView
from core.utils import FormFragmentTemplateData
from core.views import CanViewMixin
from counter.forms import RefillForm
from counter.models import Counter, Customer, Product, Selling
from counter.utils import is_logged_in_counter
from counter.views.mixins import CounterTabsMixin
from counter.views.student_card import StudentCardFormView
@ -100,7 +102,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session["too_young"] = False
request.session["not_allowed"] = False
request.session["no_age"] = False
self.refill_form = None
ret = super().get(request, *args, **kwargs)
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
self.object.type == "BAR" and len(self.object.barmen_list) == 0
@ -111,7 +112,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
def post(self, request, *args, **kwargs):
"""Handle the many possibilities of the post request."""
self.object = self.get_object()
self.refill_form = None
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
self.object.type == "BAR" and len(self.object.barmen_list) < 1
): # Check that at least one barman is logged in
@ -148,8 +148,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
self.add_product(request)
elif action == "del_product":
self.del_product(request)
elif action == "refill":
self.refill(request)
elif action == "code":
return self.parse_code(request)
elif action == "cancel":
@ -383,19 +381,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
)
def refill(self, request):
"""Refill the customer's account."""
if not self.object.can_refill():
raise PermissionDenied
form = RefillForm(request.POST)
if form.is_valid():
form.instance.counter = self.object
form.instance.operator = self.operator
form.instance.customer = self.customer
form.instance.save()
else:
self.refill_form = form
def get_context_data(self, **kwargs):
"""Add customer to the context."""
kwargs = super().get_context_data(**kwargs)
@ -413,9 +398,77 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
)
kwargs["customer"] = self.customer
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_fragment"] = StudentCardFormView.get_template_data(
self.customer
).render(self.request)
if self.object.can_refill():
kwargs["refilling_fragment"] = RefillingCreateView.get_template_data(
self.customer
).render(self.request)
return kwargs
class RefillingCreateView(FormView):
"""This is a fragment only view which integrates with counter_click.jinja"""
form_class = RefillForm
template_name = "counter/fragments/create_refill.jinja"
@classmethod
def get_template_data(
cls, customer: Customer, *, form_instance: form_class | None = None
) -> FormFragmentTemplateData[form_class]:
return FormFragmentTemplateData[cls.form_class](
form=form_instance if form_instance else cls.form_class(),
template=cls.template_name,
context={
"action": reverse_lazy(
"counter:refilling_create", kwargs={"customer_id": customer.pk}
),
},
)
def dispatch(self, request, *args, **kwargs):
self.customer: Customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
if not self.customer.can_buy:
raise Http404
if not is_logged_in_counter(request):
raise PermissionDenied
self.counter: Counter = get_object_or_404(
Counter, token=request.session["counter_token"]
)
if not self.counter.can_refill():
raise PermissionDenied
if self.customer_is_barman():
self.operator = self.customer.user
else:
self.operator = self.counter.get_random_barman()
return super().dispatch(request, *args, **kwargs)
def customer_is_barman(self) -> bool:
barmen = self.counter.barmen_list
return self.counter.type == "BAR" and self.customer.user in barmen
def form_valid(self, form):
res = super().form_valid(form)
form.clean()
form.instance.counter = self.counter
form.instance.operator = self.operator
form.instance.customer = self.customer
form.instance.save()
return res
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
data = self.get_template_data(self.customer, form_instance=context["form"])
context.update(data.context)
return context
def get_success_url(self, **kwargs):
return self.request.path

View File

@ -70,11 +70,11 @@ class StudentCardFormView(FormView):
@classmethod
def get_template_data(
cls, customer: Customer
) -> FormFragmentTemplateData[StudentCardForm]:
cls, customer: Customer, *, form_instance: form_class | None = None
) -> FormFragmentTemplateData[form_class]:
"""Get necessary data to pre-render the fragment"""
return FormFragmentTemplateData(
form=cls.form_class(),
return FormFragmentTemplateData[cls.form_class](
form=form_instance if form_instance else cls.form_class(),
template=cls.template_name,
context={
"action": reverse(
@ -105,7 +105,7 @@ class StudentCardFormView(FormView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
data = self.get_template_data(self.customer)
data = self.get_template_data(self.customer, form_instance=context["form"])
context.update(data.context)
return context