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.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema from ninja_extra.schemas import PaginatedResponseSchema
from core.api_permissions import CanAccessLookup, CanView, IsRoot from core.api_permissions import CanAccessLookup, CanView, IsLoggedInCounter, IsRoot
from counter.models import Counter, Product from counter.models import Counter, Customer, Product
from counter.schemas import ( from counter.schemas import (
CounterFilterSchema, CounterFilterSchema,
CounterSchema, CounterSchema,
CustomerBalance,
ProductSchema, ProductSchema,
SimplifiedCounterSchema, SimplifiedCounterSchema,
) )
@ -60,6 +61,13 @@ class CounterController(ControllerBase):
return filters.filter(Counter.objects.all()) 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") @api_controller("/product")
class ProductController(ControllerBase): class ProductController(ControllerBase):
@route.get( @route.get(

View File

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

View File

@ -6,7 +6,7 @@
{% endblock %} {% endblock %}
{% block additional_js %} {% 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 %} {% endblock %}
{% block info_boxes %} {% block info_boxes %}
@ -28,7 +28,7 @@
<h5>{% trans %}Customer{% endtrans %}</h5> <h5>{% trans %}Customer{% endtrans %}</h5>
{{ 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 %}<span x-text="customerBalance"></span> €</p>
{% if counter.type == 'BAR' %} {% if counter.type == 'BAR' %}
<h5>{% trans %}Student card{% endtrans %}</h3> <h5>{% trans %}Student card{% endtrans %}</h3>
@ -105,16 +105,12 @@
<input type="submit" value="{% trans %}Cancel{% endtrans %}"/> <input type="submit" value="{% trans %}Cancel{% endtrans %}"/>
</form> </form>
</div> </div>
{% if (counter.type == 'BAR' and barmens_can_refill) %} {% if refilling_fragment %}
<h5>{% trans %}Refilling{% endtrans %}</h5> <h5>{% trans %}Refilling{% endtrans %}</h5>
<div> <div
<form method="post" @htmx:after-request="updateBalance()"
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> >
{% csrf_token %} {{ refilling_fragment }}
{{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill">
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -155,9 +151,6 @@
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script> <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 = { const products = {
{%- for p in products -%} {%- for p in products -%}
{{ p.id }}: { {{ p.id }}: {
@ -176,5 +169,14 @@
}, },
{%- endfor %} {%- 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> </script>
{% endblock 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}, {"code": self.richard.customer.account_id, "counter_token": counter_token},
) )
counter_url = response.get("location") 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) assert ">Richard Batsbak</" in str(response.content)
self.client.post( self.client.post(
counter_url, refill_url,
{ {
"action": "refill",
"amount": "5", "amount": "5",
"payment_method": "CASH", "payment_method": "CASH",
"bank": "OTHER", "bank": "OTHER",
}, },
HTTP_REFERER=counter_url,
) )
self.client.post(counter_url, "action=code&code=BARB", content_type="text/xml") self.client.post(counter_url, "action=code&code=BARB", content_type="text/xml")
self.client.post( self.client.post(
@ -110,15 +117,15 @@ class TestCounter(TestCase):
) )
response = self.client.post( response = self.client.post(
counter_url, refill_url,
{ {
"action": "refill",
"amount": "5", "amount": "5",
"payment_method": "CASH", "payment_method": "CASH",
"bank": "OTHER", "bank": "OTHER",
}, },
HTTP_REFERER=counter_url,
) )
assert response.status_code == 200 assert response.status_code == 302
self.client.post( self.client.post(
reverse("counter:login", kwargs={"counter_id": self.foyer.id}), 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}, {"code": self.richard.customer.account_id, "counter_token": counter_token},
) )
counter_url = response.get("location") counter_url = response.get("location")
refill_url = reverse(
"counter:refilling_create",
kwargs={
"customer_id": self.richard.customer.pk,
},
)
response = self.client.post( response = self.client.post(
counter_url, refill_url,
{ {
"action": "refill",
"amount": "5", "amount": "5",
"payment_method": "CASH", "payment_method": "CASH",
"bank": "OTHER", "bank": "OTHER",
}, },
HTTP_REFERER=counter_url,
) )
assert response.status_code == 200 assert response.status_code == 302
def test_annotate_has_barman_queryset(self): def test_annotate_has_barman_queryset(self):
"""Test if the custom queryset method `annotate_has_barman` works as intended.""" """Test if the custom queryset method `annotate_has_barman` works as intended."""

View File

@ -39,7 +39,7 @@ from counter.views.cash import (
CashSummaryListView, CashSummaryListView,
CounterCashSummaryView, CounterCashSummaryView,
) )
from counter.views.click import CounterClick from counter.views.click import CounterClick, RefillingCreateView
from counter.views.eticket import ( from counter.views.eticket import (
EticketCreateView, EticketCreateView,
EticketEditView, EticketEditView,
@ -60,6 +60,11 @@ from counter.views.student_card import (
urlpatterns = [ urlpatterns = [
path("<int:counter_id>/", CounterMain.as_view(), name="details"), path("<int:counter_id>/", CounterMain.as_view(), name="details"),
path("<int:counter_id>/click/<int:user_id>/", CounterClick.as_view(), name="click"), 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( path(
"<int:counter_id>/last_ops/", "<int:counter_id>/last_ops/",
CounterLastOperationsView.as_view(), 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.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ 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 core.views import CanViewMixin
from counter.forms import RefillForm from counter.forms import RefillForm
from counter.models import Counter, Customer, Product, Selling 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.mixins import CounterTabsMixin
from counter.views.student_card import StudentCardFormView from counter.views.student_card import StudentCardFormView
@ -100,7 +102,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
self.refill_form = None
ret = super().get(request, *args, **kwargs) ret = super().get(request, *args, **kwargs)
if (self.object.type != "BAR" and not request.user.is_authenticated) or ( if (self.object.type != "BAR" and not request.user.is_authenticated) or (
self.object.type == "BAR" and len(self.object.barmen_list) == 0 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): def post(self, request, *args, **kwargs):
"""Handle the many possibilities of the post request.""" """Handle the many possibilities of the post request."""
self.object = self.get_object() self.object = self.get_object()
self.refill_form = None
if (self.object.type != "BAR" and not request.user.is_authenticated) or ( if (self.object.type != "BAR" and not request.user.is_authenticated) or (
self.object.type == "BAR" and len(self.object.barmen_list) < 1 self.object.type == "BAR" and len(self.object.barmen_list) < 1
): # Check that at least one barman is logged in ): # Check that at least one barman is logged in
@ -148,8 +148,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
self.add_product(request) self.add_product(request)
elif action == "del_product": elif action == "del_product":
self.del_product(request) self.del_product(request)
elif action == "refill":
self.refill(request)
elif action == "code": elif action == "code":
return self.parse_code(request) return self.parse_code(request)
elif action == "cancel": elif action == "cancel":
@ -383,19 +381,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
reverse_lazy("counter:details", args=self.args, kwargs=kwargs) 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): def get_context_data(self, **kwargs):
"""Add customer to the context.""" """Add customer to the context."""
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
@ -413,9 +398,77 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
) )
kwargs["customer"] = self.customer kwargs["customer"] = self.customer
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["barmens_can_refill"] = self.object.can_refill()
kwargs["student_card_fragment"] = StudentCardFormView.get_template_data( kwargs["student_card_fragment"] = StudentCardFormView.get_template_data(
self.customer self.customer
).render(self.request) ).render(self.request)
if self.object.can_refill():
kwargs["refilling_fragment"] = RefillingCreateView.get_template_data(
self.customer
).render(self.request)
return kwargs 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 @classmethod
def get_template_data( def get_template_data(
cls, customer: Customer cls, customer: Customer, *, form_instance: form_class | None = None
) -> FormFragmentTemplateData[StudentCardForm]: ) -> FormFragmentTemplateData[form_class]:
"""Get necessary data to pre-render the fragment""" """Get necessary data to pre-render the fragment"""
return FormFragmentTemplateData( return FormFragmentTemplateData[cls.form_class](
form=cls.form_class(), form=form_instance if form_instance else cls.form_class(),
template=cls.template_name, template=cls.template_name,
context={ context={
"action": reverse( "action": reverse(
@ -105,7 +105,7 @@ class StudentCardFormView(FormView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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) context.update(data.context)
return context return context