# # Copyright 2023 © AE UTBM # ae@utbm.fr / ae.info@utbm.fr # # This file is part of the website of the UTBM Student Association (AE UTBM), # https://ae.utbm.fr. # # You can find the source code of the website at https://github.com/ae-utbm/sith # # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE # OR WITHIN THE LOCAL FILE "LICENSE" # # import itertools import re from datetime import datetime, timedelta from datetime import timezone as tz from http import HTTPStatus from operator import itemgetter from typing import TYPE_CHECKING from urllib.parse import parse_qs from django import forms from django.conf import settings from django.core.exceptions import PermissionDenied from django.db import DataError, transaction from django.db.models import F from django.forms import CheckboxSelectMultiple from django.forms.models import modelform_factory from django.http import ( Http404, HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse, ) from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_POST from django.views.generic import DetailView, ListView, TemplateView from django.views.generic.base import View from django.views.generic.edit import ( CreateView, DeleteView, FormMixin, FormView, ProcessFormView, UpdateView, ) from accounting.models import CurrencyField from core.utils import get_semester_code, get_start_of_semester from core.views import CanEditMixin, CanViewMixin, TabedViewMixin from core.views.forms import LoginForm from counter.forms import ( CashSummaryFormBase, CounterEditForm, EticketForm, GetUserForm, NFCCardForm, ProductEditForm, RefillForm, StudentCardForm, ) from counter.models import ( CashRegisterSummary, CashRegisterSummaryItem, Counter, Customer, Eticket, Permanency, Product, ProductType, Refilling, Selling, StudentCard, ) from counter.utils import is_logged_in_counter if TYPE_CHECKING: from core.models import User class StudentCardDeleteView(DeleteView, CanEditMixin): """View used to delete a card from a user.""" model = StudentCard template_name = "core/delete_confirm.jinja" pk_url_kwarg = "card_id" def dispatch(self, request, *args, **kwargs): self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) return super().dispatch(request, *args, **kwargs) def get_success_url(self, **kwargs): return reverse_lazy( "core:user_prefs", kwargs={"user_id": self.customer.user.pk} ) class CounterMain( CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin ): """The public (barman) view.""" model = Counter template_name = "counter/counter_main.jinja" pk_url_kwarg = "counter_id" form_class = ( GetUserForm # Form to enter a client code and get the corresponding user id ) current_tab = "counter" def post(self, request, *args, **kwargs): self.object = self.get_object() if self.object.type == "BAR" and not ( "counter_token" in self.request.session and self.request.session["counter_token"] == self.object.token ): # Check the token to avoid the bar to be stolen return HttpResponseRedirect( reverse_lazy( "counter:details", args=self.args, kwargs={"counter_id": self.object.id}, ) + "?bad_location" ) return super().post(request, *args, **kwargs) def get_context_data(self, **kwargs): """We handle here the login form for the barman.""" if self.request.method == "POST": self.object = self.get_object() self.object.update_activity() kwargs = super().get_context_data(**kwargs) kwargs["login_form"] = LoginForm() kwargs["login_form"].fields["username"].widget.attrs["autofocus"] = True kwargs[ "login_form" ].cleaned_data = {} # add_error fails if there are no cleaned_data if "credentials" in self.request.GET: kwargs["login_form"].add_error(None, _("Bad credentials")) if "sellers" in self.request.GET: kwargs["login_form"].add_error(None, _("User is not barman")) kwargs["form"] = self.get_form() kwargs["form"].cleaned_data = {} # same as above if "bad_location" in self.request.GET: kwargs["form"].add_error( None, _("Bad location, someone is already logged in somewhere else") ) if self.object.type == "BAR": kwargs["barmen"] = self.object.barmen_list elif self.request.user.is_authenticated: kwargs["barmen"] = [self.request.user] if "last_basket" in self.request.session: kwargs["last_basket"] = self.request.session.pop("last_basket") kwargs["last_customer"] = self.request.session.pop("last_customer") kwargs["last_total"] = self.request.session.pop("last_total") kwargs["new_customer_amount"] = self.request.session.pop( "new_customer_amount" ) return kwargs def form_valid(self, form): """We handle here the redirection, passing the user id of the asked customer.""" self.kwargs["user_id"] = form.cleaned_data["user_id"] return super().form_valid(form) def get_success_url(self): return reverse_lazy("counter:click", args=self.args, kwargs=self.kwargs) @require_POST def counter_login(request: HttpRequest, counter_id: int) -> HttpResponseRedirect: """Log a user in a counter. A successful login will result in the beginning of a counter duty for the user. """ counter = get_object_or_404(Counter, pk=counter_id) form = LoginForm(request, data=request.POST) if not form.is_valid(): return redirect(counter.get_absolute_url() + "?credentials") user = form.get_user() if not counter.sellers.contains(user) or user in counter.barmen_list: return redirect(counter.get_absolute_url() + "?sellers") if len(counter.barmen_list) == 0: counter.gen_token() request.session["counter_token"] = counter.token counter.permanencies.create(user=user, start=timezone.now()) return redirect(counter) @require_POST def counter_logout(request: HttpRequest, counter_id: int) -> HttpResponseRedirect: """End the permanency of a user in this counter.""" Permanency.objects.filter(counter=counter_id, user=request.POST["user_id"]).update( end=F("activity") ) return redirect("counter:details", counter_id=counter_id) class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView): """Provide the last operations to allow barmen to delete them.""" model = Counter pk_url_kwarg = "counter_id" template_name = "counter/last_ops.jinja" current_tab = "last_ops" def dispatch(self, request, *args, **kwargs): """We have here again a very particular right handling.""" self.object = self.get_object() if is_logged_in_counter(request) and self.object.barmen_list: return super().dispatch(request, *args, **kwargs) return HttpResponseRedirect( reverse("counter:details", kwargs={"counter_id": self.object.id}) + "?bad_location" ) def get_context_data(self, **kwargs): """Add form to the context.""" kwargs = super().get_context_data(**kwargs) threshold = timezone.now() - timedelta( minutes=settings.SITH_LAST_OPERATIONS_LIMIT ) kwargs["last_refillings"] = ( self.object.refillings.filter(date__gte=threshold) .select_related("operator", "customer__user") .order_by("-id")[:20] ) kwargs["last_sellings"] = ( self.object.sellings.filter(date__gte=threshold) .select_related("seller", "customer__user") .order_by("-id")[:20] ) return kwargs class CounterActivityView(DetailView): """Show the bar activity.""" model = Counter pk_url_kwarg = "counter_id" template_name = "counter/activity.jinja" class StudentCardFormView(FormView): """Add a new student card.""" form_class = StudentCardForm template_name = "core/create.jinja" def dispatch(self, request, *args, **kwargs): self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) if not StudentCard.can_create(self.customer, request.user): raise PermissionDenied return super().dispatch(request, *args, **kwargs) def form_valid(self, form): data = form.clean() res = super(FormView, self).form_valid(form) StudentCard(customer=self.customer, uid=data["uid"]).save() return res def get_success_url(self, **kwargs): return reverse_lazy( "core:user_prefs", kwargs={"user_id": self.customer.user.pk} )