From 8d4d8a3abcd3a41ceb59ecc4d4f3170f62b9f843 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 17:07:08 +0100 Subject: [PATCH 01/53] create views package --- counter/views/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 counter/views/__init__.py diff --git a/counter/views/__init__.py b/counter/views/__init__.py new file mode 100644 index 00000000..e69de29b From 68ad9650afbfcaf0e8771e2018be366bc8b5978e Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 17:56:44 +0100 Subject: [PATCH 02/53] extract main views --- counter/{views.py => views/main.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/{views.py => views/main.py} (100%) diff --git a/counter/views.py b/counter/views/main.py similarity index 100% rename from counter/views.py rename to counter/views/main.py From 9d17524f4559b816194a6c8f3aab5b6d797331cb Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:00:48 +0100 Subject: [PATCH 03/53] extract main views --- counter/views/main.py | 1265 ----------------------------------------- 1 file changed, 1265 deletions(-) diff --git a/counter/views/main.py b/counter/views/main.py index 9483d335..0f58eeda 100644 --- a/counter/views/main.py +++ b/counter/views/main.py @@ -84,28 +84,6 @@ if TYPE_CHECKING: from core.models import User -class CounterAdminMixin(View): - """Protect counter admin section.""" - - edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] - edit_club = [] - - def _test_group(self, user): - return any(user.is_in_group(pk=grp_id) for grp_id in self.edit_group) - - def _test_club(self, user): - return any(c.can_be_edited_by(user) for c in self.edit_club) - - def dispatch(self, request, *args, **kwargs): - if not ( - request.user.is_root - or self._test_group(request.user) - or self._test_club(request.user) - ): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - class StudentCardDeleteView(DeleteView, CanEditMixin): """View used to delete a card from a user.""" @@ -123,42 +101,6 @@ class StudentCardDeleteView(DeleteView, CanEditMixin): ) -class CounterTabsMixin(TabedViewMixin): - def get_tabs_title(self): - return self.object - - def get_list_of_tabs(self): - tab_list = [ - { - "url": reverse_lazy( - "counter:details", kwargs={"counter_id": self.object.id} - ), - "slug": "counter", - "name": _("Counter"), - } - ] - if self.object.type == "BAR": - tab_list.append( - { - "url": reverse_lazy( - "counter:cash_summary", kwargs={"counter_id": self.object.id} - ), - "slug": "cash_summary", - "name": _("Cash summary"), - } - ) - tab_list.append( - { - "url": reverse_lazy( - "counter:last_ops", kwargs={"counter_id": self.object.id} - ), - "slug": "last_ops", - "name": _("Last operations"), - } - ) - return tab_list - - class CounterMain( CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin ): @@ -231,411 +173,6 @@ class CounterMain( return reverse_lazy("counter:click", args=self.args, kwargs=self.kwargs) -class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): - """The click view - This is a detail view not to have to worry about loading the counter - Everything is made by hand in the post method. - """ - - model = Counter - queryset = Counter.objects.annotate_is_open() - template_name = "counter/counter_click.jinja" - pk_url_kwarg = "counter_id" - current_tab = "counter" - - def render_to_response(self, *args, **kwargs): - if self.is_ajax(self.request): - response = {"errors": []} - status = HTTPStatus.OK - - if self.request.session["too_young"]: - response["errors"].append(_("Too young for that product")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_allowed"]: - response["errors"].append(_("Not allowed for that product")) - status = HTTPStatus.FORBIDDEN - if self.request.session["no_age"]: - response["errors"].append(_("No date of birth provided")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_enough"]: - response["errors"].append(_("Not enough money")) - status = HTTPStatus.PAYMENT_REQUIRED - - if len(response["errors"]) > 1: - status = HTTPStatus.BAD_REQUEST - - response["basket"] = self.request.session["basket"] - - return JsonResponse(response, status=status) - - else: # Standard HTML page - return super().render_to_response(*args, **kwargs) - - def dispatch(self, request, *args, **kwargs): - self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) - obj: Counter = self.get_object() - if not self.customer.can_buy: - raise Http404 - if obj.type != "BAR" and not request.user.is_authenticated: - raise PermissionDenied - if obj.type == "BAR" and ( - "counter_token" not in request.session - or request.session["counter_token"] != obj.token - or len(obj.barmen_list) == 0 - ): - return redirect(obj) - return super().dispatch(request, *args, **kwargs) - - def get(self, request, *args, **kwargs): - """Simple get view.""" - if "basket" not in request.session: # Init the basket session entry - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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 - ): # Check that at least one barman is logged in - ret = self.cancel(request) # Otherwise, go to main view - return ret - - 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 - return self.cancel(request) - if self.object.type == "BAR" and not ( - "counter_token" in self.request.session - and self.request.session["counter_token"] == self.object.token - ): # Also 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" - ) - if "basket" not in request.session: - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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(): - self.operator = self.customer.user - else: - self.operator = self.object.get_random_barman() - action = self.request.POST.get("action", None) - if action is None: - 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": - self.refill(request) - elif action == "code": - return self.parse_code(request) - elif action == "cancel": - return self.cancel(request) - elif action == "finish": - return self.finish(request) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def customer_is_barman(self) -> bool: - barmen = self.object.barmen_list - return self.object.type == "BAR" and self.customer.user in barmen - - def get_product(self, pid): - return Product.objects.filter(pk=int(pid)).first() - - def get_price(self, pid): - p = self.get_product(pid) - if self.customer_is_barman(): - price = p.special_selling_price - else: - price = p.selling_price - return price - - def sum_basket(self, request): - total = 0 - for infos in request.session["basket"].values(): - total += infos["price"] * infos["qty"] - return total / 100 - - def get_total_quantity_for_pid(self, request, pid): - pid = str(pid) - if pid not in request.session["basket"]: - return 0 - return ( - request.session["basket"][pid]["qty"] - + request.session["basket"][pid]["bonus_qty"] - ) - - def compute_record_product(self, request, product=None): - recorded = 0 - basket = request.session["basket"] - - if product: - if product.is_record_product: - recorded -= 1 - elif product.is_unrecord_product: - recorded += 1 - - for p in basket: - bproduct = self.get_product(str(p)) - if bproduct.is_record_product: - recorded -= basket[p]["qty"] - elif bproduct.is_unrecord_product: - recorded += basket[p]["qty"] - return recorded - - def is_record_product_ok(self, request, product): - return self.customer.can_record_more( - self.compute_record_product(request, product) - ) - - @staticmethod - def is_ajax(request): - # when using the fetch API, the django request.POST dict is empty - # this is but a wretched contrivance which strive to replace - # the deprecated django is_ajax() method - # and which must be replaced as soon as possible - # by a proper separation between the api endpoints of the counter - return len(request.POST) == 0 and len(request.body) != 0 - - def add_product(self, request, q=1, p=None): - """Add a product to the basket - q is the quantity passed as integer - p is the product id, passed as an integer. - """ - pid = p or parse_qs(request.body.decode())["product_id"][0] - pid = str(pid) - price = self.get_price(pid) - total = self.sum_basket(request) - product: Product = self.get_product(pid) - user: User = self.customer.user - buying_groups = list(product.buying_groups.values_list("pk", flat=True)) - can_buy = len(buying_groups) == 0 or any( - user.is_in_group(pk=group_id) for group_id in buying_groups - ) - if not can_buy: - request.session["not_allowed"] = True - return False - bq = 0 # Bonus quantity, for trays - if ( - product.tray - ): # Handle the tray to adjust the quantity q to add and the bonus quantity bq - total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6 - bq = int((total_qty_mod_6 + q) / 6) # Integer division - q -= bq - if self.customer.amount < ( - total + round(q * float(price), 2) - ): # Check for enough money - request.session["not_enough"] = True - return False - if product.is_unrecord_product and not self.is_record_product_ok( - request, product - ): - request.session["not_allowed"] = True - return False - if product.limit_age >= 18 and not user.date_of_birth: - request.session["no_age"] = True - return False - if product.limit_age >= 18 and user.is_banned_alcohol: - request.session["not_allowed"] = True - return False - if user.is_banned_counter: - request.session["not_allowed"] = True - return False - if ( - user.date_of_birth and self.customer.user.get_age() < product.limit_age - ): # Check if affordable - request.session["too_young"] = True - return False - if pid in request.session["basket"]: # Add if already in basket - request.session["basket"][pid]["qty"] += q - request.session["basket"][pid]["bonus_qty"] += bq - else: # or create if not - request.session["basket"][pid] = { - "qty": q, - "price": int(price * 100), - "bonus_qty": bq, - } - 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] - product = self.get_product(pid) - if pid in request.session["basket"]: - if ( - product.tray - and (self.get_total_quantity_for_pid(request, pid) % 6 == 0) - and request.session["basket"][pid]["bonus_qty"] - ): - request.session["basket"][pid]["bonus_qty"] -= 1 - else: - request.session["basket"][pid]["qty"] -= 1 - if request.session["basket"][pid]["qty"] <= 0: - del request.session["basket"][pid] - request.session.modified = True - - def parse_code(self, request): - """Parse the string entered by the barman. - - This can be of two forms : - - ``, where the string is the code of the product - - `X`, where the integer is the quantity and str the code. - """ - string = parse_qs(request.body.decode()).get("code", [""])[0].upper() - if string == "FIN": - return self.finish(request) - elif string == "ANN": - return self.cancel(request) - regex = re.compile(r"^((?P[0-9]+)X)?(?P[A-Z0-9]+)$") - m = regex.match(string) - if m is not None: - nb = m.group("nb") - code = m.group("code") - nb = int(nb) if nb is not None else 1 - p = self.object.products.filter(code=code).first() - if p is not None: - self.add_product(request, nb, p.id) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def finish(self, request): - """Finish the click session, and validate the basket.""" - with transaction.atomic(): - request.session["last_basket"] = [] - if self.sum_basket(request) > self.customer.amount: - raise DataError(_("You have not enough money to buy all the basket")) - - for pid, infos in request.session["basket"].items(): - # This duplicates code for DB optimization (prevent to load many times the same object) - p = Product.objects.filter(pk=pid).first() - if self.customer_is_barman(): - uprice = p.special_selling_price - else: - uprice = p.selling_price - request.session["last_basket"].append( - "%d x %s" % (infos["qty"] + infos["bonus_qty"], p.name) - ) - s = Selling( - label=p.name, - product=p, - club=p.club, - counter=self.object, - unit_price=uprice, - quantity=infos["qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - if infos["bonus_qty"]: - s = Selling( - label=p.name + " (Plateau)", - product=p, - club=p.club, - counter=self.object, - unit_price=0, - quantity=infos["bonus_qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - self.customer.recorded_products -= self.compute_record_product(request) - self.customer.save() - request.session["last_customer"] = self.customer.user.get_display_name() - request.session["last_total"] = "%0.2f" % self.sum_basket(request) - request.session["new_customer_amount"] = str(self.customer.amount) - del request.session["basket"] - request.session.modified = True - kwargs = {"counter_id": self.object.id} - return HttpResponseRedirect( - reverse_lazy("counter:details", args=self.args, kwargs=kwargs) - ) - - def cancel(self, request): - """Cancel the click session.""" - kwargs = {"counter_id": self.object.id} - request.session.pop("basket", None) - return HttpResponseRedirect( - 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) - products = self.object.products.select_related("product_type") - if self.customer_is_barman(): - products = products.annotate(price=F("special_selling_price")) - else: - products = products.annotate(price=F("selling_price")) - kwargs["products"] = products - kwargs["categories"] = {} - for product in kwargs["products"]: - if product.product_type: - kwargs["categories"].setdefault(product.product_type, []).append( - 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 - - @require_POST def counter_login(request: HttpRequest, counter_id: int) -> HttpResponseRedirect: """Log a user in a counter. @@ -666,455 +203,6 @@ def counter_logout(request: HttpRequest, counter_id: int) -> HttpResponseRedirec return redirect("counter:details", counter_id=counter_id) -# Counter admin views - - -class CounterAdminTabsMixin(TabedViewMixin): - tabs_title = _("Counter administration") - list_of_tabs = [ - { - "url": reverse_lazy("counter:admin_list"), - "slug": "counters", - "name": _("Counters"), - }, - { - "url": reverse_lazy("counter:product_list"), - "slug": "products", - "name": _("Products"), - }, - { - "url": reverse_lazy("counter:product_list_archived"), - "slug": "archive", - "name": _("Archived products"), - }, - { - "url": reverse_lazy("counter:producttype_list"), - "slug": "product_types", - "name": _("Product types"), - }, - { - "url": reverse_lazy("counter:cash_summary_list"), - "slug": "cash_summary", - "name": _("Cash register summaries"), - }, - { - "url": reverse_lazy("counter:invoices_call"), - "slug": "invoices_call", - "name": _("Invoices call"), - }, - { - "url": reverse_lazy("counter:eticket_list"), - "slug": "etickets", - "name": _("Etickets"), - }, - ] - - -class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): - """A list view for the admins.""" - - model = Counter - template_name = "counter/counter_list.jinja" - current_tab = "counters" - - -class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's manager).""" - - model = Counter - form_class = CounterEditForm - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - def dispatch(self, request, *args, **kwargs): - obj = self.get_object() - self.edit_club.append(obj.club) - return super().dispatch(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:admin", kwargs={"counter_id": self.object.id}) - - -class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's admin).""" - - model = Counter - form_class = modelform_factory(Counter, fields=["name", "club", "type"]) - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - -class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create a counter (for the admins).""" - - model = Counter - form_class = modelform_factory( - Counter, - fields=["name", "club", "type", "products"], - widgets={"products": CheckboxSelectMultiple}, - ) - template_name = "core/create.jinja" - current_tab = "counters" - - -class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): - """Delete a counter (for the admins).""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "core/delete_confirm.jinja" - success_url = reverse_lazy("counter:admin_list") - current_tab = "counters" - - -# Product management - - -class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = ProductType - template_name = "counter/producttype_list.jinja" - current_tab = "product_types" - - -class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = ProductType - fields = ["name", "description", "comment", "icon", "priority"] - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = ProductType - template_name = "core/edit.jinja" - fields = ["name", "description", "comment", "icon", "priority"] - pk_url_kwarg = "type_id" - current_tab = "products" - - -class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - model = Product - queryset = Product.objects.values("id", "name", "code", "product_type__name") - template_name = "counter/product_list.jinja" - ordering = [ - F("product_type__priority").desc(nulls_last=True), - "product_type", - "name", - ] - - def get_context_data(self, **kwargs): - res = super().get_context_data(**kwargs) - res["object_list"] = itertools.groupby( - res["object_list"], key=itemgetter("product_type__name") - ) - return res - - -class ArchivedProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "archive" - - def get_queryset(self): - return super().get_queryset().filter(archived=True) - - -class ActiveProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "products" - - def get_queryset(self): - return super().get_queryset().filter(archived=False) - - -class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = Product - form_class = ProductEditForm - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = Product - form_class = ProductEditForm - pk_url_kwarg = "product_id" - template_name = "core/edit.jinja" - current_tab = "products" - - -class RefillingDeleteView(DeleteView): - """Delete a refilling (for the admins).""" - - model = Refilling - pk_url_kwarg = "refilling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class SellingDeleteView(DeleteView): - """Delete a selling (for the admins).""" - - model = Selling - pk_url_kwarg = "selling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -# Cash register summaries - - -class CashRegisterSummaryForm(forms.Form): - """Provide the cash summary form.""" - - ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0) - twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0) - fifty_cents = forms.IntegerField(label=_("50 cents"), required=False, min_value=0) - one_euro = forms.IntegerField(label=_("1 euro"), required=False, min_value=0) - two_euros = forms.IntegerField(label=_("2 euros"), required=False, min_value=0) - five_euros = forms.IntegerField(label=_("5 euros"), required=False, min_value=0) - ten_euros = forms.IntegerField(label=_("10 euros"), required=False, min_value=0) - twenty_euros = forms.IntegerField(label=_("20 euros"), required=False, min_value=0) - fifty_euros = forms.IntegerField(label=_("50 euros"), required=False, min_value=0) - hundred_euros = forms.IntegerField( - label=_("100 euros"), required=False, min_value=0 - ) - check_1_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_1_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_2_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_2_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_3_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_3_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_4_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_4_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_5_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_5_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - comment = forms.CharField(label=_("Comment"), required=False) - emptied = forms.BooleanField(label=_("Emptied"), required=False) - - def __init__(self, *args, **kwargs): - instance = kwargs.pop("instance", None) - super().__init__(*args, **kwargs) - if instance: - self.fields["ten_cents"].initial = ( - instance.ten_cents.quantity if instance.ten_cents else 0 - ) - self.fields["twenty_cents"].initial = ( - instance.twenty_cents.quantity if instance.twenty_cents else 0 - ) - self.fields["fifty_cents"].initial = ( - instance.fifty_cents.quantity if instance.fifty_cents else 0 - ) - self.fields["one_euro"].initial = ( - instance.one_euro.quantity if instance.one_euro else 0 - ) - self.fields["two_euros"].initial = ( - instance.two_euros.quantity if instance.two_euros else 0 - ) - self.fields["five_euros"].initial = ( - instance.five_euros.quantity if instance.five_euros else 0 - ) - self.fields["ten_euros"].initial = ( - instance.ten_euros.quantity if instance.ten_euros else 0 - ) - self.fields["twenty_euros"].initial = ( - instance.twenty_euros.quantity if instance.twenty_euros else 0 - ) - self.fields["fifty_euros"].initial = ( - instance.fifty_euros.quantity if instance.fifty_euros else 0 - ) - self.fields["hundred_euros"].initial = ( - instance.hundred_euros.quantity if instance.hundred_euros else 0 - ) - self.fields["check_1_quantity"].initial = ( - instance.check_1.quantity if instance.check_1 else 0 - ) - self.fields["check_2_quantity"].initial = ( - instance.check_2.quantity if instance.check_2 else 0 - ) - self.fields["check_3_quantity"].initial = ( - instance.check_3.quantity if instance.check_3 else 0 - ) - self.fields["check_4_quantity"].initial = ( - instance.check_4.quantity if instance.check_4 else 0 - ) - self.fields["check_5_quantity"].initial = ( - instance.check_5.quantity if instance.check_5 else 0 - ) - self.fields["check_1_value"].initial = ( - instance.check_1.value if instance.check_1 else 0 - ) - self.fields["check_2_value"].initial = ( - instance.check_2.value if instance.check_2 else 0 - ) - self.fields["check_3_value"].initial = ( - instance.check_3.value if instance.check_3 else 0 - ) - self.fields["check_4_value"].initial = ( - instance.check_4.value if instance.check_4 else 0 - ) - self.fields["check_5_value"].initial = ( - instance.check_5.value if instance.check_5 else 0 - ) - self.fields["comment"].initial = instance.comment - self.fields["emptied"].initial = instance.emptied - self.instance = instance - else: - self.instance = None - - def save(self, counter=None): - cd = self.cleaned_data - summary = self.instance or CashRegisterSummary( - counter=counter, user=counter.get_random_barman() - ) - summary.comment = cd["comment"] - summary.emptied = cd["emptied"] - summary.save() - summary.items.all().delete() - # Cash - if cd["ten_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.1, quantity=cd["ten_cents"] - ).save() - if cd["twenty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.2, quantity=cd["twenty_cents"] - ).save() - if cd["fifty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.5, quantity=cd["fifty_cents"] - ).save() - if cd["one_euro"]: - CashRegisterSummaryItem( - cash_summary=summary, value=1, quantity=cd["one_euro"] - ).save() - if cd["two_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=2, quantity=cd["two_euros"] - ).save() - if cd["five_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=5, quantity=cd["five_euros"] - ).save() - if cd["ten_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=10, quantity=cd["ten_euros"] - ).save() - if cd["twenty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=20, quantity=cd["twenty_euros"] - ).save() - if cd["fifty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=50, quantity=cd["fifty_euros"] - ).save() - if cd["hundred_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=100, quantity=cd["hundred_euros"] - ).save() - # Checks - if cd["check_1_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_1_value"], - quantity=cd["check_1_quantity"], - is_check=True, - ).save() - if cd["check_2_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_2_value"], - quantity=cd["check_2_quantity"], - is_check=True, - ).save() - if cd["check_3_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_3_value"], - quantity=cd["check_3_quantity"], - is_check=True, - ).save() - if cd["check_4_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_4_value"], - quantity=cd["check_4_quantity"], - is_check=True, - ).save() - if cd["check_5_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_5_value"], - quantity=cd["check_5_quantity"], - is_check=True, - ).save() - if summary.items.count() < 1: - summary.delete() - - class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView): """Provide the last operations to allow barmen to delete them.""" @@ -1152,47 +240,6 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView): return kwargs -class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): - """Provide the cash summary form.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/cash_register_summary.jinja" - current_tab = "cash_summary" - - 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(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm() - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm(request.POST) - if self.form.is_valid(): - self.form.save(self.object) - return HttpResponseRedirect(self.get_success_url()) - return super().get(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id}) - - def get_context_data(self, **kwargs): - """Add form to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["form"] = self.form - return kwargs - - class CounterActivityView(DetailView): """Show the bar activity.""" @@ -1201,318 +248,6 @@ class CounterActivityView(DetailView): template_name = "counter/activity.jinja" -class CounterStatView(DetailView, CounterAdminMixin): - """Show the bar stats.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/stats.jinja" - - def get_context_data(self, **kwargs): - """Add stats to the context.""" - counter: Counter = self.object - semester_start = get_start_of_semester() - office_hours = counter.get_top_barmen() - kwargs = super().get_context_data(**kwargs) - kwargs.update( - { - "counter": counter, - "current_semester": get_semester_code(), - "total_sellings": counter.get_total_sales(since=semester_start), - "top_customers": counter.get_top_customers(since=semester_start)[:100], - "top_barman": office_hours[:100], - "top_barman_semester": ( - office_hours.filter(start__gt=semester_start)[:100] - ), - } - ) - return kwargs - - def dispatch(self, request, *args, **kwargs): - try: - return super().dispatch(request, *args, **kwargs) - except PermissionDenied: - if ( - request.user.is_root - or request.user.is_board_member - or self.get_object().is_owned_by(request.user) - ): - return super(CanEditMixin, self).dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_register_summary.jinja" - context_object_name = "cashsummary" - pk_url_kwarg = "cashsummary_id" - form_class = CashRegisterSummaryForm - current_tab = "cash_summary" - - def get_success_url(self): - return reverse("counter:cash_summary_list") - - -class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """Display a list of cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_summary_list.jinja" - context_object_name = "cashsummary_list" - current_tab = "cash_summary" - queryset = CashRegisterSummary.objects.all().order_by("-date") - paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - form = CashSummaryFormBase(self.request.GET) - kwargs["form"] = form - kwargs["summaries_sums"] = {} - kwargs["refilling_sums"] = {} - for c in Counter.objects.filter(type="BAR").all(): - refillings = Refilling.objects.filter(counter=c) - cashredistersummaries = CashRegisterSummary.objects.filter(counter=c) - if form.is_valid() and form.cleaned_data["begin_date"]: - refillings = refillings.filter( - date__gte=form.cleaned_data["begin_date"] - ) - cashredistersummaries = cashredistersummaries.filter( - date__gte=form.cleaned_data["begin_date"] - ) - else: - last_summary = ( - CashRegisterSummary.objects.filter(counter=c, emptied=True) - .order_by("-date") - .first() - ) - if last_summary: - refillings = refillings.filter(date__gt=last_summary.date) - cashredistersummaries = cashredistersummaries.filter( - date__gt=last_summary.date - ) - else: - refillings = refillings.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) # My birth date should be old enough - cashredistersummaries = cashredistersummaries.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) - if form.is_valid() and form.cleaned_data["end_date"]: - refillings = refillings.filter(date__lte=form.cleaned_data["end_date"]) - cashredistersummaries = cashredistersummaries.filter( - date__lte=form.cleaned_data["end_date"] - ) - kwargs["summaries_sums"][c.name] = sum( - [s.get_total() for s in cashredistersummaries.all()] - ) - kwargs["refilling_sums"][c.name] = sum([s.amount for s in refillings.all()]) - return kwargs - - -class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): - template_name = "counter/invoices_call.jinja" - current_tab = "invoices_call" - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC") - if "month" in self.request.GET: - start_date = datetime.strptime(self.request.GET["month"], "%Y-%m") - else: - start_date = datetime( - year=timezone.now().year, - month=(timezone.now().month + 10) % 12 + 1, - day=1, - ) - start_date = start_date.replace(tzinfo=tz.utc) - end_date = (start_date + timedelta(days=32)).replace( - day=1, hour=0, minute=0, microsecond=0 - ) - from django.db.models import Case, F, Sum, When - - kwargs["sum_cb"] = sum( - [ - r.amount - for r in Refilling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["sum_cb"] += sum( - [ - s.quantity * s.unit_price - for s in Selling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["start_date"] = start_date - kwargs["sums"] = ( - Selling.objects.values("club__name") - .annotate( - selling_sum=Sum( - Case( - When( - date__gte=start_date, - date__lt=end_date, - then=F("unit_price") * F("quantity"), - ), - output_field=CurrencyField(), - ) - ) - ) - .exclude(selling_sum=None) - .order_by("-selling_sum") - ) - return kwargs - - -class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = Eticket - template_name = "counter/eticket_list.jinja" - ordering = ["id"] - current_tab = "etickets" - - -class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create an eticket.""" - - model = Eticket - template_name = "core/create.jinja" - form_class = EticketForm - current_tab = "etickets" - - -class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit an eticket.""" - - model = Eticket - template_name = "core/edit.jinja" - form_class = EticketForm - pk_url_kwarg = "eticket_id" - current_tab = "etickets" - - -class EticketPDFView(CanViewMixin, DetailView): - """Display the PDF of an eticket.""" - - model = Selling - pk_url_kwarg = "selling_id" - - def get(self, request, *args, **kwargs): - from reportlab.graphics import renderPDF - from reportlab.graphics.barcode.qr import QrCodeWidget - from reportlab.graphics.shapes import Drawing - from reportlab.lib.units import cm - from reportlab.lib.utils import ImageReader - from reportlab.pdfgen import canvas - - if not ( - hasattr(self.object, "product") and hasattr(self.object.product, "eticket") - ): - raise Http404 - - eticket = self.object.product.eticket - user = self.object.customer.user - code = "%s %s %s %s" % ( - self.object.customer.user.id, - self.object.product.id, - self.object.id, - self.object.quantity, - ) - code += " " + eticket.get_hash(code)[:8].upper() - response = HttpResponse(content_type="application/pdf") - response["Content-Disposition"] = 'filename="eticket.pdf"' - p = canvas.Canvas(response) - p.setTitle("Eticket") - im = ImageReader("core/static/core/img/eticket.jpg") - width, height = im.getSize() - size = max(width, height) - width = 8 * cm * width / size - height = 8 * cm * height / size - p.drawImage(im, 10 * cm, 25 * cm, width, height) - if eticket.banner: - im = ImageReader(eticket.banner) - width, height = im.getSize() - size = max(width, height) - width = 6 * cm * width / size - height = 6 * cm * height / size - p.drawImage(im, 1 * cm, 25 * cm, width, height) - if user.profile_pict: - im = ImageReader(user.profile_pict.file) - width, height = im.getSize() - size = max(width, height) - width = 150 * width / size - height = 150 * height / size - p.drawImage(im, 10.5 * cm - width / 2, 16 * cm, width, height) - if eticket.event_title: - p.setFont("Helvetica-Bold", 20) - p.drawCentredString(10.5 * cm, 23.6 * cm, eticket.event_title) - if eticket.event_date: - p.setFont("Helvetica-Bold", 16) - p.drawCentredString( - 10.5 * cm, 22.6 * cm, eticket.event_date.strftime("%d %b %Y") - ) # FIXME with a locale - p.setFont("Helvetica-Bold", 14) - p.drawCentredString( - 10.5 * cm, - 15 * cm, - "%s : %d %s" - % (user.get_display_name(), self.object.quantity, str(_("people(s)"))), - ) - p.setFont("Courier-Bold", 14) - qrcode = QrCodeWidget(code) - bounds = qrcode.getBounds() - width = bounds[2] - bounds[0] - height = bounds[3] - bounds[1] - d = Drawing(260, 260, transform=[260.0 / width, 0, 0, 260.0 / height, 0, 0]) - d.add(qrcode) - renderPDF.draw(d, p, 10.5 * cm - 130, 6.1 * cm) - p.drawCentredString(10.5 * cm, 6 * cm, code) - - partners = ImageReader("core/static/core/img/partners.png") - width, height = partners.getSize() - size = max(width, height) - width = width * 2 / 3 - height = height * 2 / 3 - p.drawImage(partners, 0 * cm, 0 * cm, width, height) - - p.showPage() - p.save() - return response - - -class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """List of refillings on a counter.""" - - model = Refilling - template_name = "counter/refilling_list.jinja" - current_tab = "counters" - paginate_by = 30 - - def dispatch(self, request, *args, **kwargs): - self.counter = get_object_or_404(Counter, pk=kwargs["counter_id"]) - self.queryset = Refilling.objects.filter(counter__id=self.counter.id) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["counter"] = self.counter - return kwargs - - class StudentCardFormView(FormView): """Add a new student card.""" From de415e7e75a42ea346506c8b598ba0944a1898f9 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:40:38 +0100 Subject: [PATCH 04/53] split click views --- counter/{views.py => views/click.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/{views.py => views/click.py} (100%) diff --git a/counter/views.py b/counter/views/click.py similarity index 100% rename from counter/views.py rename to counter/views/click.py From 4133e0ccdddcad2bca8b2ae41e15d1d6b8d89957 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:41:12 +0100 Subject: [PATCH 05/53] extract click views --- counter/views/click.py | 1110 +--------------------------------------- 1 file changed, 6 insertions(+), 1104 deletions(-) diff --git a/counter/views/click.py b/counter/views/click.py index 9483d335..fe948c91 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -12,225 +12,28 @@ # 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.http import Http404, 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.urls import reverse_lazy 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 django.views.generic import DetailView -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 +from core.views import CanViewMixin +from counter.forms import NFCCardForm, RefillForm +from counter.models import Counter, Customer, Product, Selling, StudentCard if TYPE_CHECKING: from core.models import User -class CounterAdminMixin(View): - """Protect counter admin section.""" - - edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] - edit_club = [] - - def _test_group(self, user): - return any(user.is_in_group(pk=grp_id) for grp_id in self.edit_group) - - def _test_club(self, user): - return any(c.can_be_edited_by(user) for c in self.edit_club) - - def dispatch(self, request, *args, **kwargs): - if not ( - request.user.is_root - or self._test_group(request.user) - or self._test_club(request.user) - ): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - -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 CounterTabsMixin(TabedViewMixin): - def get_tabs_title(self): - return self.object - - def get_list_of_tabs(self): - tab_list = [ - { - "url": reverse_lazy( - "counter:details", kwargs={"counter_id": self.object.id} - ), - "slug": "counter", - "name": _("Counter"), - } - ] - if self.object.type == "BAR": - tab_list.append( - { - "url": reverse_lazy( - "counter:cash_summary", kwargs={"counter_id": self.object.id} - ), - "slug": "cash_summary", - "name": _("Cash summary"), - } - ) - tab_list.append( - { - "url": reverse_lazy( - "counter:last_ops", kwargs={"counter_id": self.object.id} - ), - "slug": "last_ops", - "name": _("Last operations"), - } - ) - return tab_list - - -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) - - class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): """The click view This is a detail view not to have to worry about loading the counter @@ -634,904 +437,3 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE kwargs["barmens_can_refill"] = self.object.can_refill() return 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) - - -# Counter admin views - - -class CounterAdminTabsMixin(TabedViewMixin): - tabs_title = _("Counter administration") - list_of_tabs = [ - { - "url": reverse_lazy("counter:admin_list"), - "slug": "counters", - "name": _("Counters"), - }, - { - "url": reverse_lazy("counter:product_list"), - "slug": "products", - "name": _("Products"), - }, - { - "url": reverse_lazy("counter:product_list_archived"), - "slug": "archive", - "name": _("Archived products"), - }, - { - "url": reverse_lazy("counter:producttype_list"), - "slug": "product_types", - "name": _("Product types"), - }, - { - "url": reverse_lazy("counter:cash_summary_list"), - "slug": "cash_summary", - "name": _("Cash register summaries"), - }, - { - "url": reverse_lazy("counter:invoices_call"), - "slug": "invoices_call", - "name": _("Invoices call"), - }, - { - "url": reverse_lazy("counter:eticket_list"), - "slug": "etickets", - "name": _("Etickets"), - }, - ] - - -class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): - """A list view for the admins.""" - - model = Counter - template_name = "counter/counter_list.jinja" - current_tab = "counters" - - -class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's manager).""" - - model = Counter - form_class = CounterEditForm - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - def dispatch(self, request, *args, **kwargs): - obj = self.get_object() - self.edit_club.append(obj.club) - return super().dispatch(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:admin", kwargs={"counter_id": self.object.id}) - - -class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's admin).""" - - model = Counter - form_class = modelform_factory(Counter, fields=["name", "club", "type"]) - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - -class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create a counter (for the admins).""" - - model = Counter - form_class = modelform_factory( - Counter, - fields=["name", "club", "type", "products"], - widgets={"products": CheckboxSelectMultiple}, - ) - template_name = "core/create.jinja" - current_tab = "counters" - - -class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): - """Delete a counter (for the admins).""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "core/delete_confirm.jinja" - success_url = reverse_lazy("counter:admin_list") - current_tab = "counters" - - -# Product management - - -class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = ProductType - template_name = "counter/producttype_list.jinja" - current_tab = "product_types" - - -class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = ProductType - fields = ["name", "description", "comment", "icon", "priority"] - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = ProductType - template_name = "core/edit.jinja" - fields = ["name", "description", "comment", "icon", "priority"] - pk_url_kwarg = "type_id" - current_tab = "products" - - -class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - model = Product - queryset = Product.objects.values("id", "name", "code", "product_type__name") - template_name = "counter/product_list.jinja" - ordering = [ - F("product_type__priority").desc(nulls_last=True), - "product_type", - "name", - ] - - def get_context_data(self, **kwargs): - res = super().get_context_data(**kwargs) - res["object_list"] = itertools.groupby( - res["object_list"], key=itemgetter("product_type__name") - ) - return res - - -class ArchivedProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "archive" - - def get_queryset(self): - return super().get_queryset().filter(archived=True) - - -class ActiveProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "products" - - def get_queryset(self): - return super().get_queryset().filter(archived=False) - - -class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = Product - form_class = ProductEditForm - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = Product - form_class = ProductEditForm - pk_url_kwarg = "product_id" - template_name = "core/edit.jinja" - current_tab = "products" - - -class RefillingDeleteView(DeleteView): - """Delete a refilling (for the admins).""" - - model = Refilling - pk_url_kwarg = "refilling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class SellingDeleteView(DeleteView): - """Delete a selling (for the admins).""" - - model = Selling - pk_url_kwarg = "selling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -# Cash register summaries - - -class CashRegisterSummaryForm(forms.Form): - """Provide the cash summary form.""" - - ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0) - twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0) - fifty_cents = forms.IntegerField(label=_("50 cents"), required=False, min_value=0) - one_euro = forms.IntegerField(label=_("1 euro"), required=False, min_value=0) - two_euros = forms.IntegerField(label=_("2 euros"), required=False, min_value=0) - five_euros = forms.IntegerField(label=_("5 euros"), required=False, min_value=0) - ten_euros = forms.IntegerField(label=_("10 euros"), required=False, min_value=0) - twenty_euros = forms.IntegerField(label=_("20 euros"), required=False, min_value=0) - fifty_euros = forms.IntegerField(label=_("50 euros"), required=False, min_value=0) - hundred_euros = forms.IntegerField( - label=_("100 euros"), required=False, min_value=0 - ) - check_1_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_1_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_2_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_2_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_3_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_3_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_4_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_4_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_5_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_5_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - comment = forms.CharField(label=_("Comment"), required=False) - emptied = forms.BooleanField(label=_("Emptied"), required=False) - - def __init__(self, *args, **kwargs): - instance = kwargs.pop("instance", None) - super().__init__(*args, **kwargs) - if instance: - self.fields["ten_cents"].initial = ( - instance.ten_cents.quantity if instance.ten_cents else 0 - ) - self.fields["twenty_cents"].initial = ( - instance.twenty_cents.quantity if instance.twenty_cents else 0 - ) - self.fields["fifty_cents"].initial = ( - instance.fifty_cents.quantity if instance.fifty_cents else 0 - ) - self.fields["one_euro"].initial = ( - instance.one_euro.quantity if instance.one_euro else 0 - ) - self.fields["two_euros"].initial = ( - instance.two_euros.quantity if instance.two_euros else 0 - ) - self.fields["five_euros"].initial = ( - instance.five_euros.quantity if instance.five_euros else 0 - ) - self.fields["ten_euros"].initial = ( - instance.ten_euros.quantity if instance.ten_euros else 0 - ) - self.fields["twenty_euros"].initial = ( - instance.twenty_euros.quantity if instance.twenty_euros else 0 - ) - self.fields["fifty_euros"].initial = ( - instance.fifty_euros.quantity if instance.fifty_euros else 0 - ) - self.fields["hundred_euros"].initial = ( - instance.hundred_euros.quantity if instance.hundred_euros else 0 - ) - self.fields["check_1_quantity"].initial = ( - instance.check_1.quantity if instance.check_1 else 0 - ) - self.fields["check_2_quantity"].initial = ( - instance.check_2.quantity if instance.check_2 else 0 - ) - self.fields["check_3_quantity"].initial = ( - instance.check_3.quantity if instance.check_3 else 0 - ) - self.fields["check_4_quantity"].initial = ( - instance.check_4.quantity if instance.check_4 else 0 - ) - self.fields["check_5_quantity"].initial = ( - instance.check_5.quantity if instance.check_5 else 0 - ) - self.fields["check_1_value"].initial = ( - instance.check_1.value if instance.check_1 else 0 - ) - self.fields["check_2_value"].initial = ( - instance.check_2.value if instance.check_2 else 0 - ) - self.fields["check_3_value"].initial = ( - instance.check_3.value if instance.check_3 else 0 - ) - self.fields["check_4_value"].initial = ( - instance.check_4.value if instance.check_4 else 0 - ) - self.fields["check_5_value"].initial = ( - instance.check_5.value if instance.check_5 else 0 - ) - self.fields["comment"].initial = instance.comment - self.fields["emptied"].initial = instance.emptied - self.instance = instance - else: - self.instance = None - - def save(self, counter=None): - cd = self.cleaned_data - summary = self.instance or CashRegisterSummary( - counter=counter, user=counter.get_random_barman() - ) - summary.comment = cd["comment"] - summary.emptied = cd["emptied"] - summary.save() - summary.items.all().delete() - # Cash - if cd["ten_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.1, quantity=cd["ten_cents"] - ).save() - if cd["twenty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.2, quantity=cd["twenty_cents"] - ).save() - if cd["fifty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.5, quantity=cd["fifty_cents"] - ).save() - if cd["one_euro"]: - CashRegisterSummaryItem( - cash_summary=summary, value=1, quantity=cd["one_euro"] - ).save() - if cd["two_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=2, quantity=cd["two_euros"] - ).save() - if cd["five_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=5, quantity=cd["five_euros"] - ).save() - if cd["ten_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=10, quantity=cd["ten_euros"] - ).save() - if cd["twenty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=20, quantity=cd["twenty_euros"] - ).save() - if cd["fifty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=50, quantity=cd["fifty_euros"] - ).save() - if cd["hundred_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=100, quantity=cd["hundred_euros"] - ).save() - # Checks - if cd["check_1_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_1_value"], - quantity=cd["check_1_quantity"], - is_check=True, - ).save() - if cd["check_2_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_2_value"], - quantity=cd["check_2_quantity"], - is_check=True, - ).save() - if cd["check_3_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_3_value"], - quantity=cd["check_3_quantity"], - is_check=True, - ).save() - if cd["check_4_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_4_value"], - quantity=cd["check_4_quantity"], - is_check=True, - ).save() - if cd["check_5_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_5_value"], - quantity=cd["check_5_quantity"], - is_check=True, - ).save() - if summary.items.count() < 1: - summary.delete() - - -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 CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): - """Provide the cash summary form.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/cash_register_summary.jinja" - current_tab = "cash_summary" - - 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(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm() - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm(request.POST) - if self.form.is_valid(): - self.form.save(self.object) - return HttpResponseRedirect(self.get_success_url()) - return super().get(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id}) - - def get_context_data(self, **kwargs): - """Add form to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["form"] = self.form - return kwargs - - -class CounterActivityView(DetailView): - """Show the bar activity.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/activity.jinja" - - -class CounterStatView(DetailView, CounterAdminMixin): - """Show the bar stats.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/stats.jinja" - - def get_context_data(self, **kwargs): - """Add stats to the context.""" - counter: Counter = self.object - semester_start = get_start_of_semester() - office_hours = counter.get_top_barmen() - kwargs = super().get_context_data(**kwargs) - kwargs.update( - { - "counter": counter, - "current_semester": get_semester_code(), - "total_sellings": counter.get_total_sales(since=semester_start), - "top_customers": counter.get_top_customers(since=semester_start)[:100], - "top_barman": office_hours[:100], - "top_barman_semester": ( - office_hours.filter(start__gt=semester_start)[:100] - ), - } - ) - return kwargs - - def dispatch(self, request, *args, **kwargs): - try: - return super().dispatch(request, *args, **kwargs) - except PermissionDenied: - if ( - request.user.is_root - or request.user.is_board_member - or self.get_object().is_owned_by(request.user) - ): - return super(CanEditMixin, self).dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_register_summary.jinja" - context_object_name = "cashsummary" - pk_url_kwarg = "cashsummary_id" - form_class = CashRegisterSummaryForm - current_tab = "cash_summary" - - def get_success_url(self): - return reverse("counter:cash_summary_list") - - -class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """Display a list of cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_summary_list.jinja" - context_object_name = "cashsummary_list" - current_tab = "cash_summary" - queryset = CashRegisterSummary.objects.all().order_by("-date") - paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - form = CashSummaryFormBase(self.request.GET) - kwargs["form"] = form - kwargs["summaries_sums"] = {} - kwargs["refilling_sums"] = {} - for c in Counter.objects.filter(type="BAR").all(): - refillings = Refilling.objects.filter(counter=c) - cashredistersummaries = CashRegisterSummary.objects.filter(counter=c) - if form.is_valid() and form.cleaned_data["begin_date"]: - refillings = refillings.filter( - date__gte=form.cleaned_data["begin_date"] - ) - cashredistersummaries = cashredistersummaries.filter( - date__gte=form.cleaned_data["begin_date"] - ) - else: - last_summary = ( - CashRegisterSummary.objects.filter(counter=c, emptied=True) - .order_by("-date") - .first() - ) - if last_summary: - refillings = refillings.filter(date__gt=last_summary.date) - cashredistersummaries = cashredistersummaries.filter( - date__gt=last_summary.date - ) - else: - refillings = refillings.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) # My birth date should be old enough - cashredistersummaries = cashredistersummaries.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) - if form.is_valid() and form.cleaned_data["end_date"]: - refillings = refillings.filter(date__lte=form.cleaned_data["end_date"]) - cashredistersummaries = cashredistersummaries.filter( - date__lte=form.cleaned_data["end_date"] - ) - kwargs["summaries_sums"][c.name] = sum( - [s.get_total() for s in cashredistersummaries.all()] - ) - kwargs["refilling_sums"][c.name] = sum([s.amount for s in refillings.all()]) - return kwargs - - -class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): - template_name = "counter/invoices_call.jinja" - current_tab = "invoices_call" - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC") - if "month" in self.request.GET: - start_date = datetime.strptime(self.request.GET["month"], "%Y-%m") - else: - start_date = datetime( - year=timezone.now().year, - month=(timezone.now().month + 10) % 12 + 1, - day=1, - ) - start_date = start_date.replace(tzinfo=tz.utc) - end_date = (start_date + timedelta(days=32)).replace( - day=1, hour=0, minute=0, microsecond=0 - ) - from django.db.models import Case, F, Sum, When - - kwargs["sum_cb"] = sum( - [ - r.amount - for r in Refilling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["sum_cb"] += sum( - [ - s.quantity * s.unit_price - for s in Selling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["start_date"] = start_date - kwargs["sums"] = ( - Selling.objects.values("club__name") - .annotate( - selling_sum=Sum( - Case( - When( - date__gte=start_date, - date__lt=end_date, - then=F("unit_price") * F("quantity"), - ), - output_field=CurrencyField(), - ) - ) - ) - .exclude(selling_sum=None) - .order_by("-selling_sum") - ) - return kwargs - - -class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = Eticket - template_name = "counter/eticket_list.jinja" - ordering = ["id"] - current_tab = "etickets" - - -class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create an eticket.""" - - model = Eticket - template_name = "core/create.jinja" - form_class = EticketForm - current_tab = "etickets" - - -class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit an eticket.""" - - model = Eticket - template_name = "core/edit.jinja" - form_class = EticketForm - pk_url_kwarg = "eticket_id" - current_tab = "etickets" - - -class EticketPDFView(CanViewMixin, DetailView): - """Display the PDF of an eticket.""" - - model = Selling - pk_url_kwarg = "selling_id" - - def get(self, request, *args, **kwargs): - from reportlab.graphics import renderPDF - from reportlab.graphics.barcode.qr import QrCodeWidget - from reportlab.graphics.shapes import Drawing - from reportlab.lib.units import cm - from reportlab.lib.utils import ImageReader - from reportlab.pdfgen import canvas - - if not ( - hasattr(self.object, "product") and hasattr(self.object.product, "eticket") - ): - raise Http404 - - eticket = self.object.product.eticket - user = self.object.customer.user - code = "%s %s %s %s" % ( - self.object.customer.user.id, - self.object.product.id, - self.object.id, - self.object.quantity, - ) - code += " " + eticket.get_hash(code)[:8].upper() - response = HttpResponse(content_type="application/pdf") - response["Content-Disposition"] = 'filename="eticket.pdf"' - p = canvas.Canvas(response) - p.setTitle("Eticket") - im = ImageReader("core/static/core/img/eticket.jpg") - width, height = im.getSize() - size = max(width, height) - width = 8 * cm * width / size - height = 8 * cm * height / size - p.drawImage(im, 10 * cm, 25 * cm, width, height) - if eticket.banner: - im = ImageReader(eticket.banner) - width, height = im.getSize() - size = max(width, height) - width = 6 * cm * width / size - height = 6 * cm * height / size - p.drawImage(im, 1 * cm, 25 * cm, width, height) - if user.profile_pict: - im = ImageReader(user.profile_pict.file) - width, height = im.getSize() - size = max(width, height) - width = 150 * width / size - height = 150 * height / size - p.drawImage(im, 10.5 * cm - width / 2, 16 * cm, width, height) - if eticket.event_title: - p.setFont("Helvetica-Bold", 20) - p.drawCentredString(10.5 * cm, 23.6 * cm, eticket.event_title) - if eticket.event_date: - p.setFont("Helvetica-Bold", 16) - p.drawCentredString( - 10.5 * cm, 22.6 * cm, eticket.event_date.strftime("%d %b %Y") - ) # FIXME with a locale - p.setFont("Helvetica-Bold", 14) - p.drawCentredString( - 10.5 * cm, - 15 * cm, - "%s : %d %s" - % (user.get_display_name(), self.object.quantity, str(_("people(s)"))), - ) - p.setFont("Courier-Bold", 14) - qrcode = QrCodeWidget(code) - bounds = qrcode.getBounds() - width = bounds[2] - bounds[0] - height = bounds[3] - bounds[1] - d = Drawing(260, 260, transform=[260.0 / width, 0, 0, 260.0 / height, 0, 0]) - d.add(qrcode) - renderPDF.draw(d, p, 10.5 * cm - 130, 6.1 * cm) - p.drawCentredString(10.5 * cm, 6 * cm, code) - - partners = ImageReader("core/static/core/img/partners.png") - width, height = partners.getSize() - size = max(width, height) - width = width * 2 / 3 - height = height * 2 / 3 - p.drawImage(partners, 0 * cm, 0 * cm, width, height) - - p.showPage() - p.save() - return response - - -class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """List of refillings on a counter.""" - - model = Refilling - template_name = "counter/refilling_list.jinja" - current_tab = "counters" - paginate_by = 30 - - def dispatch(self, request, *args, **kwargs): - self.counter = get_object_or_404(Counter, pk=kwargs["counter_id"]) - self.queryset = Refilling.objects.filter(counter__id=self.counter.id) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["counter"] = self.counter - return kwargs - - -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} - ) From 1c4efc9431472cdeb82ddc9f20c2431b1dc813e0 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:41:47 +0100 Subject: [PATCH 06/53] extract admin views --- counter/{views.py => views/admin.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/{views.py => views/admin.py} (100%) diff --git a/counter/views.py b/counter/views/admin.py similarity index 100% rename from counter/views.py rename to counter/views/admin.py From b46b0882f35429c928fed91bdae41020318c3524 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:42:26 +0100 Subject: [PATCH 07/53] extract admin views --- counter/views/admin.py | 1264 +--------------------------------------- 1 file changed, 7 insertions(+), 1257 deletions(-) diff --git a/counter/views/admin.py b/counter/views/admin.py index 9483d335..414e66ae 100644 --- a/counter/views/admin.py +++ b/counter/views/admin.py @@ -13,702 +13,26 @@ # # import itertools -import re -from datetime import datetime, timedelta -from datetime import timezone as tz -from http import HTTPStatus +from datetime import timedelta 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.shortcuts import get_object_or_404 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 django.views.generic import DetailView, ListView +from django.views.generic.edit import CreateView, DeleteView, 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 core.views import CanEditMixin, CanViewMixin +from counter.forms import CounterEditForm, ProductEditForm +from counter.models import Counter, Product, ProductType, Refilling, Selling from counter.utils import is_logged_in_counter -if TYPE_CHECKING: - from core.models import User - - -class CounterAdminMixin(View): - """Protect counter admin section.""" - - edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] - edit_club = [] - - def _test_group(self, user): - return any(user.is_in_group(pk=grp_id) for grp_id in self.edit_group) - - def _test_club(self, user): - return any(c.can_be_edited_by(user) for c in self.edit_club) - - def dispatch(self, request, *args, **kwargs): - if not ( - request.user.is_root - or self._test_group(request.user) - or self._test_club(request.user) - ): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - -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 CounterTabsMixin(TabedViewMixin): - def get_tabs_title(self): - return self.object - - def get_list_of_tabs(self): - tab_list = [ - { - "url": reverse_lazy( - "counter:details", kwargs={"counter_id": self.object.id} - ), - "slug": "counter", - "name": _("Counter"), - } - ] - if self.object.type == "BAR": - tab_list.append( - { - "url": reverse_lazy( - "counter:cash_summary", kwargs={"counter_id": self.object.id} - ), - "slug": "cash_summary", - "name": _("Cash summary"), - } - ) - tab_list.append( - { - "url": reverse_lazy( - "counter:last_ops", kwargs={"counter_id": self.object.id} - ), - "slug": "last_ops", - "name": _("Last operations"), - } - ) - return tab_list - - -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) - - -class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): - """The click view - This is a detail view not to have to worry about loading the counter - Everything is made by hand in the post method. - """ - - model = Counter - queryset = Counter.objects.annotate_is_open() - template_name = "counter/counter_click.jinja" - pk_url_kwarg = "counter_id" - current_tab = "counter" - - def render_to_response(self, *args, **kwargs): - if self.is_ajax(self.request): - response = {"errors": []} - status = HTTPStatus.OK - - if self.request.session["too_young"]: - response["errors"].append(_("Too young for that product")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_allowed"]: - response["errors"].append(_("Not allowed for that product")) - status = HTTPStatus.FORBIDDEN - if self.request.session["no_age"]: - response["errors"].append(_("No date of birth provided")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_enough"]: - response["errors"].append(_("Not enough money")) - status = HTTPStatus.PAYMENT_REQUIRED - - if len(response["errors"]) > 1: - status = HTTPStatus.BAD_REQUEST - - response["basket"] = self.request.session["basket"] - - return JsonResponse(response, status=status) - - else: # Standard HTML page - return super().render_to_response(*args, **kwargs) - - def dispatch(self, request, *args, **kwargs): - self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) - obj: Counter = self.get_object() - if not self.customer.can_buy: - raise Http404 - if obj.type != "BAR" and not request.user.is_authenticated: - raise PermissionDenied - if obj.type == "BAR" and ( - "counter_token" not in request.session - or request.session["counter_token"] != obj.token - or len(obj.barmen_list) == 0 - ): - return redirect(obj) - return super().dispatch(request, *args, **kwargs) - - def get(self, request, *args, **kwargs): - """Simple get view.""" - if "basket" not in request.session: # Init the basket session entry - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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 - ): # Check that at least one barman is logged in - ret = self.cancel(request) # Otherwise, go to main view - return ret - - 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 - return self.cancel(request) - if self.object.type == "BAR" and not ( - "counter_token" in self.request.session - and self.request.session["counter_token"] == self.object.token - ): # Also 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" - ) - if "basket" not in request.session: - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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(): - self.operator = self.customer.user - else: - self.operator = self.object.get_random_barman() - action = self.request.POST.get("action", None) - if action is None: - 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": - self.refill(request) - elif action == "code": - return self.parse_code(request) - elif action == "cancel": - return self.cancel(request) - elif action == "finish": - return self.finish(request) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def customer_is_barman(self) -> bool: - barmen = self.object.barmen_list - return self.object.type == "BAR" and self.customer.user in barmen - - def get_product(self, pid): - return Product.objects.filter(pk=int(pid)).first() - - def get_price(self, pid): - p = self.get_product(pid) - if self.customer_is_barman(): - price = p.special_selling_price - else: - price = p.selling_price - return price - - def sum_basket(self, request): - total = 0 - for infos in request.session["basket"].values(): - total += infos["price"] * infos["qty"] - return total / 100 - - def get_total_quantity_for_pid(self, request, pid): - pid = str(pid) - if pid not in request.session["basket"]: - return 0 - return ( - request.session["basket"][pid]["qty"] - + request.session["basket"][pid]["bonus_qty"] - ) - - def compute_record_product(self, request, product=None): - recorded = 0 - basket = request.session["basket"] - - if product: - if product.is_record_product: - recorded -= 1 - elif product.is_unrecord_product: - recorded += 1 - - for p in basket: - bproduct = self.get_product(str(p)) - if bproduct.is_record_product: - recorded -= basket[p]["qty"] - elif bproduct.is_unrecord_product: - recorded += basket[p]["qty"] - return recorded - - def is_record_product_ok(self, request, product): - return self.customer.can_record_more( - self.compute_record_product(request, product) - ) - - @staticmethod - def is_ajax(request): - # when using the fetch API, the django request.POST dict is empty - # this is but a wretched contrivance which strive to replace - # the deprecated django is_ajax() method - # and which must be replaced as soon as possible - # by a proper separation between the api endpoints of the counter - return len(request.POST) == 0 and len(request.body) != 0 - - def add_product(self, request, q=1, p=None): - """Add a product to the basket - q is the quantity passed as integer - p is the product id, passed as an integer. - """ - pid = p or parse_qs(request.body.decode())["product_id"][0] - pid = str(pid) - price = self.get_price(pid) - total = self.sum_basket(request) - product: Product = self.get_product(pid) - user: User = self.customer.user - buying_groups = list(product.buying_groups.values_list("pk", flat=True)) - can_buy = len(buying_groups) == 0 or any( - user.is_in_group(pk=group_id) for group_id in buying_groups - ) - if not can_buy: - request.session["not_allowed"] = True - return False - bq = 0 # Bonus quantity, for trays - if ( - product.tray - ): # Handle the tray to adjust the quantity q to add and the bonus quantity bq - total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6 - bq = int((total_qty_mod_6 + q) / 6) # Integer division - q -= bq - if self.customer.amount < ( - total + round(q * float(price), 2) - ): # Check for enough money - request.session["not_enough"] = True - return False - if product.is_unrecord_product and not self.is_record_product_ok( - request, product - ): - request.session["not_allowed"] = True - return False - if product.limit_age >= 18 and not user.date_of_birth: - request.session["no_age"] = True - return False - if product.limit_age >= 18 and user.is_banned_alcohol: - request.session["not_allowed"] = True - return False - if user.is_banned_counter: - request.session["not_allowed"] = True - return False - if ( - user.date_of_birth and self.customer.user.get_age() < product.limit_age - ): # Check if affordable - request.session["too_young"] = True - return False - if pid in request.session["basket"]: # Add if already in basket - request.session["basket"][pid]["qty"] += q - request.session["basket"][pid]["bonus_qty"] += bq - else: # or create if not - request.session["basket"][pid] = { - "qty": q, - "price": int(price * 100), - "bonus_qty": bq, - } - 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] - product = self.get_product(pid) - if pid in request.session["basket"]: - if ( - product.tray - and (self.get_total_quantity_for_pid(request, pid) % 6 == 0) - and request.session["basket"][pid]["bonus_qty"] - ): - request.session["basket"][pid]["bonus_qty"] -= 1 - else: - request.session["basket"][pid]["qty"] -= 1 - if request.session["basket"][pid]["qty"] <= 0: - del request.session["basket"][pid] - request.session.modified = True - - def parse_code(self, request): - """Parse the string entered by the barman. - - This can be of two forms : - - ``, where the string is the code of the product - - `X`, where the integer is the quantity and str the code. - """ - string = parse_qs(request.body.decode()).get("code", [""])[0].upper() - if string == "FIN": - return self.finish(request) - elif string == "ANN": - return self.cancel(request) - regex = re.compile(r"^((?P[0-9]+)X)?(?P[A-Z0-9]+)$") - m = regex.match(string) - if m is not None: - nb = m.group("nb") - code = m.group("code") - nb = int(nb) if nb is not None else 1 - p = self.object.products.filter(code=code).first() - if p is not None: - self.add_product(request, nb, p.id) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def finish(self, request): - """Finish the click session, and validate the basket.""" - with transaction.atomic(): - request.session["last_basket"] = [] - if self.sum_basket(request) > self.customer.amount: - raise DataError(_("You have not enough money to buy all the basket")) - - for pid, infos in request.session["basket"].items(): - # This duplicates code for DB optimization (prevent to load many times the same object) - p = Product.objects.filter(pk=pid).first() - if self.customer_is_barman(): - uprice = p.special_selling_price - else: - uprice = p.selling_price - request.session["last_basket"].append( - "%d x %s" % (infos["qty"] + infos["bonus_qty"], p.name) - ) - s = Selling( - label=p.name, - product=p, - club=p.club, - counter=self.object, - unit_price=uprice, - quantity=infos["qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - if infos["bonus_qty"]: - s = Selling( - label=p.name + " (Plateau)", - product=p, - club=p.club, - counter=self.object, - unit_price=0, - quantity=infos["bonus_qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - self.customer.recorded_products -= self.compute_record_product(request) - self.customer.save() - request.session["last_customer"] = self.customer.user.get_display_name() - request.session["last_total"] = "%0.2f" % self.sum_basket(request) - request.session["new_customer_amount"] = str(self.customer.amount) - del request.session["basket"] - request.session.modified = True - kwargs = {"counter_id": self.object.id} - return HttpResponseRedirect( - reverse_lazy("counter:details", args=self.args, kwargs=kwargs) - ) - - def cancel(self, request): - """Cancel the click session.""" - kwargs = {"counter_id": self.object.id} - request.session.pop("basket", None) - return HttpResponseRedirect( - 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) - products = self.object.products.select_related("product_type") - if self.customer_is_barman(): - products = products.annotate(price=F("special_selling_price")) - else: - products = products.annotate(price=F("selling_price")) - kwargs["products"] = products - kwargs["categories"] = {} - for product in kwargs["products"]: - if product.product_type: - kwargs["categories"].setdefault(product.product_type, []).append( - 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 - - -@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) - - -# Counter admin views - - -class CounterAdminTabsMixin(TabedViewMixin): - tabs_title = _("Counter administration") - list_of_tabs = [ - { - "url": reverse_lazy("counter:admin_list"), - "slug": "counters", - "name": _("Counters"), - }, - { - "url": reverse_lazy("counter:product_list"), - "slug": "products", - "name": _("Products"), - }, - { - "url": reverse_lazy("counter:product_list_archived"), - "slug": "archive", - "name": _("Archived products"), - }, - { - "url": reverse_lazy("counter:producttype_list"), - "slug": "product_types", - "name": _("Product types"), - }, - { - "url": reverse_lazy("counter:cash_summary_list"), - "slug": "cash_summary", - "name": _("Cash register summaries"), - }, - { - "url": reverse_lazy("counter:invoices_call"), - "slug": "invoices_call", - "name": _("Invoices call"), - }, - { - "url": reverse_lazy("counter:eticket_list"), - "slug": "etickets", - "name": _("Etickets"), - }, - ] - class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): """A list view for the admins.""" @@ -904,303 +228,6 @@ class SellingDeleteView(DeleteView): raise PermissionDenied -# Cash register summaries - - -class CashRegisterSummaryForm(forms.Form): - """Provide the cash summary form.""" - - ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0) - twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0) - fifty_cents = forms.IntegerField(label=_("50 cents"), required=False, min_value=0) - one_euro = forms.IntegerField(label=_("1 euro"), required=False, min_value=0) - two_euros = forms.IntegerField(label=_("2 euros"), required=False, min_value=0) - five_euros = forms.IntegerField(label=_("5 euros"), required=False, min_value=0) - ten_euros = forms.IntegerField(label=_("10 euros"), required=False, min_value=0) - twenty_euros = forms.IntegerField(label=_("20 euros"), required=False, min_value=0) - fifty_euros = forms.IntegerField(label=_("50 euros"), required=False, min_value=0) - hundred_euros = forms.IntegerField( - label=_("100 euros"), required=False, min_value=0 - ) - check_1_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_1_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_2_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_2_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_3_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_3_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_4_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_4_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_5_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_5_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - comment = forms.CharField(label=_("Comment"), required=False) - emptied = forms.BooleanField(label=_("Emptied"), required=False) - - def __init__(self, *args, **kwargs): - instance = kwargs.pop("instance", None) - super().__init__(*args, **kwargs) - if instance: - self.fields["ten_cents"].initial = ( - instance.ten_cents.quantity if instance.ten_cents else 0 - ) - self.fields["twenty_cents"].initial = ( - instance.twenty_cents.quantity if instance.twenty_cents else 0 - ) - self.fields["fifty_cents"].initial = ( - instance.fifty_cents.quantity if instance.fifty_cents else 0 - ) - self.fields["one_euro"].initial = ( - instance.one_euro.quantity if instance.one_euro else 0 - ) - self.fields["two_euros"].initial = ( - instance.two_euros.quantity if instance.two_euros else 0 - ) - self.fields["five_euros"].initial = ( - instance.five_euros.quantity if instance.five_euros else 0 - ) - self.fields["ten_euros"].initial = ( - instance.ten_euros.quantity if instance.ten_euros else 0 - ) - self.fields["twenty_euros"].initial = ( - instance.twenty_euros.quantity if instance.twenty_euros else 0 - ) - self.fields["fifty_euros"].initial = ( - instance.fifty_euros.quantity if instance.fifty_euros else 0 - ) - self.fields["hundred_euros"].initial = ( - instance.hundred_euros.quantity if instance.hundred_euros else 0 - ) - self.fields["check_1_quantity"].initial = ( - instance.check_1.quantity if instance.check_1 else 0 - ) - self.fields["check_2_quantity"].initial = ( - instance.check_2.quantity if instance.check_2 else 0 - ) - self.fields["check_3_quantity"].initial = ( - instance.check_3.quantity if instance.check_3 else 0 - ) - self.fields["check_4_quantity"].initial = ( - instance.check_4.quantity if instance.check_4 else 0 - ) - self.fields["check_5_quantity"].initial = ( - instance.check_5.quantity if instance.check_5 else 0 - ) - self.fields["check_1_value"].initial = ( - instance.check_1.value if instance.check_1 else 0 - ) - self.fields["check_2_value"].initial = ( - instance.check_2.value if instance.check_2 else 0 - ) - self.fields["check_3_value"].initial = ( - instance.check_3.value if instance.check_3 else 0 - ) - self.fields["check_4_value"].initial = ( - instance.check_4.value if instance.check_4 else 0 - ) - self.fields["check_5_value"].initial = ( - instance.check_5.value if instance.check_5 else 0 - ) - self.fields["comment"].initial = instance.comment - self.fields["emptied"].initial = instance.emptied - self.instance = instance - else: - self.instance = None - - def save(self, counter=None): - cd = self.cleaned_data - summary = self.instance or CashRegisterSummary( - counter=counter, user=counter.get_random_barman() - ) - summary.comment = cd["comment"] - summary.emptied = cd["emptied"] - summary.save() - summary.items.all().delete() - # Cash - if cd["ten_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.1, quantity=cd["ten_cents"] - ).save() - if cd["twenty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.2, quantity=cd["twenty_cents"] - ).save() - if cd["fifty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.5, quantity=cd["fifty_cents"] - ).save() - if cd["one_euro"]: - CashRegisterSummaryItem( - cash_summary=summary, value=1, quantity=cd["one_euro"] - ).save() - if cd["two_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=2, quantity=cd["two_euros"] - ).save() - if cd["five_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=5, quantity=cd["five_euros"] - ).save() - if cd["ten_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=10, quantity=cd["ten_euros"] - ).save() - if cd["twenty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=20, quantity=cd["twenty_euros"] - ).save() - if cd["fifty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=50, quantity=cd["fifty_euros"] - ).save() - if cd["hundred_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=100, quantity=cd["hundred_euros"] - ).save() - # Checks - if cd["check_1_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_1_value"], - quantity=cd["check_1_quantity"], - is_check=True, - ).save() - if cd["check_2_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_2_value"], - quantity=cd["check_2_quantity"], - is_check=True, - ).save() - if cd["check_3_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_3_value"], - quantity=cd["check_3_quantity"], - is_check=True, - ).save() - if cd["check_4_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_4_value"], - quantity=cd["check_4_quantity"], - is_check=True, - ).save() - if cd["check_5_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_5_value"], - quantity=cd["check_5_quantity"], - is_check=True, - ).save() - if summary.items.count() < 1: - summary.delete() - - -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 CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): - """Provide the cash summary form.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/cash_register_summary.jinja" - current_tab = "cash_summary" - - 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(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm() - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm(request.POST) - if self.form.is_valid(): - self.form.save(self.object) - return HttpResponseRedirect(self.get_success_url()) - return super().get(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id}) - - def get_context_data(self, **kwargs): - """Add form to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["form"] = self.form - return kwargs - - -class CounterActivityView(DetailView): - """Show the bar activity.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/activity.jinja" - - class CounterStatView(DetailView, CounterAdminMixin): """Show the bar stats.""" @@ -1241,259 +268,6 @@ class CounterStatView(DetailView, CounterAdminMixin): raise PermissionDenied -class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_register_summary.jinja" - context_object_name = "cashsummary" - pk_url_kwarg = "cashsummary_id" - form_class = CashRegisterSummaryForm - current_tab = "cash_summary" - - def get_success_url(self): - return reverse("counter:cash_summary_list") - - -class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """Display a list of cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_summary_list.jinja" - context_object_name = "cashsummary_list" - current_tab = "cash_summary" - queryset = CashRegisterSummary.objects.all().order_by("-date") - paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - form = CashSummaryFormBase(self.request.GET) - kwargs["form"] = form - kwargs["summaries_sums"] = {} - kwargs["refilling_sums"] = {} - for c in Counter.objects.filter(type="BAR").all(): - refillings = Refilling.objects.filter(counter=c) - cashredistersummaries = CashRegisterSummary.objects.filter(counter=c) - if form.is_valid() and form.cleaned_data["begin_date"]: - refillings = refillings.filter( - date__gte=form.cleaned_data["begin_date"] - ) - cashredistersummaries = cashredistersummaries.filter( - date__gte=form.cleaned_data["begin_date"] - ) - else: - last_summary = ( - CashRegisterSummary.objects.filter(counter=c, emptied=True) - .order_by("-date") - .first() - ) - if last_summary: - refillings = refillings.filter(date__gt=last_summary.date) - cashredistersummaries = cashredistersummaries.filter( - date__gt=last_summary.date - ) - else: - refillings = refillings.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) # My birth date should be old enough - cashredistersummaries = cashredistersummaries.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) - if form.is_valid() and form.cleaned_data["end_date"]: - refillings = refillings.filter(date__lte=form.cleaned_data["end_date"]) - cashredistersummaries = cashredistersummaries.filter( - date__lte=form.cleaned_data["end_date"] - ) - kwargs["summaries_sums"][c.name] = sum( - [s.get_total() for s in cashredistersummaries.all()] - ) - kwargs["refilling_sums"][c.name] = sum([s.amount for s in refillings.all()]) - return kwargs - - -class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): - template_name = "counter/invoices_call.jinja" - current_tab = "invoices_call" - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC") - if "month" in self.request.GET: - start_date = datetime.strptime(self.request.GET["month"], "%Y-%m") - else: - start_date = datetime( - year=timezone.now().year, - month=(timezone.now().month + 10) % 12 + 1, - day=1, - ) - start_date = start_date.replace(tzinfo=tz.utc) - end_date = (start_date + timedelta(days=32)).replace( - day=1, hour=0, minute=0, microsecond=0 - ) - from django.db.models import Case, F, Sum, When - - kwargs["sum_cb"] = sum( - [ - r.amount - for r in Refilling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["sum_cb"] += sum( - [ - s.quantity * s.unit_price - for s in Selling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["start_date"] = start_date - kwargs["sums"] = ( - Selling.objects.values("club__name") - .annotate( - selling_sum=Sum( - Case( - When( - date__gte=start_date, - date__lt=end_date, - then=F("unit_price") * F("quantity"), - ), - output_field=CurrencyField(), - ) - ) - ) - .exclude(selling_sum=None) - .order_by("-selling_sum") - ) - return kwargs - - -class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = Eticket - template_name = "counter/eticket_list.jinja" - ordering = ["id"] - current_tab = "etickets" - - -class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create an eticket.""" - - model = Eticket - template_name = "core/create.jinja" - form_class = EticketForm - current_tab = "etickets" - - -class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit an eticket.""" - - model = Eticket - template_name = "core/edit.jinja" - form_class = EticketForm - pk_url_kwarg = "eticket_id" - current_tab = "etickets" - - -class EticketPDFView(CanViewMixin, DetailView): - """Display the PDF of an eticket.""" - - model = Selling - pk_url_kwarg = "selling_id" - - def get(self, request, *args, **kwargs): - from reportlab.graphics import renderPDF - from reportlab.graphics.barcode.qr import QrCodeWidget - from reportlab.graphics.shapes import Drawing - from reportlab.lib.units import cm - from reportlab.lib.utils import ImageReader - from reportlab.pdfgen import canvas - - if not ( - hasattr(self.object, "product") and hasattr(self.object.product, "eticket") - ): - raise Http404 - - eticket = self.object.product.eticket - user = self.object.customer.user - code = "%s %s %s %s" % ( - self.object.customer.user.id, - self.object.product.id, - self.object.id, - self.object.quantity, - ) - code += " " + eticket.get_hash(code)[:8].upper() - response = HttpResponse(content_type="application/pdf") - response["Content-Disposition"] = 'filename="eticket.pdf"' - p = canvas.Canvas(response) - p.setTitle("Eticket") - im = ImageReader("core/static/core/img/eticket.jpg") - width, height = im.getSize() - size = max(width, height) - width = 8 * cm * width / size - height = 8 * cm * height / size - p.drawImage(im, 10 * cm, 25 * cm, width, height) - if eticket.banner: - im = ImageReader(eticket.banner) - width, height = im.getSize() - size = max(width, height) - width = 6 * cm * width / size - height = 6 * cm * height / size - p.drawImage(im, 1 * cm, 25 * cm, width, height) - if user.profile_pict: - im = ImageReader(user.profile_pict.file) - width, height = im.getSize() - size = max(width, height) - width = 150 * width / size - height = 150 * height / size - p.drawImage(im, 10.5 * cm - width / 2, 16 * cm, width, height) - if eticket.event_title: - p.setFont("Helvetica-Bold", 20) - p.drawCentredString(10.5 * cm, 23.6 * cm, eticket.event_title) - if eticket.event_date: - p.setFont("Helvetica-Bold", 16) - p.drawCentredString( - 10.5 * cm, 22.6 * cm, eticket.event_date.strftime("%d %b %Y") - ) # FIXME with a locale - p.setFont("Helvetica-Bold", 14) - p.drawCentredString( - 10.5 * cm, - 15 * cm, - "%s : %d %s" - % (user.get_display_name(), self.object.quantity, str(_("people(s)"))), - ) - p.setFont("Courier-Bold", 14) - qrcode = QrCodeWidget(code) - bounds = qrcode.getBounds() - width = bounds[2] - bounds[0] - height = bounds[3] - bounds[1] - d = Drawing(260, 260, transform=[260.0 / width, 0, 0, 260.0 / height, 0, 0]) - d.add(qrcode) - renderPDF.draw(d, p, 10.5 * cm - 130, 6.1 * cm) - p.drawCentredString(10.5 * cm, 6 * cm, code) - - partners = ImageReader("core/static/core/img/partners.png") - width, height = partners.getSize() - size = max(width, height) - width = width * 2 / 3 - height = height * 2 / 3 - p.drawImage(partners, 0 * cm, 0 * cm, width, height) - - p.showPage() - p.save() - return response - - class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): """List of refillings on a counter.""" @@ -1511,27 +285,3 @@ class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListVie kwargs = super().get_context_data(**kwargs) kwargs["counter"] = self.counter return kwargs - - -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} - ) From c4643ee52c55ddd36b32f51e207e656a5db9c901 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:42:50 +0100 Subject: [PATCH 08/53] extract invoice views --- counter/{views.py => views/invoice.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/{views.py => views/invoice.py} (100%) diff --git a/counter/views.py b/counter/views/invoice.py similarity index 100% rename from counter/views.py rename to counter/views/invoice.py From 71c71581247e5fc1b1db7d36217d29362d3ba731 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:43:26 +0100 Subject: [PATCH 09/53] extract invoice views --- counter/views/invoice.py | 1455 +------------------------------------- 1 file changed, 3 insertions(+), 1452 deletions(-) diff --git a/counter/views/invoice.py b/counter/views/invoice.py index 9483d335..19c94c8b 100644 --- a/counter/views/invoice.py +++ b/counter/views/invoice.py @@ -12,1304 +12,15 @@ # 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 django.views.generic import TemplateView 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 CounterAdminMixin(View): - """Protect counter admin section.""" - - edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] - edit_club = [] - - def _test_group(self, user): - return any(user.is_in_group(pk=grp_id) for grp_id in self.edit_group) - - def _test_club(self, user): - return any(c.can_be_edited_by(user) for c in self.edit_club) - - def dispatch(self, request, *args, **kwargs): - if not ( - request.user.is_root - or self._test_group(request.user) - or self._test_club(request.user) - ): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - -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 CounterTabsMixin(TabedViewMixin): - def get_tabs_title(self): - return self.object - - def get_list_of_tabs(self): - tab_list = [ - { - "url": reverse_lazy( - "counter:details", kwargs={"counter_id": self.object.id} - ), - "slug": "counter", - "name": _("Counter"), - } - ] - if self.object.type == "BAR": - tab_list.append( - { - "url": reverse_lazy( - "counter:cash_summary", kwargs={"counter_id": self.object.id} - ), - "slug": "cash_summary", - "name": _("Cash summary"), - } - ) - tab_list.append( - { - "url": reverse_lazy( - "counter:last_ops", kwargs={"counter_id": self.object.id} - ), - "slug": "last_ops", - "name": _("Last operations"), - } - ) - return tab_list - - -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) - - -class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): - """The click view - This is a detail view not to have to worry about loading the counter - Everything is made by hand in the post method. - """ - - model = Counter - queryset = Counter.objects.annotate_is_open() - template_name = "counter/counter_click.jinja" - pk_url_kwarg = "counter_id" - current_tab = "counter" - - def render_to_response(self, *args, **kwargs): - if self.is_ajax(self.request): - response = {"errors": []} - status = HTTPStatus.OK - - if self.request.session["too_young"]: - response["errors"].append(_("Too young for that product")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_allowed"]: - response["errors"].append(_("Not allowed for that product")) - status = HTTPStatus.FORBIDDEN - if self.request.session["no_age"]: - response["errors"].append(_("No date of birth provided")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_enough"]: - response["errors"].append(_("Not enough money")) - status = HTTPStatus.PAYMENT_REQUIRED - - if len(response["errors"]) > 1: - status = HTTPStatus.BAD_REQUEST - - response["basket"] = self.request.session["basket"] - - return JsonResponse(response, status=status) - - else: # Standard HTML page - return super().render_to_response(*args, **kwargs) - - def dispatch(self, request, *args, **kwargs): - self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) - obj: Counter = self.get_object() - if not self.customer.can_buy: - raise Http404 - if obj.type != "BAR" and not request.user.is_authenticated: - raise PermissionDenied - if obj.type == "BAR" and ( - "counter_token" not in request.session - or request.session["counter_token"] != obj.token - or len(obj.barmen_list) == 0 - ): - return redirect(obj) - return super().dispatch(request, *args, **kwargs) - - def get(self, request, *args, **kwargs): - """Simple get view.""" - if "basket" not in request.session: # Init the basket session entry - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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 - ): # Check that at least one barman is logged in - ret = self.cancel(request) # Otherwise, go to main view - return ret - - 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 - return self.cancel(request) - if self.object.type == "BAR" and not ( - "counter_token" in self.request.session - and self.request.session["counter_token"] == self.object.token - ): # Also 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" - ) - if "basket" not in request.session: - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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(): - self.operator = self.customer.user - else: - self.operator = self.object.get_random_barman() - action = self.request.POST.get("action", None) - if action is None: - 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": - self.refill(request) - elif action == "code": - return self.parse_code(request) - elif action == "cancel": - return self.cancel(request) - elif action == "finish": - return self.finish(request) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def customer_is_barman(self) -> bool: - barmen = self.object.barmen_list - return self.object.type == "BAR" and self.customer.user in barmen - - def get_product(self, pid): - return Product.objects.filter(pk=int(pid)).first() - - def get_price(self, pid): - p = self.get_product(pid) - if self.customer_is_barman(): - price = p.special_selling_price - else: - price = p.selling_price - return price - - def sum_basket(self, request): - total = 0 - for infos in request.session["basket"].values(): - total += infos["price"] * infos["qty"] - return total / 100 - - def get_total_quantity_for_pid(self, request, pid): - pid = str(pid) - if pid not in request.session["basket"]: - return 0 - return ( - request.session["basket"][pid]["qty"] - + request.session["basket"][pid]["bonus_qty"] - ) - - def compute_record_product(self, request, product=None): - recorded = 0 - basket = request.session["basket"] - - if product: - if product.is_record_product: - recorded -= 1 - elif product.is_unrecord_product: - recorded += 1 - - for p in basket: - bproduct = self.get_product(str(p)) - if bproduct.is_record_product: - recorded -= basket[p]["qty"] - elif bproduct.is_unrecord_product: - recorded += basket[p]["qty"] - return recorded - - def is_record_product_ok(self, request, product): - return self.customer.can_record_more( - self.compute_record_product(request, product) - ) - - @staticmethod - def is_ajax(request): - # when using the fetch API, the django request.POST dict is empty - # this is but a wretched contrivance which strive to replace - # the deprecated django is_ajax() method - # and which must be replaced as soon as possible - # by a proper separation between the api endpoints of the counter - return len(request.POST) == 0 and len(request.body) != 0 - - def add_product(self, request, q=1, p=None): - """Add a product to the basket - q is the quantity passed as integer - p is the product id, passed as an integer. - """ - pid = p or parse_qs(request.body.decode())["product_id"][0] - pid = str(pid) - price = self.get_price(pid) - total = self.sum_basket(request) - product: Product = self.get_product(pid) - user: User = self.customer.user - buying_groups = list(product.buying_groups.values_list("pk", flat=True)) - can_buy = len(buying_groups) == 0 or any( - user.is_in_group(pk=group_id) for group_id in buying_groups - ) - if not can_buy: - request.session["not_allowed"] = True - return False - bq = 0 # Bonus quantity, for trays - if ( - product.tray - ): # Handle the tray to adjust the quantity q to add and the bonus quantity bq - total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6 - bq = int((total_qty_mod_6 + q) / 6) # Integer division - q -= bq - if self.customer.amount < ( - total + round(q * float(price), 2) - ): # Check for enough money - request.session["not_enough"] = True - return False - if product.is_unrecord_product and not self.is_record_product_ok( - request, product - ): - request.session["not_allowed"] = True - return False - if product.limit_age >= 18 and not user.date_of_birth: - request.session["no_age"] = True - return False - if product.limit_age >= 18 and user.is_banned_alcohol: - request.session["not_allowed"] = True - return False - if user.is_banned_counter: - request.session["not_allowed"] = True - return False - if ( - user.date_of_birth and self.customer.user.get_age() < product.limit_age - ): # Check if affordable - request.session["too_young"] = True - return False - if pid in request.session["basket"]: # Add if already in basket - request.session["basket"][pid]["qty"] += q - request.session["basket"][pid]["bonus_qty"] += bq - else: # or create if not - request.session["basket"][pid] = { - "qty": q, - "price": int(price * 100), - "bonus_qty": bq, - } - 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] - product = self.get_product(pid) - if pid in request.session["basket"]: - if ( - product.tray - and (self.get_total_quantity_for_pid(request, pid) % 6 == 0) - and request.session["basket"][pid]["bonus_qty"] - ): - request.session["basket"][pid]["bonus_qty"] -= 1 - else: - request.session["basket"][pid]["qty"] -= 1 - if request.session["basket"][pid]["qty"] <= 0: - del request.session["basket"][pid] - request.session.modified = True - - def parse_code(self, request): - """Parse the string entered by the barman. - - This can be of two forms : - - ``, where the string is the code of the product - - `X`, where the integer is the quantity and str the code. - """ - string = parse_qs(request.body.decode()).get("code", [""])[0].upper() - if string == "FIN": - return self.finish(request) - elif string == "ANN": - return self.cancel(request) - regex = re.compile(r"^((?P[0-9]+)X)?(?P[A-Z0-9]+)$") - m = regex.match(string) - if m is not None: - nb = m.group("nb") - code = m.group("code") - nb = int(nb) if nb is not None else 1 - p = self.object.products.filter(code=code).first() - if p is not None: - self.add_product(request, nb, p.id) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def finish(self, request): - """Finish the click session, and validate the basket.""" - with transaction.atomic(): - request.session["last_basket"] = [] - if self.sum_basket(request) > self.customer.amount: - raise DataError(_("You have not enough money to buy all the basket")) - - for pid, infos in request.session["basket"].items(): - # This duplicates code for DB optimization (prevent to load many times the same object) - p = Product.objects.filter(pk=pid).first() - if self.customer_is_barman(): - uprice = p.special_selling_price - else: - uprice = p.selling_price - request.session["last_basket"].append( - "%d x %s" % (infos["qty"] + infos["bonus_qty"], p.name) - ) - s = Selling( - label=p.name, - product=p, - club=p.club, - counter=self.object, - unit_price=uprice, - quantity=infos["qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - if infos["bonus_qty"]: - s = Selling( - label=p.name + " (Plateau)", - product=p, - club=p.club, - counter=self.object, - unit_price=0, - quantity=infos["bonus_qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - self.customer.recorded_products -= self.compute_record_product(request) - self.customer.save() - request.session["last_customer"] = self.customer.user.get_display_name() - request.session["last_total"] = "%0.2f" % self.sum_basket(request) - request.session["new_customer_amount"] = str(self.customer.amount) - del request.session["basket"] - request.session.modified = True - kwargs = {"counter_id": self.object.id} - return HttpResponseRedirect( - reverse_lazy("counter:details", args=self.args, kwargs=kwargs) - ) - - def cancel(self, request): - """Cancel the click session.""" - kwargs = {"counter_id": self.object.id} - request.session.pop("basket", None) - return HttpResponseRedirect( - 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) - products = self.object.products.select_related("product_type") - if self.customer_is_barman(): - products = products.annotate(price=F("special_selling_price")) - else: - products = products.annotate(price=F("selling_price")) - kwargs["products"] = products - kwargs["categories"] = {} - for product in kwargs["products"]: - if product.product_type: - kwargs["categories"].setdefault(product.product_type, []).append( - 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 - - -@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) - - -# Counter admin views - - -class CounterAdminTabsMixin(TabedViewMixin): - tabs_title = _("Counter administration") - list_of_tabs = [ - { - "url": reverse_lazy("counter:admin_list"), - "slug": "counters", - "name": _("Counters"), - }, - { - "url": reverse_lazy("counter:product_list"), - "slug": "products", - "name": _("Products"), - }, - { - "url": reverse_lazy("counter:product_list_archived"), - "slug": "archive", - "name": _("Archived products"), - }, - { - "url": reverse_lazy("counter:producttype_list"), - "slug": "product_types", - "name": _("Product types"), - }, - { - "url": reverse_lazy("counter:cash_summary_list"), - "slug": "cash_summary", - "name": _("Cash register summaries"), - }, - { - "url": reverse_lazy("counter:invoices_call"), - "slug": "invoices_call", - "name": _("Invoices call"), - }, - { - "url": reverse_lazy("counter:eticket_list"), - "slug": "etickets", - "name": _("Etickets"), - }, - ] - - -class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): - """A list view for the admins.""" - - model = Counter - template_name = "counter/counter_list.jinja" - current_tab = "counters" - - -class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's manager).""" - - model = Counter - form_class = CounterEditForm - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - def dispatch(self, request, *args, **kwargs): - obj = self.get_object() - self.edit_club.append(obj.club) - return super().dispatch(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:admin", kwargs={"counter_id": self.object.id}) - - -class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's admin).""" - - model = Counter - form_class = modelform_factory(Counter, fields=["name", "club", "type"]) - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - -class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create a counter (for the admins).""" - - model = Counter - form_class = modelform_factory( - Counter, - fields=["name", "club", "type", "products"], - widgets={"products": CheckboxSelectMultiple}, - ) - template_name = "core/create.jinja" - current_tab = "counters" - - -class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): - """Delete a counter (for the admins).""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "core/delete_confirm.jinja" - success_url = reverse_lazy("counter:admin_list") - current_tab = "counters" - - -# Product management - - -class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = ProductType - template_name = "counter/producttype_list.jinja" - current_tab = "product_types" - - -class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = ProductType - fields = ["name", "description", "comment", "icon", "priority"] - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = ProductType - template_name = "core/edit.jinja" - fields = ["name", "description", "comment", "icon", "priority"] - pk_url_kwarg = "type_id" - current_tab = "products" - - -class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - model = Product - queryset = Product.objects.values("id", "name", "code", "product_type__name") - template_name = "counter/product_list.jinja" - ordering = [ - F("product_type__priority").desc(nulls_last=True), - "product_type", - "name", - ] - - def get_context_data(self, **kwargs): - res = super().get_context_data(**kwargs) - res["object_list"] = itertools.groupby( - res["object_list"], key=itemgetter("product_type__name") - ) - return res - - -class ArchivedProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "archive" - - def get_queryset(self): - return super().get_queryset().filter(archived=True) - - -class ActiveProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "products" - - def get_queryset(self): - return super().get_queryset().filter(archived=False) - - -class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = Product - form_class = ProductEditForm - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = Product - form_class = ProductEditForm - pk_url_kwarg = "product_id" - template_name = "core/edit.jinja" - current_tab = "products" - - -class RefillingDeleteView(DeleteView): - """Delete a refilling (for the admins).""" - - model = Refilling - pk_url_kwarg = "refilling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class SellingDeleteView(DeleteView): - """Delete a selling (for the admins).""" - - model = Selling - pk_url_kwarg = "selling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -# Cash register summaries - - -class CashRegisterSummaryForm(forms.Form): - """Provide the cash summary form.""" - - ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0) - twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0) - fifty_cents = forms.IntegerField(label=_("50 cents"), required=False, min_value=0) - one_euro = forms.IntegerField(label=_("1 euro"), required=False, min_value=0) - two_euros = forms.IntegerField(label=_("2 euros"), required=False, min_value=0) - five_euros = forms.IntegerField(label=_("5 euros"), required=False, min_value=0) - ten_euros = forms.IntegerField(label=_("10 euros"), required=False, min_value=0) - twenty_euros = forms.IntegerField(label=_("20 euros"), required=False, min_value=0) - fifty_euros = forms.IntegerField(label=_("50 euros"), required=False, min_value=0) - hundred_euros = forms.IntegerField( - label=_("100 euros"), required=False, min_value=0 - ) - check_1_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_1_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_2_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_2_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_3_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_3_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_4_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_4_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_5_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_5_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - comment = forms.CharField(label=_("Comment"), required=False) - emptied = forms.BooleanField(label=_("Emptied"), required=False) - - def __init__(self, *args, **kwargs): - instance = kwargs.pop("instance", None) - super().__init__(*args, **kwargs) - if instance: - self.fields["ten_cents"].initial = ( - instance.ten_cents.quantity if instance.ten_cents else 0 - ) - self.fields["twenty_cents"].initial = ( - instance.twenty_cents.quantity if instance.twenty_cents else 0 - ) - self.fields["fifty_cents"].initial = ( - instance.fifty_cents.quantity if instance.fifty_cents else 0 - ) - self.fields["one_euro"].initial = ( - instance.one_euro.quantity if instance.one_euro else 0 - ) - self.fields["two_euros"].initial = ( - instance.two_euros.quantity if instance.two_euros else 0 - ) - self.fields["five_euros"].initial = ( - instance.five_euros.quantity if instance.five_euros else 0 - ) - self.fields["ten_euros"].initial = ( - instance.ten_euros.quantity if instance.ten_euros else 0 - ) - self.fields["twenty_euros"].initial = ( - instance.twenty_euros.quantity if instance.twenty_euros else 0 - ) - self.fields["fifty_euros"].initial = ( - instance.fifty_euros.quantity if instance.fifty_euros else 0 - ) - self.fields["hundred_euros"].initial = ( - instance.hundred_euros.quantity if instance.hundred_euros else 0 - ) - self.fields["check_1_quantity"].initial = ( - instance.check_1.quantity if instance.check_1 else 0 - ) - self.fields["check_2_quantity"].initial = ( - instance.check_2.quantity if instance.check_2 else 0 - ) - self.fields["check_3_quantity"].initial = ( - instance.check_3.quantity if instance.check_3 else 0 - ) - self.fields["check_4_quantity"].initial = ( - instance.check_4.quantity if instance.check_4 else 0 - ) - self.fields["check_5_quantity"].initial = ( - instance.check_5.quantity if instance.check_5 else 0 - ) - self.fields["check_1_value"].initial = ( - instance.check_1.value if instance.check_1 else 0 - ) - self.fields["check_2_value"].initial = ( - instance.check_2.value if instance.check_2 else 0 - ) - self.fields["check_3_value"].initial = ( - instance.check_3.value if instance.check_3 else 0 - ) - self.fields["check_4_value"].initial = ( - instance.check_4.value if instance.check_4 else 0 - ) - self.fields["check_5_value"].initial = ( - instance.check_5.value if instance.check_5 else 0 - ) - self.fields["comment"].initial = instance.comment - self.fields["emptied"].initial = instance.emptied - self.instance = instance - else: - self.instance = None - - def save(self, counter=None): - cd = self.cleaned_data - summary = self.instance or CashRegisterSummary( - counter=counter, user=counter.get_random_barman() - ) - summary.comment = cd["comment"] - summary.emptied = cd["emptied"] - summary.save() - summary.items.all().delete() - # Cash - if cd["ten_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.1, quantity=cd["ten_cents"] - ).save() - if cd["twenty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.2, quantity=cd["twenty_cents"] - ).save() - if cd["fifty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.5, quantity=cd["fifty_cents"] - ).save() - if cd["one_euro"]: - CashRegisterSummaryItem( - cash_summary=summary, value=1, quantity=cd["one_euro"] - ).save() - if cd["two_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=2, quantity=cd["two_euros"] - ).save() - if cd["five_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=5, quantity=cd["five_euros"] - ).save() - if cd["ten_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=10, quantity=cd["ten_euros"] - ).save() - if cd["twenty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=20, quantity=cd["twenty_euros"] - ).save() - if cd["fifty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=50, quantity=cd["fifty_euros"] - ).save() - if cd["hundred_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=100, quantity=cd["hundred_euros"] - ).save() - # Checks - if cd["check_1_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_1_value"], - quantity=cd["check_1_quantity"], - is_check=True, - ).save() - if cd["check_2_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_2_value"], - quantity=cd["check_2_quantity"], - is_check=True, - ).save() - if cd["check_3_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_3_value"], - quantity=cd["check_3_quantity"], - is_check=True, - ).save() - if cd["check_4_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_4_value"], - quantity=cd["check_4_quantity"], - is_check=True, - ).save() - if cd["check_5_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_5_value"], - quantity=cd["check_5_quantity"], - is_check=True, - ).save() - if summary.items.count() < 1: - summary.delete() - - -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 CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): - """Provide the cash summary form.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/cash_register_summary.jinja" - current_tab = "cash_summary" - - 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(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm() - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm(request.POST) - if self.form.is_valid(): - self.form.save(self.object) - return HttpResponseRedirect(self.get_success_url()) - return super().get(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id}) - - def get_context_data(self, **kwargs): - """Add form to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["form"] = self.form - return kwargs - - -class CounterActivityView(DetailView): - """Show the bar activity.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/activity.jinja" - - -class CounterStatView(DetailView, CounterAdminMixin): - """Show the bar stats.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/stats.jinja" - - def get_context_data(self, **kwargs): - """Add stats to the context.""" - counter: Counter = self.object - semester_start = get_start_of_semester() - office_hours = counter.get_top_barmen() - kwargs = super().get_context_data(**kwargs) - kwargs.update( - { - "counter": counter, - "current_semester": get_semester_code(), - "total_sellings": counter.get_total_sales(since=semester_start), - "top_customers": counter.get_top_customers(since=semester_start)[:100], - "top_barman": office_hours[:100], - "top_barman_semester": ( - office_hours.filter(start__gt=semester_start)[:100] - ), - } - ) - return kwargs - - def dispatch(self, request, *args, **kwargs): - try: - return super().dispatch(request, *args, **kwargs) - except PermissionDenied: - if ( - request.user.is_root - or request.user.is_board_member - or self.get_object().is_owned_by(request.user) - ): - return super(CanEditMixin, self).dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_register_summary.jinja" - context_object_name = "cashsummary" - pk_url_kwarg = "cashsummary_id" - form_class = CashRegisterSummaryForm - current_tab = "cash_summary" - - def get_success_url(self): - return reverse("counter:cash_summary_list") - - -class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """Display a list of cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_summary_list.jinja" - context_object_name = "cashsummary_list" - current_tab = "cash_summary" - queryset = CashRegisterSummary.objects.all().order_by("-date") - paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - form = CashSummaryFormBase(self.request.GET) - kwargs["form"] = form - kwargs["summaries_sums"] = {} - kwargs["refilling_sums"] = {} - for c in Counter.objects.filter(type="BAR").all(): - refillings = Refilling.objects.filter(counter=c) - cashredistersummaries = CashRegisterSummary.objects.filter(counter=c) - if form.is_valid() and form.cleaned_data["begin_date"]: - refillings = refillings.filter( - date__gte=form.cleaned_data["begin_date"] - ) - cashredistersummaries = cashredistersummaries.filter( - date__gte=form.cleaned_data["begin_date"] - ) - else: - last_summary = ( - CashRegisterSummary.objects.filter(counter=c, emptied=True) - .order_by("-date") - .first() - ) - if last_summary: - refillings = refillings.filter(date__gt=last_summary.date) - cashredistersummaries = cashredistersummaries.filter( - date__gt=last_summary.date - ) - else: - refillings = refillings.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) # My birth date should be old enough - cashredistersummaries = cashredistersummaries.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) - if form.is_valid() and form.cleaned_data["end_date"]: - refillings = refillings.filter(date__lte=form.cleaned_data["end_date"]) - cashredistersummaries = cashredistersummaries.filter( - date__lte=form.cleaned_data["end_date"] - ) - kwargs["summaries_sums"][c.name] = sum( - [s.get_total() for s in cashredistersummaries.all()] - ) - kwargs["refilling_sums"][c.name] = sum([s.amount for s in refillings.all()]) - return kwargs +from counter.models import Refilling, Selling class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): @@ -1332,7 +43,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): end_date = (start_date + timedelta(days=32)).replace( day=1, hour=0, minute=0, microsecond=0 ) - from django.db.models import Case, F, Sum, When + from django.db.models import Case, Sum, When kwargs["sum_cb"] = sum( [ @@ -1375,163 +86,3 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): .order_by("-selling_sum") ) return kwargs - - -class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = Eticket - template_name = "counter/eticket_list.jinja" - ordering = ["id"] - current_tab = "etickets" - - -class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create an eticket.""" - - model = Eticket - template_name = "core/create.jinja" - form_class = EticketForm - current_tab = "etickets" - - -class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit an eticket.""" - - model = Eticket - template_name = "core/edit.jinja" - form_class = EticketForm - pk_url_kwarg = "eticket_id" - current_tab = "etickets" - - -class EticketPDFView(CanViewMixin, DetailView): - """Display the PDF of an eticket.""" - - model = Selling - pk_url_kwarg = "selling_id" - - def get(self, request, *args, **kwargs): - from reportlab.graphics import renderPDF - from reportlab.graphics.barcode.qr import QrCodeWidget - from reportlab.graphics.shapes import Drawing - from reportlab.lib.units import cm - from reportlab.lib.utils import ImageReader - from reportlab.pdfgen import canvas - - if not ( - hasattr(self.object, "product") and hasattr(self.object.product, "eticket") - ): - raise Http404 - - eticket = self.object.product.eticket - user = self.object.customer.user - code = "%s %s %s %s" % ( - self.object.customer.user.id, - self.object.product.id, - self.object.id, - self.object.quantity, - ) - code += " " + eticket.get_hash(code)[:8].upper() - response = HttpResponse(content_type="application/pdf") - response["Content-Disposition"] = 'filename="eticket.pdf"' - p = canvas.Canvas(response) - p.setTitle("Eticket") - im = ImageReader("core/static/core/img/eticket.jpg") - width, height = im.getSize() - size = max(width, height) - width = 8 * cm * width / size - height = 8 * cm * height / size - p.drawImage(im, 10 * cm, 25 * cm, width, height) - if eticket.banner: - im = ImageReader(eticket.banner) - width, height = im.getSize() - size = max(width, height) - width = 6 * cm * width / size - height = 6 * cm * height / size - p.drawImage(im, 1 * cm, 25 * cm, width, height) - if user.profile_pict: - im = ImageReader(user.profile_pict.file) - width, height = im.getSize() - size = max(width, height) - width = 150 * width / size - height = 150 * height / size - p.drawImage(im, 10.5 * cm - width / 2, 16 * cm, width, height) - if eticket.event_title: - p.setFont("Helvetica-Bold", 20) - p.drawCentredString(10.5 * cm, 23.6 * cm, eticket.event_title) - if eticket.event_date: - p.setFont("Helvetica-Bold", 16) - p.drawCentredString( - 10.5 * cm, 22.6 * cm, eticket.event_date.strftime("%d %b %Y") - ) # FIXME with a locale - p.setFont("Helvetica-Bold", 14) - p.drawCentredString( - 10.5 * cm, - 15 * cm, - "%s : %d %s" - % (user.get_display_name(), self.object.quantity, str(_("people(s)"))), - ) - p.setFont("Courier-Bold", 14) - qrcode = QrCodeWidget(code) - bounds = qrcode.getBounds() - width = bounds[2] - bounds[0] - height = bounds[3] - bounds[1] - d = Drawing(260, 260, transform=[260.0 / width, 0, 0, 260.0 / height, 0, 0]) - d.add(qrcode) - renderPDF.draw(d, p, 10.5 * cm - 130, 6.1 * cm) - p.drawCentredString(10.5 * cm, 6 * cm, code) - - partners = ImageReader("core/static/core/img/partners.png") - width, height = partners.getSize() - size = max(width, height) - width = width * 2 / 3 - height = height * 2 / 3 - p.drawImage(partners, 0 * cm, 0 * cm, width, height) - - p.showPage() - p.save() - return response - - -class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """List of refillings on a counter.""" - - model = Refilling - template_name = "counter/refilling_list.jinja" - current_tab = "counters" - paginate_by = 30 - - def dispatch(self, request, *args, **kwargs): - self.counter = get_object_or_404(Counter, pk=kwargs["counter_id"]) - self.queryset = Refilling.objects.filter(counter__id=self.counter.id) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["counter"] = self.counter - return kwargs - - -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} - ) From f845bbf20aa3f2e63e0cd2ba88b9bb58a084ba8d Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:45:27 +0100 Subject: [PATCH 10/53] extract cash views --- counter/{views.py => views/cash.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/{views.py => views/cash.py} (100%) diff --git a/counter/views.py b/counter/views/cash.py similarity index 100% rename from counter/views.py rename to counter/views/cash.py From 4805c39b45566ca07577ce79b04680812516455a Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:46:24 +0100 Subject: [PATCH 11/53] extract cash views --- counter/views/cash.py | 1195 +---------------------------------------- 1 file changed, 6 insertions(+), 1189 deletions(-) diff --git a/counter/views/cash.py b/counter/views/cash.py index 9483d335..f117a151 100644 --- a/counter/views/cash.py +++ b/counter/views/cash.py @@ -12,900 +12,27 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # -import itertools -import re -from datetime import datetime, timedelta +from datetime import datetime 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.http import HttpResponseRedirect 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 django.views.generic import DetailView, ListView +from django.views.generic.edit import 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 core.views import CanViewMixin +from counter.forms import CashSummaryFormBase 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 CounterAdminMixin(View): - """Protect counter admin section.""" - - edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] - edit_club = [] - - def _test_group(self, user): - return any(user.is_in_group(pk=grp_id) for grp_id in self.edit_group) - - def _test_club(self, user): - return any(c.can_be_edited_by(user) for c in self.edit_club) - - def dispatch(self, request, *args, **kwargs): - if not ( - request.user.is_root - or self._test_group(request.user) - or self._test_club(request.user) - ): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - -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 CounterTabsMixin(TabedViewMixin): - def get_tabs_title(self): - return self.object - - def get_list_of_tabs(self): - tab_list = [ - { - "url": reverse_lazy( - "counter:details", kwargs={"counter_id": self.object.id} - ), - "slug": "counter", - "name": _("Counter"), - } - ] - if self.object.type == "BAR": - tab_list.append( - { - "url": reverse_lazy( - "counter:cash_summary", kwargs={"counter_id": self.object.id} - ), - "slug": "cash_summary", - "name": _("Cash summary"), - } - ) - tab_list.append( - { - "url": reverse_lazy( - "counter:last_ops", kwargs={"counter_id": self.object.id} - ), - "slug": "last_ops", - "name": _("Last operations"), - } - ) - return tab_list - - -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) - - -class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): - """The click view - This is a detail view not to have to worry about loading the counter - Everything is made by hand in the post method. - """ - - model = Counter - queryset = Counter.objects.annotate_is_open() - template_name = "counter/counter_click.jinja" - pk_url_kwarg = "counter_id" - current_tab = "counter" - - def render_to_response(self, *args, **kwargs): - if self.is_ajax(self.request): - response = {"errors": []} - status = HTTPStatus.OK - - if self.request.session["too_young"]: - response["errors"].append(_("Too young for that product")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_allowed"]: - response["errors"].append(_("Not allowed for that product")) - status = HTTPStatus.FORBIDDEN - if self.request.session["no_age"]: - response["errors"].append(_("No date of birth provided")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_enough"]: - response["errors"].append(_("Not enough money")) - status = HTTPStatus.PAYMENT_REQUIRED - - if len(response["errors"]) > 1: - status = HTTPStatus.BAD_REQUEST - - response["basket"] = self.request.session["basket"] - - return JsonResponse(response, status=status) - - else: # Standard HTML page - return super().render_to_response(*args, **kwargs) - - def dispatch(self, request, *args, **kwargs): - self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) - obj: Counter = self.get_object() - if not self.customer.can_buy: - raise Http404 - if obj.type != "BAR" and not request.user.is_authenticated: - raise PermissionDenied - if obj.type == "BAR" and ( - "counter_token" not in request.session - or request.session["counter_token"] != obj.token - or len(obj.barmen_list) == 0 - ): - return redirect(obj) - return super().dispatch(request, *args, **kwargs) - - def get(self, request, *args, **kwargs): - """Simple get view.""" - if "basket" not in request.session: # Init the basket session entry - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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 - ): # Check that at least one barman is logged in - ret = self.cancel(request) # Otherwise, go to main view - return ret - - 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 - return self.cancel(request) - if self.object.type == "BAR" and not ( - "counter_token" in self.request.session - and self.request.session["counter_token"] == self.object.token - ): # Also 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" - ) - if "basket" not in request.session: - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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(): - self.operator = self.customer.user - else: - self.operator = self.object.get_random_barman() - action = self.request.POST.get("action", None) - if action is None: - 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": - self.refill(request) - elif action == "code": - return self.parse_code(request) - elif action == "cancel": - return self.cancel(request) - elif action == "finish": - return self.finish(request) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def customer_is_barman(self) -> bool: - barmen = self.object.barmen_list - return self.object.type == "BAR" and self.customer.user in barmen - - def get_product(self, pid): - return Product.objects.filter(pk=int(pid)).first() - - def get_price(self, pid): - p = self.get_product(pid) - if self.customer_is_barman(): - price = p.special_selling_price - else: - price = p.selling_price - return price - - def sum_basket(self, request): - total = 0 - for infos in request.session["basket"].values(): - total += infos["price"] * infos["qty"] - return total / 100 - - def get_total_quantity_for_pid(self, request, pid): - pid = str(pid) - if pid not in request.session["basket"]: - return 0 - return ( - request.session["basket"][pid]["qty"] - + request.session["basket"][pid]["bonus_qty"] - ) - - def compute_record_product(self, request, product=None): - recorded = 0 - basket = request.session["basket"] - - if product: - if product.is_record_product: - recorded -= 1 - elif product.is_unrecord_product: - recorded += 1 - - for p in basket: - bproduct = self.get_product(str(p)) - if bproduct.is_record_product: - recorded -= basket[p]["qty"] - elif bproduct.is_unrecord_product: - recorded += basket[p]["qty"] - return recorded - - def is_record_product_ok(self, request, product): - return self.customer.can_record_more( - self.compute_record_product(request, product) - ) - - @staticmethod - def is_ajax(request): - # when using the fetch API, the django request.POST dict is empty - # this is but a wretched contrivance which strive to replace - # the deprecated django is_ajax() method - # and which must be replaced as soon as possible - # by a proper separation between the api endpoints of the counter - return len(request.POST) == 0 and len(request.body) != 0 - - def add_product(self, request, q=1, p=None): - """Add a product to the basket - q is the quantity passed as integer - p is the product id, passed as an integer. - """ - pid = p or parse_qs(request.body.decode())["product_id"][0] - pid = str(pid) - price = self.get_price(pid) - total = self.sum_basket(request) - product: Product = self.get_product(pid) - user: User = self.customer.user - buying_groups = list(product.buying_groups.values_list("pk", flat=True)) - can_buy = len(buying_groups) == 0 or any( - user.is_in_group(pk=group_id) for group_id in buying_groups - ) - if not can_buy: - request.session["not_allowed"] = True - return False - bq = 0 # Bonus quantity, for trays - if ( - product.tray - ): # Handle the tray to adjust the quantity q to add and the bonus quantity bq - total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6 - bq = int((total_qty_mod_6 + q) / 6) # Integer division - q -= bq - if self.customer.amount < ( - total + round(q * float(price), 2) - ): # Check for enough money - request.session["not_enough"] = True - return False - if product.is_unrecord_product and not self.is_record_product_ok( - request, product - ): - request.session["not_allowed"] = True - return False - if product.limit_age >= 18 and not user.date_of_birth: - request.session["no_age"] = True - return False - if product.limit_age >= 18 and user.is_banned_alcohol: - request.session["not_allowed"] = True - return False - if user.is_banned_counter: - request.session["not_allowed"] = True - return False - if ( - user.date_of_birth and self.customer.user.get_age() < product.limit_age - ): # Check if affordable - request.session["too_young"] = True - return False - if pid in request.session["basket"]: # Add if already in basket - request.session["basket"][pid]["qty"] += q - request.session["basket"][pid]["bonus_qty"] += bq - else: # or create if not - request.session["basket"][pid] = { - "qty": q, - "price": int(price * 100), - "bonus_qty": bq, - } - 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] - product = self.get_product(pid) - if pid in request.session["basket"]: - if ( - product.tray - and (self.get_total_quantity_for_pid(request, pid) % 6 == 0) - and request.session["basket"][pid]["bonus_qty"] - ): - request.session["basket"][pid]["bonus_qty"] -= 1 - else: - request.session["basket"][pid]["qty"] -= 1 - if request.session["basket"][pid]["qty"] <= 0: - del request.session["basket"][pid] - request.session.modified = True - - def parse_code(self, request): - """Parse the string entered by the barman. - - This can be of two forms : - - ``, where the string is the code of the product - - `X`, where the integer is the quantity and str the code. - """ - string = parse_qs(request.body.decode()).get("code", [""])[0].upper() - if string == "FIN": - return self.finish(request) - elif string == "ANN": - return self.cancel(request) - regex = re.compile(r"^((?P[0-9]+)X)?(?P[A-Z0-9]+)$") - m = regex.match(string) - if m is not None: - nb = m.group("nb") - code = m.group("code") - nb = int(nb) if nb is not None else 1 - p = self.object.products.filter(code=code).first() - if p is not None: - self.add_product(request, nb, p.id) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def finish(self, request): - """Finish the click session, and validate the basket.""" - with transaction.atomic(): - request.session["last_basket"] = [] - if self.sum_basket(request) > self.customer.amount: - raise DataError(_("You have not enough money to buy all the basket")) - - for pid, infos in request.session["basket"].items(): - # This duplicates code for DB optimization (prevent to load many times the same object) - p = Product.objects.filter(pk=pid).first() - if self.customer_is_barman(): - uprice = p.special_selling_price - else: - uprice = p.selling_price - request.session["last_basket"].append( - "%d x %s" % (infos["qty"] + infos["bonus_qty"], p.name) - ) - s = Selling( - label=p.name, - product=p, - club=p.club, - counter=self.object, - unit_price=uprice, - quantity=infos["qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - if infos["bonus_qty"]: - s = Selling( - label=p.name + " (Plateau)", - product=p, - club=p.club, - counter=self.object, - unit_price=0, - quantity=infos["bonus_qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - self.customer.recorded_products -= self.compute_record_product(request) - self.customer.save() - request.session["last_customer"] = self.customer.user.get_display_name() - request.session["last_total"] = "%0.2f" % self.sum_basket(request) - request.session["new_customer_amount"] = str(self.customer.amount) - del request.session["basket"] - request.session.modified = True - kwargs = {"counter_id": self.object.id} - return HttpResponseRedirect( - reverse_lazy("counter:details", args=self.args, kwargs=kwargs) - ) - - def cancel(self, request): - """Cancel the click session.""" - kwargs = {"counter_id": self.object.id} - request.session.pop("basket", None) - return HttpResponseRedirect( - 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) - products = self.object.products.select_related("product_type") - if self.customer_is_barman(): - products = products.annotate(price=F("special_selling_price")) - else: - products = products.annotate(price=F("selling_price")) - kwargs["products"] = products - kwargs["categories"] = {} - for product in kwargs["products"]: - if product.product_type: - kwargs["categories"].setdefault(product.product_type, []).append( - 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 - - -@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) - - -# Counter admin views - - -class CounterAdminTabsMixin(TabedViewMixin): - tabs_title = _("Counter administration") - list_of_tabs = [ - { - "url": reverse_lazy("counter:admin_list"), - "slug": "counters", - "name": _("Counters"), - }, - { - "url": reverse_lazy("counter:product_list"), - "slug": "products", - "name": _("Products"), - }, - { - "url": reverse_lazy("counter:product_list_archived"), - "slug": "archive", - "name": _("Archived products"), - }, - { - "url": reverse_lazy("counter:producttype_list"), - "slug": "product_types", - "name": _("Product types"), - }, - { - "url": reverse_lazy("counter:cash_summary_list"), - "slug": "cash_summary", - "name": _("Cash register summaries"), - }, - { - "url": reverse_lazy("counter:invoices_call"), - "slug": "invoices_call", - "name": _("Invoices call"), - }, - { - "url": reverse_lazy("counter:eticket_list"), - "slug": "etickets", - "name": _("Etickets"), - }, - ] - - -class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): - """A list view for the admins.""" - - model = Counter - template_name = "counter/counter_list.jinja" - current_tab = "counters" - - -class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's manager).""" - - model = Counter - form_class = CounterEditForm - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - def dispatch(self, request, *args, **kwargs): - obj = self.get_object() - self.edit_club.append(obj.club) - return super().dispatch(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:admin", kwargs={"counter_id": self.object.id}) - - -class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's admin).""" - - model = Counter - form_class = modelform_factory(Counter, fields=["name", "club", "type"]) - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - -class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create a counter (for the admins).""" - - model = Counter - form_class = modelform_factory( - Counter, - fields=["name", "club", "type", "products"], - widgets={"products": CheckboxSelectMultiple}, - ) - template_name = "core/create.jinja" - current_tab = "counters" - - -class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): - """Delete a counter (for the admins).""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "core/delete_confirm.jinja" - success_url = reverse_lazy("counter:admin_list") - current_tab = "counters" - - -# Product management - - -class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = ProductType - template_name = "counter/producttype_list.jinja" - current_tab = "product_types" - - -class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = ProductType - fields = ["name", "description", "comment", "icon", "priority"] - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = ProductType - template_name = "core/edit.jinja" - fields = ["name", "description", "comment", "icon", "priority"] - pk_url_kwarg = "type_id" - current_tab = "products" - - -class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - model = Product - queryset = Product.objects.values("id", "name", "code", "product_type__name") - template_name = "counter/product_list.jinja" - ordering = [ - F("product_type__priority").desc(nulls_last=True), - "product_type", - "name", - ] - - def get_context_data(self, **kwargs): - res = super().get_context_data(**kwargs) - res["object_list"] = itertools.groupby( - res["object_list"], key=itemgetter("product_type__name") - ) - return res - - -class ArchivedProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "archive" - - def get_queryset(self): - return super().get_queryset().filter(archived=True) - - -class ActiveProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "products" - - def get_queryset(self): - return super().get_queryset().filter(archived=False) - - -class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = Product - form_class = ProductEditForm - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = Product - form_class = ProductEditForm - pk_url_kwarg = "product_id" - template_name = "core/edit.jinja" - current_tab = "products" - - -class RefillingDeleteView(DeleteView): - """Delete a refilling (for the admins).""" - - model = Refilling - pk_url_kwarg = "refilling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class SellingDeleteView(DeleteView): - """Delete a selling (for the admins).""" - - model = Selling - pk_url_kwarg = "selling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -# Cash register summaries - class CashRegisterSummaryForm(forms.Form): """Provide the cash summary form.""" @@ -1115,43 +242,6 @@ class CashRegisterSummaryForm(forms.Form): summary.delete() -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 CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): """Provide the cash summary form.""" @@ -1193,54 +283,6 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): return kwargs -class CounterActivityView(DetailView): - """Show the bar activity.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/activity.jinja" - - -class CounterStatView(DetailView, CounterAdminMixin): - """Show the bar stats.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/stats.jinja" - - def get_context_data(self, **kwargs): - """Add stats to the context.""" - counter: Counter = self.object - semester_start = get_start_of_semester() - office_hours = counter.get_top_barmen() - kwargs = super().get_context_data(**kwargs) - kwargs.update( - { - "counter": counter, - "current_semester": get_semester_code(), - "total_sellings": counter.get_total_sales(since=semester_start), - "top_customers": counter.get_top_customers(since=semester_start)[:100], - "top_barman": office_hours[:100], - "top_barman_semester": ( - office_hours.filter(start__gt=semester_start)[:100] - ), - } - ) - return kwargs - - def dispatch(self, request, *args, **kwargs): - try: - return super().dispatch(request, *args, **kwargs) - except PermissionDenied: - if ( - request.user.is_root - or request.user.is_board_member - or self.get_object().is_owned_by(request.user) - ): - return super(CanEditMixin, self).dispatch(request, *args, **kwargs) - raise PermissionDenied - - class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): """Edit cash summaries.""" @@ -1310,228 +352,3 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): ) kwargs["refilling_sums"][c.name] = sum([s.amount for s in refillings.all()]) return kwargs - - -class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): - template_name = "counter/invoices_call.jinja" - current_tab = "invoices_call" - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC") - if "month" in self.request.GET: - start_date = datetime.strptime(self.request.GET["month"], "%Y-%m") - else: - start_date = datetime( - year=timezone.now().year, - month=(timezone.now().month + 10) % 12 + 1, - day=1, - ) - start_date = start_date.replace(tzinfo=tz.utc) - end_date = (start_date + timedelta(days=32)).replace( - day=1, hour=0, minute=0, microsecond=0 - ) - from django.db.models import Case, F, Sum, When - - kwargs["sum_cb"] = sum( - [ - r.amount - for r in Refilling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["sum_cb"] += sum( - [ - s.quantity * s.unit_price - for s in Selling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["start_date"] = start_date - kwargs["sums"] = ( - Selling.objects.values("club__name") - .annotate( - selling_sum=Sum( - Case( - When( - date__gte=start_date, - date__lt=end_date, - then=F("unit_price") * F("quantity"), - ), - output_field=CurrencyField(), - ) - ) - ) - .exclude(selling_sum=None) - .order_by("-selling_sum") - ) - return kwargs - - -class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = Eticket - template_name = "counter/eticket_list.jinja" - ordering = ["id"] - current_tab = "etickets" - - -class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create an eticket.""" - - model = Eticket - template_name = "core/create.jinja" - form_class = EticketForm - current_tab = "etickets" - - -class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit an eticket.""" - - model = Eticket - template_name = "core/edit.jinja" - form_class = EticketForm - pk_url_kwarg = "eticket_id" - current_tab = "etickets" - - -class EticketPDFView(CanViewMixin, DetailView): - """Display the PDF of an eticket.""" - - model = Selling - pk_url_kwarg = "selling_id" - - def get(self, request, *args, **kwargs): - from reportlab.graphics import renderPDF - from reportlab.graphics.barcode.qr import QrCodeWidget - from reportlab.graphics.shapes import Drawing - from reportlab.lib.units import cm - from reportlab.lib.utils import ImageReader - from reportlab.pdfgen import canvas - - if not ( - hasattr(self.object, "product") and hasattr(self.object.product, "eticket") - ): - raise Http404 - - eticket = self.object.product.eticket - user = self.object.customer.user - code = "%s %s %s %s" % ( - self.object.customer.user.id, - self.object.product.id, - self.object.id, - self.object.quantity, - ) - code += " " + eticket.get_hash(code)[:8].upper() - response = HttpResponse(content_type="application/pdf") - response["Content-Disposition"] = 'filename="eticket.pdf"' - p = canvas.Canvas(response) - p.setTitle("Eticket") - im = ImageReader("core/static/core/img/eticket.jpg") - width, height = im.getSize() - size = max(width, height) - width = 8 * cm * width / size - height = 8 * cm * height / size - p.drawImage(im, 10 * cm, 25 * cm, width, height) - if eticket.banner: - im = ImageReader(eticket.banner) - width, height = im.getSize() - size = max(width, height) - width = 6 * cm * width / size - height = 6 * cm * height / size - p.drawImage(im, 1 * cm, 25 * cm, width, height) - if user.profile_pict: - im = ImageReader(user.profile_pict.file) - width, height = im.getSize() - size = max(width, height) - width = 150 * width / size - height = 150 * height / size - p.drawImage(im, 10.5 * cm - width / 2, 16 * cm, width, height) - if eticket.event_title: - p.setFont("Helvetica-Bold", 20) - p.drawCentredString(10.5 * cm, 23.6 * cm, eticket.event_title) - if eticket.event_date: - p.setFont("Helvetica-Bold", 16) - p.drawCentredString( - 10.5 * cm, 22.6 * cm, eticket.event_date.strftime("%d %b %Y") - ) # FIXME with a locale - p.setFont("Helvetica-Bold", 14) - p.drawCentredString( - 10.5 * cm, - 15 * cm, - "%s : %d %s" - % (user.get_display_name(), self.object.quantity, str(_("people(s)"))), - ) - p.setFont("Courier-Bold", 14) - qrcode = QrCodeWidget(code) - bounds = qrcode.getBounds() - width = bounds[2] - bounds[0] - height = bounds[3] - bounds[1] - d = Drawing(260, 260, transform=[260.0 / width, 0, 0, 260.0 / height, 0, 0]) - d.add(qrcode) - renderPDF.draw(d, p, 10.5 * cm - 130, 6.1 * cm) - p.drawCentredString(10.5 * cm, 6 * cm, code) - - partners = ImageReader("core/static/core/img/partners.png") - width, height = partners.getSize() - size = max(width, height) - width = width * 2 / 3 - height = height * 2 / 3 - p.drawImage(partners, 0 * cm, 0 * cm, width, height) - - p.showPage() - p.save() - return response - - -class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """List of refillings on a counter.""" - - model = Refilling - template_name = "counter/refilling_list.jinja" - current_tab = "counters" - paginate_by = 30 - - def dispatch(self, request, *args, **kwargs): - self.counter = get_object_or_404(Counter, pk=kwargs["counter_id"]) - self.queryset = Refilling.objects.filter(counter__id=self.counter.id) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["counter"] = self.counter - return kwargs - - -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} - ) From 08286254cdc49d8801f66aede95638deaea8fef5 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:47:03 +0100 Subject: [PATCH 12/53] extract eticket views --- counter/{views.py => views/eticket.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/{views.py => views/eticket.py} (100%) diff --git a/counter/views.py b/counter/views/eticket.py similarity index 100% rename from counter/views.py rename to counter/views/eticket.py From d2b19424ff28edb14a06b718dc420076042fa9a3 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:47:18 +0100 Subject: [PATCH 13/53] extract eticket views --- counter/views/eticket.py | 1409 +------------------------------------- 1 file changed, 6 insertions(+), 1403 deletions(-) diff --git a/counter/views/eticket.py b/counter/views/eticket.py index 9483d335..56757fc2 100644 --- a/counter/views/eticket.py +++ b/counter/views/eticket.py @@ -12,1369 +12,15 @@ # 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.http import Http404, HttpResponse 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 django.views.generic import DetailView, ListView +from django.views.generic.edit import CreateView, 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 CounterAdminMixin(View): - """Protect counter admin section.""" - - edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] - edit_club = [] - - def _test_group(self, user): - return any(user.is_in_group(pk=grp_id) for grp_id in self.edit_group) - - def _test_club(self, user): - return any(c.can_be_edited_by(user) for c in self.edit_club) - - def dispatch(self, request, *args, **kwargs): - if not ( - request.user.is_root - or self._test_group(request.user) - or self._test_club(request.user) - ): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - -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 CounterTabsMixin(TabedViewMixin): - def get_tabs_title(self): - return self.object - - def get_list_of_tabs(self): - tab_list = [ - { - "url": reverse_lazy( - "counter:details", kwargs={"counter_id": self.object.id} - ), - "slug": "counter", - "name": _("Counter"), - } - ] - if self.object.type == "BAR": - tab_list.append( - { - "url": reverse_lazy( - "counter:cash_summary", kwargs={"counter_id": self.object.id} - ), - "slug": "cash_summary", - "name": _("Cash summary"), - } - ) - tab_list.append( - { - "url": reverse_lazy( - "counter:last_ops", kwargs={"counter_id": self.object.id} - ), - "slug": "last_ops", - "name": _("Last operations"), - } - ) - return tab_list - - -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) - - -class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): - """The click view - This is a detail view not to have to worry about loading the counter - Everything is made by hand in the post method. - """ - - model = Counter - queryset = Counter.objects.annotate_is_open() - template_name = "counter/counter_click.jinja" - pk_url_kwarg = "counter_id" - current_tab = "counter" - - def render_to_response(self, *args, **kwargs): - if self.is_ajax(self.request): - response = {"errors": []} - status = HTTPStatus.OK - - if self.request.session["too_young"]: - response["errors"].append(_("Too young for that product")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_allowed"]: - response["errors"].append(_("Not allowed for that product")) - status = HTTPStatus.FORBIDDEN - if self.request.session["no_age"]: - response["errors"].append(_("No date of birth provided")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_enough"]: - response["errors"].append(_("Not enough money")) - status = HTTPStatus.PAYMENT_REQUIRED - - if len(response["errors"]) > 1: - status = HTTPStatus.BAD_REQUEST - - response["basket"] = self.request.session["basket"] - - return JsonResponse(response, status=status) - - else: # Standard HTML page - return super().render_to_response(*args, **kwargs) - - def dispatch(self, request, *args, **kwargs): - self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) - obj: Counter = self.get_object() - if not self.customer.can_buy: - raise Http404 - if obj.type != "BAR" and not request.user.is_authenticated: - raise PermissionDenied - if obj.type == "BAR" and ( - "counter_token" not in request.session - or request.session["counter_token"] != obj.token - or len(obj.barmen_list) == 0 - ): - return redirect(obj) - return super().dispatch(request, *args, **kwargs) - - def get(self, request, *args, **kwargs): - """Simple get view.""" - if "basket" not in request.session: # Init the basket session entry - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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 - ): # Check that at least one barman is logged in - ret = self.cancel(request) # Otherwise, go to main view - return ret - - 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 - return self.cancel(request) - if self.object.type == "BAR" and not ( - "counter_token" in self.request.session - and self.request.session["counter_token"] == self.object.token - ): # Also 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" - ) - if "basket" not in request.session: - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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(): - self.operator = self.customer.user - else: - self.operator = self.object.get_random_barman() - action = self.request.POST.get("action", None) - if action is None: - 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": - self.refill(request) - elif action == "code": - return self.parse_code(request) - elif action == "cancel": - return self.cancel(request) - elif action == "finish": - return self.finish(request) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def customer_is_barman(self) -> bool: - barmen = self.object.barmen_list - return self.object.type == "BAR" and self.customer.user in barmen - - def get_product(self, pid): - return Product.objects.filter(pk=int(pid)).first() - - def get_price(self, pid): - p = self.get_product(pid) - if self.customer_is_barman(): - price = p.special_selling_price - else: - price = p.selling_price - return price - - def sum_basket(self, request): - total = 0 - for infos in request.session["basket"].values(): - total += infos["price"] * infos["qty"] - return total / 100 - - def get_total_quantity_for_pid(self, request, pid): - pid = str(pid) - if pid not in request.session["basket"]: - return 0 - return ( - request.session["basket"][pid]["qty"] - + request.session["basket"][pid]["bonus_qty"] - ) - - def compute_record_product(self, request, product=None): - recorded = 0 - basket = request.session["basket"] - - if product: - if product.is_record_product: - recorded -= 1 - elif product.is_unrecord_product: - recorded += 1 - - for p in basket: - bproduct = self.get_product(str(p)) - if bproduct.is_record_product: - recorded -= basket[p]["qty"] - elif bproduct.is_unrecord_product: - recorded += basket[p]["qty"] - return recorded - - def is_record_product_ok(self, request, product): - return self.customer.can_record_more( - self.compute_record_product(request, product) - ) - - @staticmethod - def is_ajax(request): - # when using the fetch API, the django request.POST dict is empty - # this is but a wretched contrivance which strive to replace - # the deprecated django is_ajax() method - # and which must be replaced as soon as possible - # by a proper separation between the api endpoints of the counter - return len(request.POST) == 0 and len(request.body) != 0 - - def add_product(self, request, q=1, p=None): - """Add a product to the basket - q is the quantity passed as integer - p is the product id, passed as an integer. - """ - pid = p or parse_qs(request.body.decode())["product_id"][0] - pid = str(pid) - price = self.get_price(pid) - total = self.sum_basket(request) - product: Product = self.get_product(pid) - user: User = self.customer.user - buying_groups = list(product.buying_groups.values_list("pk", flat=True)) - can_buy = len(buying_groups) == 0 or any( - user.is_in_group(pk=group_id) for group_id in buying_groups - ) - if not can_buy: - request.session["not_allowed"] = True - return False - bq = 0 # Bonus quantity, for trays - if ( - product.tray - ): # Handle the tray to adjust the quantity q to add and the bonus quantity bq - total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6 - bq = int((total_qty_mod_6 + q) / 6) # Integer division - q -= bq - if self.customer.amount < ( - total + round(q * float(price), 2) - ): # Check for enough money - request.session["not_enough"] = True - return False - if product.is_unrecord_product and not self.is_record_product_ok( - request, product - ): - request.session["not_allowed"] = True - return False - if product.limit_age >= 18 and not user.date_of_birth: - request.session["no_age"] = True - return False - if product.limit_age >= 18 and user.is_banned_alcohol: - request.session["not_allowed"] = True - return False - if user.is_banned_counter: - request.session["not_allowed"] = True - return False - if ( - user.date_of_birth and self.customer.user.get_age() < product.limit_age - ): # Check if affordable - request.session["too_young"] = True - return False - if pid in request.session["basket"]: # Add if already in basket - request.session["basket"][pid]["qty"] += q - request.session["basket"][pid]["bonus_qty"] += bq - else: # or create if not - request.session["basket"][pid] = { - "qty": q, - "price": int(price * 100), - "bonus_qty": bq, - } - 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] - product = self.get_product(pid) - if pid in request.session["basket"]: - if ( - product.tray - and (self.get_total_quantity_for_pid(request, pid) % 6 == 0) - and request.session["basket"][pid]["bonus_qty"] - ): - request.session["basket"][pid]["bonus_qty"] -= 1 - else: - request.session["basket"][pid]["qty"] -= 1 - if request.session["basket"][pid]["qty"] <= 0: - del request.session["basket"][pid] - request.session.modified = True - - def parse_code(self, request): - """Parse the string entered by the barman. - - This can be of two forms : - - ``, where the string is the code of the product - - `X`, where the integer is the quantity and str the code. - """ - string = parse_qs(request.body.decode()).get("code", [""])[0].upper() - if string == "FIN": - return self.finish(request) - elif string == "ANN": - return self.cancel(request) - regex = re.compile(r"^((?P[0-9]+)X)?(?P[A-Z0-9]+)$") - m = regex.match(string) - if m is not None: - nb = m.group("nb") - code = m.group("code") - nb = int(nb) if nb is not None else 1 - p = self.object.products.filter(code=code).first() - if p is not None: - self.add_product(request, nb, p.id) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def finish(self, request): - """Finish the click session, and validate the basket.""" - with transaction.atomic(): - request.session["last_basket"] = [] - if self.sum_basket(request) > self.customer.amount: - raise DataError(_("You have not enough money to buy all the basket")) - - for pid, infos in request.session["basket"].items(): - # This duplicates code for DB optimization (prevent to load many times the same object) - p = Product.objects.filter(pk=pid).first() - if self.customer_is_barman(): - uprice = p.special_selling_price - else: - uprice = p.selling_price - request.session["last_basket"].append( - "%d x %s" % (infos["qty"] + infos["bonus_qty"], p.name) - ) - s = Selling( - label=p.name, - product=p, - club=p.club, - counter=self.object, - unit_price=uprice, - quantity=infos["qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - if infos["bonus_qty"]: - s = Selling( - label=p.name + " (Plateau)", - product=p, - club=p.club, - counter=self.object, - unit_price=0, - quantity=infos["bonus_qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - self.customer.recorded_products -= self.compute_record_product(request) - self.customer.save() - request.session["last_customer"] = self.customer.user.get_display_name() - request.session["last_total"] = "%0.2f" % self.sum_basket(request) - request.session["new_customer_amount"] = str(self.customer.amount) - del request.session["basket"] - request.session.modified = True - kwargs = {"counter_id": self.object.id} - return HttpResponseRedirect( - reverse_lazy("counter:details", args=self.args, kwargs=kwargs) - ) - - def cancel(self, request): - """Cancel the click session.""" - kwargs = {"counter_id": self.object.id} - request.session.pop("basket", None) - return HttpResponseRedirect( - 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) - products = self.object.products.select_related("product_type") - if self.customer_is_barman(): - products = products.annotate(price=F("special_selling_price")) - else: - products = products.annotate(price=F("selling_price")) - kwargs["products"] = products - kwargs["categories"] = {} - for product in kwargs["products"]: - if product.product_type: - kwargs["categories"].setdefault(product.product_type, []).append( - 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 - - -@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) - - -# Counter admin views - - -class CounterAdminTabsMixin(TabedViewMixin): - tabs_title = _("Counter administration") - list_of_tabs = [ - { - "url": reverse_lazy("counter:admin_list"), - "slug": "counters", - "name": _("Counters"), - }, - { - "url": reverse_lazy("counter:product_list"), - "slug": "products", - "name": _("Products"), - }, - { - "url": reverse_lazy("counter:product_list_archived"), - "slug": "archive", - "name": _("Archived products"), - }, - { - "url": reverse_lazy("counter:producttype_list"), - "slug": "product_types", - "name": _("Product types"), - }, - { - "url": reverse_lazy("counter:cash_summary_list"), - "slug": "cash_summary", - "name": _("Cash register summaries"), - }, - { - "url": reverse_lazy("counter:invoices_call"), - "slug": "invoices_call", - "name": _("Invoices call"), - }, - { - "url": reverse_lazy("counter:eticket_list"), - "slug": "etickets", - "name": _("Etickets"), - }, - ] - - -class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): - """A list view for the admins.""" - - model = Counter - template_name = "counter/counter_list.jinja" - current_tab = "counters" - - -class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's manager).""" - - model = Counter - form_class = CounterEditForm - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - def dispatch(self, request, *args, **kwargs): - obj = self.get_object() - self.edit_club.append(obj.club) - return super().dispatch(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:admin", kwargs={"counter_id": self.object.id}) - - -class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's admin).""" - - model = Counter - form_class = modelform_factory(Counter, fields=["name", "club", "type"]) - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - -class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create a counter (for the admins).""" - - model = Counter - form_class = modelform_factory( - Counter, - fields=["name", "club", "type", "products"], - widgets={"products": CheckboxSelectMultiple}, - ) - template_name = "core/create.jinja" - current_tab = "counters" - - -class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): - """Delete a counter (for the admins).""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "core/delete_confirm.jinja" - success_url = reverse_lazy("counter:admin_list") - current_tab = "counters" - - -# Product management - - -class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = ProductType - template_name = "counter/producttype_list.jinja" - current_tab = "product_types" - - -class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = ProductType - fields = ["name", "description", "comment", "icon", "priority"] - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = ProductType - template_name = "core/edit.jinja" - fields = ["name", "description", "comment", "icon", "priority"] - pk_url_kwarg = "type_id" - current_tab = "products" - - -class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - model = Product - queryset = Product.objects.values("id", "name", "code", "product_type__name") - template_name = "counter/product_list.jinja" - ordering = [ - F("product_type__priority").desc(nulls_last=True), - "product_type", - "name", - ] - - def get_context_data(self, **kwargs): - res = super().get_context_data(**kwargs) - res["object_list"] = itertools.groupby( - res["object_list"], key=itemgetter("product_type__name") - ) - return res - - -class ArchivedProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "archive" - - def get_queryset(self): - return super().get_queryset().filter(archived=True) - - -class ActiveProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "products" - - def get_queryset(self): - return super().get_queryset().filter(archived=False) - - -class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = Product - form_class = ProductEditForm - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = Product - form_class = ProductEditForm - pk_url_kwarg = "product_id" - template_name = "core/edit.jinja" - current_tab = "products" - - -class RefillingDeleteView(DeleteView): - """Delete a refilling (for the admins).""" - - model = Refilling - pk_url_kwarg = "refilling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class SellingDeleteView(DeleteView): - """Delete a selling (for the admins).""" - - model = Selling - pk_url_kwarg = "selling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -# Cash register summaries - - -class CashRegisterSummaryForm(forms.Form): - """Provide the cash summary form.""" - - ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0) - twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0) - fifty_cents = forms.IntegerField(label=_("50 cents"), required=False, min_value=0) - one_euro = forms.IntegerField(label=_("1 euro"), required=False, min_value=0) - two_euros = forms.IntegerField(label=_("2 euros"), required=False, min_value=0) - five_euros = forms.IntegerField(label=_("5 euros"), required=False, min_value=0) - ten_euros = forms.IntegerField(label=_("10 euros"), required=False, min_value=0) - twenty_euros = forms.IntegerField(label=_("20 euros"), required=False, min_value=0) - fifty_euros = forms.IntegerField(label=_("50 euros"), required=False, min_value=0) - hundred_euros = forms.IntegerField( - label=_("100 euros"), required=False, min_value=0 - ) - check_1_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_1_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_2_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_2_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_3_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_3_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_4_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_4_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_5_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_5_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - comment = forms.CharField(label=_("Comment"), required=False) - emptied = forms.BooleanField(label=_("Emptied"), required=False) - - def __init__(self, *args, **kwargs): - instance = kwargs.pop("instance", None) - super().__init__(*args, **kwargs) - if instance: - self.fields["ten_cents"].initial = ( - instance.ten_cents.quantity if instance.ten_cents else 0 - ) - self.fields["twenty_cents"].initial = ( - instance.twenty_cents.quantity if instance.twenty_cents else 0 - ) - self.fields["fifty_cents"].initial = ( - instance.fifty_cents.quantity if instance.fifty_cents else 0 - ) - self.fields["one_euro"].initial = ( - instance.one_euro.quantity if instance.one_euro else 0 - ) - self.fields["two_euros"].initial = ( - instance.two_euros.quantity if instance.two_euros else 0 - ) - self.fields["five_euros"].initial = ( - instance.five_euros.quantity if instance.five_euros else 0 - ) - self.fields["ten_euros"].initial = ( - instance.ten_euros.quantity if instance.ten_euros else 0 - ) - self.fields["twenty_euros"].initial = ( - instance.twenty_euros.quantity if instance.twenty_euros else 0 - ) - self.fields["fifty_euros"].initial = ( - instance.fifty_euros.quantity if instance.fifty_euros else 0 - ) - self.fields["hundred_euros"].initial = ( - instance.hundred_euros.quantity if instance.hundred_euros else 0 - ) - self.fields["check_1_quantity"].initial = ( - instance.check_1.quantity if instance.check_1 else 0 - ) - self.fields["check_2_quantity"].initial = ( - instance.check_2.quantity if instance.check_2 else 0 - ) - self.fields["check_3_quantity"].initial = ( - instance.check_3.quantity if instance.check_3 else 0 - ) - self.fields["check_4_quantity"].initial = ( - instance.check_4.quantity if instance.check_4 else 0 - ) - self.fields["check_5_quantity"].initial = ( - instance.check_5.quantity if instance.check_5 else 0 - ) - self.fields["check_1_value"].initial = ( - instance.check_1.value if instance.check_1 else 0 - ) - self.fields["check_2_value"].initial = ( - instance.check_2.value if instance.check_2 else 0 - ) - self.fields["check_3_value"].initial = ( - instance.check_3.value if instance.check_3 else 0 - ) - self.fields["check_4_value"].initial = ( - instance.check_4.value if instance.check_4 else 0 - ) - self.fields["check_5_value"].initial = ( - instance.check_5.value if instance.check_5 else 0 - ) - self.fields["comment"].initial = instance.comment - self.fields["emptied"].initial = instance.emptied - self.instance = instance - else: - self.instance = None - - def save(self, counter=None): - cd = self.cleaned_data - summary = self.instance or CashRegisterSummary( - counter=counter, user=counter.get_random_barman() - ) - summary.comment = cd["comment"] - summary.emptied = cd["emptied"] - summary.save() - summary.items.all().delete() - # Cash - if cd["ten_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.1, quantity=cd["ten_cents"] - ).save() - if cd["twenty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.2, quantity=cd["twenty_cents"] - ).save() - if cd["fifty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.5, quantity=cd["fifty_cents"] - ).save() - if cd["one_euro"]: - CashRegisterSummaryItem( - cash_summary=summary, value=1, quantity=cd["one_euro"] - ).save() - if cd["two_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=2, quantity=cd["two_euros"] - ).save() - if cd["five_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=5, quantity=cd["five_euros"] - ).save() - if cd["ten_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=10, quantity=cd["ten_euros"] - ).save() - if cd["twenty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=20, quantity=cd["twenty_euros"] - ).save() - if cd["fifty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=50, quantity=cd["fifty_euros"] - ).save() - if cd["hundred_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=100, quantity=cd["hundred_euros"] - ).save() - # Checks - if cd["check_1_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_1_value"], - quantity=cd["check_1_quantity"], - is_check=True, - ).save() - if cd["check_2_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_2_value"], - quantity=cd["check_2_quantity"], - is_check=True, - ).save() - if cd["check_3_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_3_value"], - quantity=cd["check_3_quantity"], - is_check=True, - ).save() - if cd["check_4_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_4_value"], - quantity=cd["check_4_quantity"], - is_check=True, - ).save() - if cd["check_5_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_5_value"], - quantity=cd["check_5_quantity"], - is_check=True, - ).save() - if summary.items.count() < 1: - summary.delete() - - -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 CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): - """Provide the cash summary form.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/cash_register_summary.jinja" - current_tab = "cash_summary" - - 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(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm() - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm(request.POST) - if self.form.is_valid(): - self.form.save(self.object) - return HttpResponseRedirect(self.get_success_url()) - return super().get(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id}) - - def get_context_data(self, **kwargs): - """Add form to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["form"] = self.form - return kwargs - - -class CounterActivityView(DetailView): - """Show the bar activity.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/activity.jinja" - - -class CounterStatView(DetailView, CounterAdminMixin): - """Show the bar stats.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/stats.jinja" - - def get_context_data(self, **kwargs): - """Add stats to the context.""" - counter: Counter = self.object - semester_start = get_start_of_semester() - office_hours = counter.get_top_barmen() - kwargs = super().get_context_data(**kwargs) - kwargs.update( - { - "counter": counter, - "current_semester": get_semester_code(), - "total_sellings": counter.get_total_sales(since=semester_start), - "top_customers": counter.get_top_customers(since=semester_start)[:100], - "top_barman": office_hours[:100], - "top_barman_semester": ( - office_hours.filter(start__gt=semester_start)[:100] - ), - } - ) - return kwargs - - def dispatch(self, request, *args, **kwargs): - try: - return super().dispatch(request, *args, **kwargs) - except PermissionDenied: - if ( - request.user.is_root - or request.user.is_board_member - or self.get_object().is_owned_by(request.user) - ): - return super(CanEditMixin, self).dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_register_summary.jinja" - context_object_name = "cashsummary" - pk_url_kwarg = "cashsummary_id" - form_class = CashRegisterSummaryForm - current_tab = "cash_summary" - - def get_success_url(self): - return reverse("counter:cash_summary_list") - - -class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """Display a list of cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_summary_list.jinja" - context_object_name = "cashsummary_list" - current_tab = "cash_summary" - queryset = CashRegisterSummary.objects.all().order_by("-date") - paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - form = CashSummaryFormBase(self.request.GET) - kwargs["form"] = form - kwargs["summaries_sums"] = {} - kwargs["refilling_sums"] = {} - for c in Counter.objects.filter(type="BAR").all(): - refillings = Refilling.objects.filter(counter=c) - cashredistersummaries = CashRegisterSummary.objects.filter(counter=c) - if form.is_valid() and form.cleaned_data["begin_date"]: - refillings = refillings.filter( - date__gte=form.cleaned_data["begin_date"] - ) - cashredistersummaries = cashredistersummaries.filter( - date__gte=form.cleaned_data["begin_date"] - ) - else: - last_summary = ( - CashRegisterSummary.objects.filter(counter=c, emptied=True) - .order_by("-date") - .first() - ) - if last_summary: - refillings = refillings.filter(date__gt=last_summary.date) - cashredistersummaries = cashredistersummaries.filter( - date__gt=last_summary.date - ) - else: - refillings = refillings.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) # My birth date should be old enough - cashredistersummaries = cashredistersummaries.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) - if form.is_valid() and form.cleaned_data["end_date"]: - refillings = refillings.filter(date__lte=form.cleaned_data["end_date"]) - cashredistersummaries = cashredistersummaries.filter( - date__lte=form.cleaned_data["end_date"] - ) - kwargs["summaries_sums"][c.name] = sum( - [s.get_total() for s in cashredistersummaries.all()] - ) - kwargs["refilling_sums"][c.name] = sum([s.amount for s in refillings.all()]) - return kwargs - - -class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): - template_name = "counter/invoices_call.jinja" - current_tab = "invoices_call" - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC") - if "month" in self.request.GET: - start_date = datetime.strptime(self.request.GET["month"], "%Y-%m") - else: - start_date = datetime( - year=timezone.now().year, - month=(timezone.now().month + 10) % 12 + 1, - day=1, - ) - start_date = start_date.replace(tzinfo=tz.utc) - end_date = (start_date + timedelta(days=32)).replace( - day=1, hour=0, minute=0, microsecond=0 - ) - from django.db.models import Case, F, Sum, When - - kwargs["sum_cb"] = sum( - [ - r.amount - for r in Refilling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["sum_cb"] += sum( - [ - s.quantity * s.unit_price - for s in Selling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["start_date"] = start_date - kwargs["sums"] = ( - Selling.objects.values("club__name") - .annotate( - selling_sum=Sum( - Case( - When( - date__gte=start_date, - date__lt=end_date, - then=F("unit_price") * F("quantity"), - ), - output_field=CurrencyField(), - ) - ) - ) - .exclude(selling_sum=None) - .order_by("-selling_sum") - ) - return kwargs +from core.views import CanViewMixin +from counter.forms import EticketForm +from counter.models import Eticket, Selling class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): @@ -1492,46 +138,3 @@ class EticketPDFView(CanViewMixin, DetailView): p.showPage() p.save() return response - - -class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """List of refillings on a counter.""" - - model = Refilling - template_name = "counter/refilling_list.jinja" - current_tab = "counters" - paginate_by = 30 - - def dispatch(self, request, *args, **kwargs): - self.counter = get_object_or_404(Counter, pk=kwargs["counter_id"]) - self.queryset = Refilling.objects.filter(counter__id=self.counter.id) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["counter"] = self.counter - return kwargs - - -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} - ) From 5e4ebd16f948d8b4cdec912e10fa31ab83fc5e8d Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:47:55 +0100 Subject: [PATCH 14/53] extract mixins views --- counter/{views.py => views/mixins.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/{views.py => views/mixins.py} (100%) diff --git a/counter/views.py b/counter/views/mixins.py similarity index 100% rename from counter/views.py rename to counter/views/mixins.py From d0ff9bc16c2fba1a37b35b6d82d66e5d1c957113 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 18:48:06 +0100 Subject: [PATCH 15/53] extract mixins views --- counter/views/mixins.py | 1420 +-------------------------------------- 1 file changed, 2 insertions(+), 1418 deletions(-) diff --git a/counter/views/mixins.py b/counter/views/mixins.py index 9483d335..4a07d848 100644 --- a/counter/views/mixins.py +++ b/counter/views/mixins.py @@ -12,76 +12,14 @@ # 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.urls import reverse_lazy 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 +from core.views import TabedViewMixin class CounterAdminMixin(View): @@ -106,23 +44,6 @@ class CounterAdminMixin(View): return super().dispatch(request, *args, **kwargs) -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 CounterTabsMixin(TabedViewMixin): def get_tabs_title(self): return self.object @@ -159,516 +80,6 @@ class CounterTabsMixin(TabedViewMixin): return tab_list -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) - - -class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): - """The click view - This is a detail view not to have to worry about loading the counter - Everything is made by hand in the post method. - """ - - model = Counter - queryset = Counter.objects.annotate_is_open() - template_name = "counter/counter_click.jinja" - pk_url_kwarg = "counter_id" - current_tab = "counter" - - def render_to_response(self, *args, **kwargs): - if self.is_ajax(self.request): - response = {"errors": []} - status = HTTPStatus.OK - - if self.request.session["too_young"]: - response["errors"].append(_("Too young for that product")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_allowed"]: - response["errors"].append(_("Not allowed for that product")) - status = HTTPStatus.FORBIDDEN - if self.request.session["no_age"]: - response["errors"].append(_("No date of birth provided")) - status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS - if self.request.session["not_enough"]: - response["errors"].append(_("Not enough money")) - status = HTTPStatus.PAYMENT_REQUIRED - - if len(response["errors"]) > 1: - status = HTTPStatus.BAD_REQUEST - - response["basket"] = self.request.session["basket"] - - return JsonResponse(response, status=status) - - else: # Standard HTML page - return super().render_to_response(*args, **kwargs) - - def dispatch(self, request, *args, **kwargs): - self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) - obj: Counter = self.get_object() - if not self.customer.can_buy: - raise Http404 - if obj.type != "BAR" and not request.user.is_authenticated: - raise PermissionDenied - if obj.type == "BAR" and ( - "counter_token" not in request.session - or request.session["counter_token"] != obj.token - or len(obj.barmen_list) == 0 - ): - return redirect(obj) - return super().dispatch(request, *args, **kwargs) - - def get(self, request, *args, **kwargs): - """Simple get view.""" - if "basket" not in request.session: # Init the basket session entry - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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 - ): # Check that at least one barman is logged in - ret = self.cancel(request) # Otherwise, go to main view - return ret - - 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 - return self.cancel(request) - if self.object.type == "BAR" and not ( - "counter_token" in self.request.session - and self.request.session["counter_token"] == self.object.token - ): # Also 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" - ) - if "basket" not in request.session: - request.session["basket"] = {} - request.session["basket_total"] = 0 - request.session["not_enough"] = False # Reset every variable - 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(): - self.operator = self.customer.user - else: - self.operator = self.object.get_random_barman() - action = self.request.POST.get("action", None) - if action is None: - 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": - self.refill(request) - elif action == "code": - return self.parse_code(request) - elif action == "cancel": - return self.cancel(request) - elif action == "finish": - return self.finish(request) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def customer_is_barman(self) -> bool: - barmen = self.object.barmen_list - return self.object.type == "BAR" and self.customer.user in barmen - - def get_product(self, pid): - return Product.objects.filter(pk=int(pid)).first() - - def get_price(self, pid): - p = self.get_product(pid) - if self.customer_is_barman(): - price = p.special_selling_price - else: - price = p.selling_price - return price - - def sum_basket(self, request): - total = 0 - for infos in request.session["basket"].values(): - total += infos["price"] * infos["qty"] - return total / 100 - - def get_total_quantity_for_pid(self, request, pid): - pid = str(pid) - if pid not in request.session["basket"]: - return 0 - return ( - request.session["basket"][pid]["qty"] - + request.session["basket"][pid]["bonus_qty"] - ) - - def compute_record_product(self, request, product=None): - recorded = 0 - basket = request.session["basket"] - - if product: - if product.is_record_product: - recorded -= 1 - elif product.is_unrecord_product: - recorded += 1 - - for p in basket: - bproduct = self.get_product(str(p)) - if bproduct.is_record_product: - recorded -= basket[p]["qty"] - elif bproduct.is_unrecord_product: - recorded += basket[p]["qty"] - return recorded - - def is_record_product_ok(self, request, product): - return self.customer.can_record_more( - self.compute_record_product(request, product) - ) - - @staticmethod - def is_ajax(request): - # when using the fetch API, the django request.POST dict is empty - # this is but a wretched contrivance which strive to replace - # the deprecated django is_ajax() method - # and which must be replaced as soon as possible - # by a proper separation between the api endpoints of the counter - return len(request.POST) == 0 and len(request.body) != 0 - - def add_product(self, request, q=1, p=None): - """Add a product to the basket - q is the quantity passed as integer - p is the product id, passed as an integer. - """ - pid = p or parse_qs(request.body.decode())["product_id"][0] - pid = str(pid) - price = self.get_price(pid) - total = self.sum_basket(request) - product: Product = self.get_product(pid) - user: User = self.customer.user - buying_groups = list(product.buying_groups.values_list("pk", flat=True)) - can_buy = len(buying_groups) == 0 or any( - user.is_in_group(pk=group_id) for group_id in buying_groups - ) - if not can_buy: - request.session["not_allowed"] = True - return False - bq = 0 # Bonus quantity, for trays - if ( - product.tray - ): # Handle the tray to adjust the quantity q to add and the bonus quantity bq - total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6 - bq = int((total_qty_mod_6 + q) / 6) # Integer division - q -= bq - if self.customer.amount < ( - total + round(q * float(price), 2) - ): # Check for enough money - request.session["not_enough"] = True - return False - if product.is_unrecord_product and not self.is_record_product_ok( - request, product - ): - request.session["not_allowed"] = True - return False - if product.limit_age >= 18 and not user.date_of_birth: - request.session["no_age"] = True - return False - if product.limit_age >= 18 and user.is_banned_alcohol: - request.session["not_allowed"] = True - return False - if user.is_banned_counter: - request.session["not_allowed"] = True - return False - if ( - user.date_of_birth and self.customer.user.get_age() < product.limit_age - ): # Check if affordable - request.session["too_young"] = True - return False - if pid in request.session["basket"]: # Add if already in basket - request.session["basket"][pid]["qty"] += q - request.session["basket"][pid]["bonus_qty"] += bq - else: # or create if not - request.session["basket"][pid] = { - "qty": q, - "price": int(price * 100), - "bonus_qty": bq, - } - 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] - product = self.get_product(pid) - if pid in request.session["basket"]: - if ( - product.tray - and (self.get_total_quantity_for_pid(request, pid) % 6 == 0) - and request.session["basket"][pid]["bonus_qty"] - ): - request.session["basket"][pid]["bonus_qty"] -= 1 - else: - request.session["basket"][pid]["qty"] -= 1 - if request.session["basket"][pid]["qty"] <= 0: - del request.session["basket"][pid] - request.session.modified = True - - def parse_code(self, request): - """Parse the string entered by the barman. - - This can be of two forms : - - ``, where the string is the code of the product - - `X`, where the integer is the quantity and str the code. - """ - string = parse_qs(request.body.decode()).get("code", [""])[0].upper() - if string == "FIN": - return self.finish(request) - elif string == "ANN": - return self.cancel(request) - regex = re.compile(r"^((?P[0-9]+)X)?(?P[A-Z0-9]+)$") - m = regex.match(string) - if m is not None: - nb = m.group("nb") - code = m.group("code") - nb = int(nb) if nb is not None else 1 - p = self.object.products.filter(code=code).first() - if p is not None: - self.add_product(request, nb, p.id) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) - - def finish(self, request): - """Finish the click session, and validate the basket.""" - with transaction.atomic(): - request.session["last_basket"] = [] - if self.sum_basket(request) > self.customer.amount: - raise DataError(_("You have not enough money to buy all the basket")) - - for pid, infos in request.session["basket"].items(): - # This duplicates code for DB optimization (prevent to load many times the same object) - p = Product.objects.filter(pk=pid).first() - if self.customer_is_barman(): - uprice = p.special_selling_price - else: - uprice = p.selling_price - request.session["last_basket"].append( - "%d x %s" % (infos["qty"] + infos["bonus_qty"], p.name) - ) - s = Selling( - label=p.name, - product=p, - club=p.club, - counter=self.object, - unit_price=uprice, - quantity=infos["qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - if infos["bonus_qty"]: - s = Selling( - label=p.name + " (Plateau)", - product=p, - club=p.club, - counter=self.object, - unit_price=0, - quantity=infos["bonus_qty"], - seller=self.operator, - customer=self.customer, - ) - s.save() - self.customer.recorded_products -= self.compute_record_product(request) - self.customer.save() - request.session["last_customer"] = self.customer.user.get_display_name() - request.session["last_total"] = "%0.2f" % self.sum_basket(request) - request.session["new_customer_amount"] = str(self.customer.amount) - del request.session["basket"] - request.session.modified = True - kwargs = {"counter_id": self.object.id} - return HttpResponseRedirect( - reverse_lazy("counter:details", args=self.args, kwargs=kwargs) - ) - - def cancel(self, request): - """Cancel the click session.""" - kwargs = {"counter_id": self.object.id} - request.session.pop("basket", None) - return HttpResponseRedirect( - 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) - products = self.object.products.select_related("product_type") - if self.customer_is_barman(): - products = products.annotate(price=F("special_selling_price")) - else: - products = products.annotate(price=F("selling_price")) - kwargs["products"] = products - kwargs["categories"] = {} - for product in kwargs["products"]: - if product.product_type: - kwargs["categories"].setdefault(product.product_type, []).append( - 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 - - -@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) - - -# Counter admin views - - class CounterAdminTabsMixin(TabedViewMixin): tabs_title = _("Counter administration") list_of_tabs = [ @@ -708,830 +119,3 @@ class CounterAdminTabsMixin(TabedViewMixin): "name": _("Etickets"), }, ] - - -class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): - """A list view for the admins.""" - - model = Counter - template_name = "counter/counter_list.jinja" - current_tab = "counters" - - -class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's manager).""" - - model = Counter - form_class = CounterEditForm - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - def dispatch(self, request, *args, **kwargs): - obj = self.get_object() - self.edit_club.append(obj.club) - return super().dispatch(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:admin", kwargs={"counter_id": self.object.id}) - - -class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit a counter's main informations (for the counter's admin).""" - - model = Counter - form_class = modelform_factory(Counter, fields=["name", "club", "type"]) - pk_url_kwarg = "counter_id" - template_name = "core/edit.jinja" - current_tab = "counters" - - -class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create a counter (for the admins).""" - - model = Counter - form_class = modelform_factory( - Counter, - fields=["name", "club", "type", "products"], - widgets={"products": CheckboxSelectMultiple}, - ) - template_name = "core/create.jinja" - current_tab = "counters" - - -class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView): - """Delete a counter (for the admins).""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "core/delete_confirm.jinja" - success_url = reverse_lazy("counter:admin_list") - current_tab = "counters" - - -# Product management - - -class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = ProductType - template_name = "counter/producttype_list.jinja" - current_tab = "product_types" - - -class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = ProductType - fields = ["name", "description", "comment", "icon", "priority"] - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = ProductType - template_name = "core/edit.jinja" - fields = ["name", "description", "comment", "icon", "priority"] - pk_url_kwarg = "type_id" - current_tab = "products" - - -class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - model = Product - queryset = Product.objects.values("id", "name", "code", "product_type__name") - template_name = "counter/product_list.jinja" - ordering = [ - F("product_type__priority").desc(nulls_last=True), - "product_type", - "name", - ] - - def get_context_data(self, **kwargs): - res = super().get_context_data(**kwargs) - res["object_list"] = itertools.groupby( - res["object_list"], key=itemgetter("product_type__name") - ) - return res - - -class ArchivedProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "archive" - - def get_queryset(self): - return super().get_queryset().filter(archived=True) - - -class ActiveProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "products" - - def get_queryset(self): - return super().get_queryset().filter(archived=False) - - -class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """A create view for the admins.""" - - model = Product - form_class = ProductEditForm - template_name = "core/create.jinja" - current_tab = "products" - - -class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """An edit view for the admins.""" - - model = Product - form_class = ProductEditForm - pk_url_kwarg = "product_id" - template_name = "core/edit.jinja" - current_tab = "products" - - -class RefillingDeleteView(DeleteView): - """Delete a refilling (for the admins).""" - - model = Refilling - pk_url_kwarg = "refilling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class SellingDeleteView(DeleteView): - """Delete a selling (for the admins).""" - - model = Selling - pk_url_kwarg = "selling_id" - template_name = "core/delete_confirm.jinja" - - def dispatch(self, request, *args, **kwargs): - """We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" - self.object = self.get_object() - if timezone.now() - self.object.date <= timedelta( - minutes=settings.SITH_LAST_OPERATIONS_LIMIT - ) and is_logged_in_counter(request): - self.success_url = reverse( - "counter:details", kwargs={"counter_id": self.object.counter.id} - ) - return super().dispatch(request, *args, **kwargs) - elif self.object.is_owned_by(request.user): - self.success_url = reverse( - "core:user_account", kwargs={"user_id": self.object.customer.user.id} - ) - return super().dispatch(request, *args, **kwargs) - raise PermissionDenied - - -# Cash register summaries - - -class CashRegisterSummaryForm(forms.Form): - """Provide the cash summary form.""" - - ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0) - twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0) - fifty_cents = forms.IntegerField(label=_("50 cents"), required=False, min_value=0) - one_euro = forms.IntegerField(label=_("1 euro"), required=False, min_value=0) - two_euros = forms.IntegerField(label=_("2 euros"), required=False, min_value=0) - five_euros = forms.IntegerField(label=_("5 euros"), required=False, min_value=0) - ten_euros = forms.IntegerField(label=_("10 euros"), required=False, min_value=0) - twenty_euros = forms.IntegerField(label=_("20 euros"), required=False, min_value=0) - fifty_euros = forms.IntegerField(label=_("50 euros"), required=False, min_value=0) - hundred_euros = forms.IntegerField( - label=_("100 euros"), required=False, min_value=0 - ) - check_1_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_1_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_2_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_2_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_3_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_3_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_4_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_4_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - check_5_value = forms.DecimalField( - label=_("Check amount"), required=False, min_value=0 - ) - check_5_quantity = forms.IntegerField( - label=_("Check quantity"), required=False, min_value=0 - ) - comment = forms.CharField(label=_("Comment"), required=False) - emptied = forms.BooleanField(label=_("Emptied"), required=False) - - def __init__(self, *args, **kwargs): - instance = kwargs.pop("instance", None) - super().__init__(*args, **kwargs) - if instance: - self.fields["ten_cents"].initial = ( - instance.ten_cents.quantity if instance.ten_cents else 0 - ) - self.fields["twenty_cents"].initial = ( - instance.twenty_cents.quantity if instance.twenty_cents else 0 - ) - self.fields["fifty_cents"].initial = ( - instance.fifty_cents.quantity if instance.fifty_cents else 0 - ) - self.fields["one_euro"].initial = ( - instance.one_euro.quantity if instance.one_euro else 0 - ) - self.fields["two_euros"].initial = ( - instance.two_euros.quantity if instance.two_euros else 0 - ) - self.fields["five_euros"].initial = ( - instance.five_euros.quantity if instance.five_euros else 0 - ) - self.fields["ten_euros"].initial = ( - instance.ten_euros.quantity if instance.ten_euros else 0 - ) - self.fields["twenty_euros"].initial = ( - instance.twenty_euros.quantity if instance.twenty_euros else 0 - ) - self.fields["fifty_euros"].initial = ( - instance.fifty_euros.quantity if instance.fifty_euros else 0 - ) - self.fields["hundred_euros"].initial = ( - instance.hundred_euros.quantity if instance.hundred_euros else 0 - ) - self.fields["check_1_quantity"].initial = ( - instance.check_1.quantity if instance.check_1 else 0 - ) - self.fields["check_2_quantity"].initial = ( - instance.check_2.quantity if instance.check_2 else 0 - ) - self.fields["check_3_quantity"].initial = ( - instance.check_3.quantity if instance.check_3 else 0 - ) - self.fields["check_4_quantity"].initial = ( - instance.check_4.quantity if instance.check_4 else 0 - ) - self.fields["check_5_quantity"].initial = ( - instance.check_5.quantity if instance.check_5 else 0 - ) - self.fields["check_1_value"].initial = ( - instance.check_1.value if instance.check_1 else 0 - ) - self.fields["check_2_value"].initial = ( - instance.check_2.value if instance.check_2 else 0 - ) - self.fields["check_3_value"].initial = ( - instance.check_3.value if instance.check_3 else 0 - ) - self.fields["check_4_value"].initial = ( - instance.check_4.value if instance.check_4 else 0 - ) - self.fields["check_5_value"].initial = ( - instance.check_5.value if instance.check_5 else 0 - ) - self.fields["comment"].initial = instance.comment - self.fields["emptied"].initial = instance.emptied - self.instance = instance - else: - self.instance = None - - def save(self, counter=None): - cd = self.cleaned_data - summary = self.instance or CashRegisterSummary( - counter=counter, user=counter.get_random_barman() - ) - summary.comment = cd["comment"] - summary.emptied = cd["emptied"] - summary.save() - summary.items.all().delete() - # Cash - if cd["ten_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.1, quantity=cd["ten_cents"] - ).save() - if cd["twenty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.2, quantity=cd["twenty_cents"] - ).save() - if cd["fifty_cents"]: - CashRegisterSummaryItem( - cash_summary=summary, value=0.5, quantity=cd["fifty_cents"] - ).save() - if cd["one_euro"]: - CashRegisterSummaryItem( - cash_summary=summary, value=1, quantity=cd["one_euro"] - ).save() - if cd["two_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=2, quantity=cd["two_euros"] - ).save() - if cd["five_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=5, quantity=cd["five_euros"] - ).save() - if cd["ten_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=10, quantity=cd["ten_euros"] - ).save() - if cd["twenty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=20, quantity=cd["twenty_euros"] - ).save() - if cd["fifty_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=50, quantity=cd["fifty_euros"] - ).save() - if cd["hundred_euros"]: - CashRegisterSummaryItem( - cash_summary=summary, value=100, quantity=cd["hundred_euros"] - ).save() - # Checks - if cd["check_1_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_1_value"], - quantity=cd["check_1_quantity"], - is_check=True, - ).save() - if cd["check_2_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_2_value"], - quantity=cd["check_2_quantity"], - is_check=True, - ).save() - if cd["check_3_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_3_value"], - quantity=cd["check_3_quantity"], - is_check=True, - ).save() - if cd["check_4_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_4_value"], - quantity=cd["check_4_quantity"], - is_check=True, - ).save() - if cd["check_5_quantity"]: - CashRegisterSummaryItem( - cash_summary=summary, - value=cd["check_5_value"], - quantity=cd["check_5_quantity"], - is_check=True, - ).save() - if summary.items.count() < 1: - summary.delete() - - -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 CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView): - """Provide the cash summary form.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/cash_register_summary.jinja" - current_tab = "cash_summary" - - 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(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm() - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - self.object = self.get_object() - self.form = CashRegisterSummaryForm(request.POST) - if self.form.is_valid(): - self.form.save(self.object) - return HttpResponseRedirect(self.get_success_url()) - return super().get(request, *args, **kwargs) - - def get_success_url(self): - return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id}) - - def get_context_data(self, **kwargs): - """Add form to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["form"] = self.form - return kwargs - - -class CounterActivityView(DetailView): - """Show the bar activity.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/activity.jinja" - - -class CounterStatView(DetailView, CounterAdminMixin): - """Show the bar stats.""" - - model = Counter - pk_url_kwarg = "counter_id" - template_name = "counter/stats.jinja" - - def get_context_data(self, **kwargs): - """Add stats to the context.""" - counter: Counter = self.object - semester_start = get_start_of_semester() - office_hours = counter.get_top_barmen() - kwargs = super().get_context_data(**kwargs) - kwargs.update( - { - "counter": counter, - "current_semester": get_semester_code(), - "total_sellings": counter.get_total_sales(since=semester_start), - "top_customers": counter.get_top_customers(since=semester_start)[:100], - "top_barman": office_hours[:100], - "top_barman_semester": ( - office_hours.filter(start__gt=semester_start)[:100] - ), - } - ) - return kwargs - - def dispatch(self, request, *args, **kwargs): - try: - return super().dispatch(request, *args, **kwargs) - except PermissionDenied: - if ( - request.user.is_root - or request.user.is_board_member - or self.get_object().is_owned_by(request.user) - ): - return super(CanEditMixin, self).dispatch(request, *args, **kwargs) - raise PermissionDenied - - -class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_register_summary.jinja" - context_object_name = "cashsummary" - pk_url_kwarg = "cashsummary_id" - form_class = CashRegisterSummaryForm - current_tab = "cash_summary" - - def get_success_url(self): - return reverse("counter:cash_summary_list") - - -class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """Display a list of cash summaries.""" - - model = CashRegisterSummary - template_name = "counter/cash_summary_list.jinja" - context_object_name = "cashsummary_list" - current_tab = "cash_summary" - queryset = CashRegisterSummary.objects.all().order_by("-date") - paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - form = CashSummaryFormBase(self.request.GET) - kwargs["form"] = form - kwargs["summaries_sums"] = {} - kwargs["refilling_sums"] = {} - for c in Counter.objects.filter(type="BAR").all(): - refillings = Refilling.objects.filter(counter=c) - cashredistersummaries = CashRegisterSummary.objects.filter(counter=c) - if form.is_valid() and form.cleaned_data["begin_date"]: - refillings = refillings.filter( - date__gte=form.cleaned_data["begin_date"] - ) - cashredistersummaries = cashredistersummaries.filter( - date__gte=form.cleaned_data["begin_date"] - ) - else: - last_summary = ( - CashRegisterSummary.objects.filter(counter=c, emptied=True) - .order_by("-date") - .first() - ) - if last_summary: - refillings = refillings.filter(date__gt=last_summary.date) - cashredistersummaries = cashredistersummaries.filter( - date__gt=last_summary.date - ) - else: - refillings = refillings.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) # My birth date should be old enough - cashredistersummaries = cashredistersummaries.filter( - date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc) - ) - if form.is_valid() and form.cleaned_data["end_date"]: - refillings = refillings.filter(date__lte=form.cleaned_data["end_date"]) - cashredistersummaries = cashredistersummaries.filter( - date__lte=form.cleaned_data["end_date"] - ) - kwargs["summaries_sums"][c.name] = sum( - [s.get_total() for s in cashredistersummaries.all()] - ) - kwargs["refilling_sums"][c.name] = sum([s.amount for s in refillings.all()]) - return kwargs - - -class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): - template_name = "counter/invoices_call.jinja" - current_tab = "invoices_call" - - def get_context_data(self, **kwargs): - """Add sums to the context.""" - kwargs = super().get_context_data(**kwargs) - kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC") - if "month" in self.request.GET: - start_date = datetime.strptime(self.request.GET["month"], "%Y-%m") - else: - start_date = datetime( - year=timezone.now().year, - month=(timezone.now().month + 10) % 12 + 1, - day=1, - ) - start_date = start_date.replace(tzinfo=tz.utc) - end_date = (start_date + timedelta(days=32)).replace( - day=1, hour=0, minute=0, microsecond=0 - ) - from django.db.models import Case, F, Sum, When - - kwargs["sum_cb"] = sum( - [ - r.amount - for r in Refilling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["sum_cb"] += sum( - [ - s.quantity * s.unit_price - for s in Selling.objects.filter( - payment_method="CARD", - is_validated=True, - date__gte=start_date, - date__lte=end_date, - ) - ] - ) - kwargs["start_date"] = start_date - kwargs["sums"] = ( - Selling.objects.values("club__name") - .annotate( - selling_sum=Sum( - Case( - When( - date__gte=start_date, - date__lt=end_date, - then=F("unit_price") * F("quantity"), - ), - output_field=CurrencyField(), - ) - ) - ) - .exclude(selling_sum=None) - .order_by("-selling_sum") - ) - return kwargs - - -class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """A list view for the admins.""" - - model = Eticket - template_name = "counter/eticket_list.jinja" - ordering = ["id"] - current_tab = "etickets" - - -class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): - """Create an eticket.""" - - model = Eticket - template_name = "core/create.jinja" - form_class = EticketForm - current_tab = "etickets" - - -class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): - """Edit an eticket.""" - - model = Eticket - template_name = "core/edit.jinja" - form_class = EticketForm - pk_url_kwarg = "eticket_id" - current_tab = "etickets" - - -class EticketPDFView(CanViewMixin, DetailView): - """Display the PDF of an eticket.""" - - model = Selling - pk_url_kwarg = "selling_id" - - def get(self, request, *args, **kwargs): - from reportlab.graphics import renderPDF - from reportlab.graphics.barcode.qr import QrCodeWidget - from reportlab.graphics.shapes import Drawing - from reportlab.lib.units import cm - from reportlab.lib.utils import ImageReader - from reportlab.pdfgen import canvas - - if not ( - hasattr(self.object, "product") and hasattr(self.object.product, "eticket") - ): - raise Http404 - - eticket = self.object.product.eticket - user = self.object.customer.user - code = "%s %s %s %s" % ( - self.object.customer.user.id, - self.object.product.id, - self.object.id, - self.object.quantity, - ) - code += " " + eticket.get_hash(code)[:8].upper() - response = HttpResponse(content_type="application/pdf") - response["Content-Disposition"] = 'filename="eticket.pdf"' - p = canvas.Canvas(response) - p.setTitle("Eticket") - im = ImageReader("core/static/core/img/eticket.jpg") - width, height = im.getSize() - size = max(width, height) - width = 8 * cm * width / size - height = 8 * cm * height / size - p.drawImage(im, 10 * cm, 25 * cm, width, height) - if eticket.banner: - im = ImageReader(eticket.banner) - width, height = im.getSize() - size = max(width, height) - width = 6 * cm * width / size - height = 6 * cm * height / size - p.drawImage(im, 1 * cm, 25 * cm, width, height) - if user.profile_pict: - im = ImageReader(user.profile_pict.file) - width, height = im.getSize() - size = max(width, height) - width = 150 * width / size - height = 150 * height / size - p.drawImage(im, 10.5 * cm - width / 2, 16 * cm, width, height) - if eticket.event_title: - p.setFont("Helvetica-Bold", 20) - p.drawCentredString(10.5 * cm, 23.6 * cm, eticket.event_title) - if eticket.event_date: - p.setFont("Helvetica-Bold", 16) - p.drawCentredString( - 10.5 * cm, 22.6 * cm, eticket.event_date.strftime("%d %b %Y") - ) # FIXME with a locale - p.setFont("Helvetica-Bold", 14) - p.drawCentredString( - 10.5 * cm, - 15 * cm, - "%s : %d %s" - % (user.get_display_name(), self.object.quantity, str(_("people(s)"))), - ) - p.setFont("Courier-Bold", 14) - qrcode = QrCodeWidget(code) - bounds = qrcode.getBounds() - width = bounds[2] - bounds[0] - height = bounds[3] - bounds[1] - d = Drawing(260, 260, transform=[260.0 / width, 0, 0, 260.0 / height, 0, 0]) - d.add(qrcode) - renderPDF.draw(d, p, 10.5 * cm - 130, 6.1 * cm) - p.drawCentredString(10.5 * cm, 6 * cm, code) - - partners = ImageReader("core/static/core/img/partners.png") - width, height = partners.getSize() - size = max(width, height) - width = width * 2 / 3 - height = height * 2 / 3 - p.drawImage(partners, 0 * cm, 0 * cm, width, height) - - p.showPage() - p.save() - return response - - -class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - """List of refillings on a counter.""" - - model = Refilling - template_name = "counter/refilling_list.jinja" - current_tab = "counters" - paginate_by = 30 - - def dispatch(self, request, *args, **kwargs): - self.counter = get_object_or_404(Counter, pk=kwargs["counter_id"]) - self.queryset = Refilling.objects.filter(counter__id=self.counter.id) - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["counter"] = self.counter - return kwargs - - -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} - ) From 10f42b1522f576016f0b5619cd76bd4c7625d55c Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 27 Nov 2024 19:02:48 +0100 Subject: [PATCH 16/53] fix imports --- counter/urls.py | 32 +++++++++++-------- counter/views/admin.py | 1 + counter/views/cash.py | 5 +++ counter/views/click.py | 1 + counter/views/eticket.py | 1 + counter/views/invoice.py | 1 + counter/views/main.py | 66 +++++----------------------------------- 7 files changed, 36 insertions(+), 71 deletions(-) diff --git a/counter/urls.py b/counter/urls.py index a2732925..ee478743 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -15,28 +15,16 @@ from django.urls import path -from counter.views import ( +from counter.views.admin import ( ActiveProductListView, ArchivedProductListView, - CashSummaryEditView, - CashSummaryListView, - CounterActivityView, - CounterCashSummaryView, - CounterClick, CounterCreateView, CounterDeleteView, CounterEditPropView, CounterEditView, - CounterLastOperationsView, CounterListView, - CounterMain, CounterRefillingListView, CounterStatView, - EticketCreateView, - EticketEditView, - EticketListView, - EticketPDFView, - InvoiceCallView, ProductCreateView, ProductEditView, ProductTypeCreateView, @@ -44,6 +32,24 @@ from counter.views import ( ProductTypeListView, RefillingDeleteView, SellingDeleteView, +) +from counter.views.cash import ( + CashSummaryEditView, + CashSummaryListView, + CounterCashSummaryView, +) +from counter.views.click import CounterClick +from counter.views.eticket import ( + EticketCreateView, + EticketEditView, + EticketListView, + EticketPDFView, +) +from counter.views.invoice import InvoiceCallView +from counter.views.main import ( + CounterActivityView, + CounterLastOperationsView, + CounterMain, StudentCardDeleteView, StudentCardFormView, counter_login, diff --git a/counter/views/admin.py b/counter/views/admin.py index 414e66ae..fbf466b3 100644 --- a/counter/views/admin.py +++ b/counter/views/admin.py @@ -32,6 +32,7 @@ from core.views import CanEditMixin, CanViewMixin from counter.forms import CounterEditForm, ProductEditForm from counter.models import Counter, Product, ProductType, Refilling, Selling from counter.utils import is_logged_in_counter +from counter.views.mixins import CounterAdminMixin, CounterAdminTabsMixin class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView): diff --git a/counter/views/cash.py b/counter/views/cash.py index f117a151..d4a03af1 100644 --- a/counter/views/cash.py +++ b/counter/views/cash.py @@ -32,6 +32,11 @@ from counter.models import ( Refilling, ) from counter.utils import is_logged_in_counter +from counter.views.mixins import ( + CounterAdminMixin, + CounterAdminTabsMixin, + CounterTabsMixin, +) class CashRegisterSummaryForm(forms.Form): diff --git a/counter/views/click.py b/counter/views/click.py index fe948c91..88875fe4 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -29,6 +29,7 @@ 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.views.mixins import CounterTabsMixin if TYPE_CHECKING: from core.models import User diff --git a/counter/views/eticket.py b/counter/views/eticket.py index 56757fc2..a05020d6 100644 --- a/counter/views/eticket.py +++ b/counter/views/eticket.py @@ -21,6 +21,7 @@ from django.views.generic.edit import CreateView, UpdateView from core.views import CanViewMixin from counter.forms import EticketForm from counter.models import Eticket, Selling +from counter.views.mixins import CounterAdminMixin, CounterAdminTabsMixin class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): diff --git a/counter/views/invoice.py b/counter/views/invoice.py index 19c94c8b..dbd6e7cb 100644 --- a/counter/views/invoice.py +++ b/counter/views/invoice.py @@ -21,6 +21,7 @@ from django.views.generic import TemplateView from accounting.models import CurrencyField from counter.models import Refilling, Selling +from counter.views.mixins import CounterAdminMixin, CounterAdminTabsMixin class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): diff --git a/counter/views/main.py b/counter/views/main.py index 0f58eeda..e04f8944 100644 --- a/counter/views/main.py +++ b/counter/views/main.py @@ -12,76 +12,26 @@ # 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 datetime import timedelta -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.http import HttpRequest, HttpResponseRedirect 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 django.views.generic import DetailView +from django.views.generic.edit import DeleteView, FormMixin, FormView, ProcessFormView -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 import CanEditMixin, CanViewMixin 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.forms import GetUserForm, StudentCardForm +from counter.models import Counter, Customer, Permanency, StudentCard from counter.utils import is_logged_in_counter - -if TYPE_CHECKING: - from core.models import User +from counter.views.mixins import CounterTabsMixin class StudentCardDeleteView(DeleteView, CanEditMixin): From 007e17fd8bda9b81146c27efe8af6f7683faf9f6 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 4 Dec 2024 18:18:30 +0100 Subject: [PATCH 17/53] Fix the account dump command. - a missing `fail_silently` flag made the whole command fail if an invalid recipient is used (like closed utbm mail address) - Not specifying the seller make the account detail pages crash. --- counter/management/commands/dump_accounts.py | 17 ++++++++++++----- sith/settings.py | 3 +++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/counter/management/commands/dump_accounts.py b/counter/management/commands/dump_accounts.py index 01ba35cd..9897f2c8 100644 --- a/counter/management/commands/dump_accounts.py +++ b/counter/management/commands/dump_accounts.py @@ -55,7 +55,9 @@ class Command(BaseCommand): customer__user__in=reactivated_users ).delete() self._dump_accounts({u.customer for u in users_to_dump}) - self._send_mails(users_to_dump) + self.stdout.write("Accounts dumped") + nb_successful_mails = self._send_mails(users_to_dump) + self.stdout.write(f"{nb_successful_mails} were successfuly sent.") self.stdout.write("Finished !") @staticmethod @@ -103,13 +105,14 @@ class Command(BaseCommand): if len(pending_dumps) != len(customer_ids): raise ValueError("One or more accounts were not engaged in a dump process") counter = Counter.objects.get(pk=settings.SITH_COUNTER_ACCOUNT_DUMP_ID) + seller = User.objects.get(pk=settings.SITH_ROOT_USER_ID) sales = Selling.objects.bulk_create( [ Selling( label="Vidange compte inactif", club=counter.club, counter=counter, - seller=None, + seller=seller, product=None, customer=account, quantity=1, @@ -134,8 +137,12 @@ class Command(BaseCommand): Customer.objects.filter(pk__in=customer_ids).update(amount=0) @staticmethod - def _send_mails(users: Iterable[User]): - """Send the mails informing users that their account has been dumped.""" + def _send_mails(users: Iterable[User]) -> int: + """Send the mails informing users that their account has been dumped. + + Returns: + The number of emails successfully sent. + """ mails = [ ( _("Your AE account has been emptied"), @@ -145,4 +152,4 @@ class Command(BaseCommand): ) for user in users ] - send_mass_mail(mails) + return send_mass_mail(mails, fail_silently=True) diff --git a/sith/settings.py b/sith/settings.py index a06043ef..ba0a35da 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -351,6 +351,9 @@ SITH_SEMESTER_START_SPRING = (2, 15) # 15 February # Used to determine the valid promos SITH_SCHOOL_START_YEAR = 1999 +# id of the Root account +SITH_ROOT_USER_ID = 0 + SITH_GROUP_ROOT_ID = 1 SITH_GROUP_PUBLIC_ID = 2 SITH_GROUP_SUBSCRIBERS_ID = 3 From 0b509f22008d3982535ba002e8d24858bffdeaaa Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 6 Dec 2024 17:40:18 +0100 Subject: [PATCH 18/53] fix N+1 queries on user search --- core/schemas.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/schemas.py b/core/schemas.py index 775cd6b0..f4080c90 100644 --- a/core/schemas.py +++ b/core/schemas.py @@ -4,6 +4,7 @@ from typing import Annotated from annotated_types import MinLen from django.contrib.staticfiles.storage import staticfiles_storage from django.db.models import Q +from django.urls import reverse from django.utils.text import slugify from haystack.query import SearchQuerySet from ninja import FilterSchema, ModelSchema, Schema @@ -37,13 +38,13 @@ class UserProfileSchema(ModelSchema): @staticmethod def resolve_profile_url(obj: User) -> str: - return obj.get_absolute_url() + return reverse("core:user_profile", kwargs={"user_id": obj.pk}) @staticmethod def resolve_profile_pict(obj: User) -> str: if obj.profile_pict_id is None: return staticfiles_storage.url("core/img/unknown.jpg") - return obj.profile_pict.get_download_url() + return reverse("core:download", kwargs={"file_id": obj.profile_pict_id}) class SithFileSchema(ModelSchema): From 84d7e40e668d77d158b0b9044eccd5215c8288e4 Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 6 Dec 2024 18:28:12 +0100 Subject: [PATCH 19/53] feat: client-side cache for ajax-select inputs --- .../bundled/core/components/ajax-select-base.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core/static/bundled/core/components/ajax-select-base.ts b/core/static/bundled/core/components/ajax-select-base.ts index 0befd398..674b7b73 100644 --- a/core/static/bundled/core/components/ajax-select-base.ts +++ b/core/static/bundled/core/components/ajax-select-base.ts @@ -103,6 +103,12 @@ export class AutoCompleteSelectBase extends inheritHtmlElement("select") { export abstract class AjaxSelect extends AutoCompleteSelectBase { protected filter?: (items: TomOption[]) => TomOption[] = null; protected minCharNumberForSearch = 2; + /** + * A cache of researches that have been made using this input. + * For each record, the key is the user's query and the value + * is the list of results sent back by the server. + */ + protected cache = {} as Record; protected abstract valueField: string; protected abstract labelField: string; @@ -135,7 +141,13 @@ export abstract class AjaxSelect extends AutoCompleteSelectBase { this.widget.clearOptions(); } - const resp = await this.search(query); + // Check in the cache if this query has already been typed + // and do an actual HTTP request only if the result isn't cached + let resp = this.cache[query]; + if (!resp) { + resp = await this.search(query); + this.cache[query] = resp; + } if (this.filter) { callback(this.filter(resp), []); From 5dc99dbfcbe452a19fe9dfa9f59deee4ce4623e3 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 7 Dec 2024 00:05:45 +0100 Subject: [PATCH 20/53] extract student card views --- counter/views/{main.py => student_card.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/views/{main.py => student_card.py} (100%) diff --git a/counter/views/main.py b/counter/views/student_card.py similarity index 100% rename from counter/views/main.py rename to counter/views/student_card.py From c9d83e591692d418fb3b2d85afe29b8eeba76ae6 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 7 Dec 2024 00:06:33 +0100 Subject: [PATCH 21/53] extract student card views --- counter/views/student_card.py | 170 ++-------------------------------- 1 file changed, 6 insertions(+), 164 deletions(-) diff --git a/counter/views/student_card.py b/counter/views/student_card.py index e04f8944..b39cc519 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -12,26 +12,15 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # -from datetime import timedelta -from django.conf import settings from django.core.exceptions import PermissionDenied -from django.db.models import F -from django.http import HttpRequest, HttpResponseRedirect -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 -from django.views.generic.edit import DeleteView, FormMixin, FormView, ProcessFormView +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, CanViewMixin -from core.views.forms import LoginForm -from counter.forms import GetUserForm, StudentCardForm -from counter.models import Counter, Customer, Permanency, StudentCard -from counter.utils import is_logged_in_counter -from counter.views.mixins import CounterTabsMixin +from core.views import CanEditMixin +from counter.forms import StudentCardForm +from counter.models import Customer, StudentCard class StudentCardDeleteView(DeleteView, CanEditMixin): @@ -51,153 +40,6 @@ class StudentCardDeleteView(DeleteView, CanEditMixin): ) -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.""" From ff68e65250c6707f0d1bd66343dd03562ac1a068 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 7 Dec 2024 00:07:37 +0100 Subject: [PATCH 22/53] extract counter home views --- counter/views/{main.py => home.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/views/{main.py => home.py} (100%) diff --git a/counter/views/main.py b/counter/views/home.py similarity index 100% rename from counter/views/main.py rename to counter/views/home.py From c4764110d8658a14a556c36e34633e9eee3c1956 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 7 Dec 2024 00:10:46 +0100 Subject: [PATCH 23/53] extract counter home views --- counter/views/home.py | 85 +++---------------------------------------- 1 file changed, 5 insertions(+), 80 deletions(-) diff --git a/counter/views/home.py b/counter/views/home.py index e04f8944..60cc5a5a 100644 --- a/counter/views/home.py +++ b/counter/views/home.py @@ -15,42 +15,21 @@ from datetime import timedelta from django.conf import settings -from django.core.exceptions import PermissionDenied -from django.db.models import F -from django.http import HttpRequest, HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect +from django.http import HttpResponseRedirect 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 -from django.views.generic.edit import DeleteView, FormMixin, FormView, ProcessFormView +from django.views.generic.edit import FormMixin, ProcessFormView -from core.views import CanEditMixin, CanViewMixin +from core.views import CanViewMixin from core.views.forms import LoginForm -from counter.forms import GetUserForm, StudentCardForm -from counter.models import Counter, Customer, Permanency, StudentCard +from counter.forms import GetUserForm +from counter.models import Counter from counter.utils import is_logged_in_counter from counter.views.mixins import CounterTabsMixin -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 ): @@ -123,36 +102,6 @@ class CounterMain( 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.""" @@ -196,27 +145,3 @@ class CounterActivityView(DetailView): 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} - ) From 7a91a7156516f2607cb221afa89a586fd9825e51 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 7 Dec 2024 00:11:18 +0100 Subject: [PATCH 24/53] extract counter auth views --- counter/views/{main.py => auth.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename counter/views/{main.py => auth.py} (100%) diff --git a/counter/views/main.py b/counter/views/auth.py similarity index 100% rename from counter/views/main.py rename to counter/views/auth.py From 6e48f88c06c1a5195483358ecaebe2824a0620ba Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 7 Dec 2024 00:12:10 +0100 Subject: [PATCH 25/53] extract counter auth views --- counter/views/auth.py | 171 +----------------------------------------- 1 file changed, 1 insertion(+), 170 deletions(-) diff --git a/counter/views/auth.py b/counter/views/auth.py index e04f8944..87cce72c 100644 --- a/counter/views/auth.py +++ b/counter/views/auth.py @@ -12,115 +12,15 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # -from datetime import timedelta -from django.conf import settings -from django.core.exceptions import PermissionDenied from django.db.models import F from django.http import HttpRequest, HttpResponseRedirect 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 -from django.views.generic.edit import DeleteView, FormMixin, FormView, ProcessFormView -from core.views import CanEditMixin, CanViewMixin from core.views.forms import LoginForm -from counter.forms import GetUserForm, StudentCardForm -from counter.models import Counter, Customer, Permanency, StudentCard -from counter.utils import is_logged_in_counter -from counter.views.mixins import CounterTabsMixin - - -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) +from counter.models import Counter, Permanency @require_POST @@ -151,72 +51,3 @@ def counter_logout(request: HttpRequest, counter_id: int) -> HttpResponseRedirec 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} - ) From 10dde3f0021cd49853aaf6a7515165b966e80142 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 7 Dec 2024 00:18:17 +0100 Subject: [PATCH 26/53] fix imports --- counter/urls.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/counter/urls.py b/counter/urls.py index ee478743..d5247478 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -33,6 +33,7 @@ from counter.views.admin import ( RefillingDeleteView, SellingDeleteView, ) +from counter.views.auth import counter_login, counter_logout from counter.views.cash import ( CashSummaryEditView, CashSummaryListView, @@ -45,16 +46,13 @@ from counter.views.eticket import ( EticketListView, EticketPDFView, ) -from counter.views.invoice import InvoiceCallView -from counter.views.main import ( +from counter.views.home import ( CounterActivityView, CounterLastOperationsView, CounterMain, - StudentCardDeleteView, - StudentCardFormView, - counter_login, - counter_logout, ) +from counter.views.invoice import InvoiceCallView +from counter.views.student_card import StudentCardDeleteView, StudentCardFormView urlpatterns = [ path("/", CounterMain.as_view(), name="details"), From b81cf49d0aca043c576405983de9d865556b04bc Mon Sep 17 00:00:00 2001 From: Sli Date: Thu, 14 Nov 2024 16:17:10 +0100 Subject: [PATCH 27/53] 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 --- core/static/bundled/utils/web-components.ts | 11 +- counter/forms.py | 12 +- .../counter/add_student_card_fragment.jinja | 25 ++++ counter/templates/counter/counter_click.jinja | 27 ++-- counter/tests/test_customer.py | 125 ++++++++++++------ counter/urls.py | 11 +- counter/views/click.py | 27 +--- counter/views/student_card.py | 55 +++++++- 8 files changed, 196 insertions(+), 97 deletions(-) create mode 100644 counter/templates/counter/add_student_card_fragment.jinja diff --git a/core/static/bundled/utils/web-components.ts b/core/static/bundled/utils/web-components.ts index 8bec98f9..c2b089c5 100644 --- a/core/static/bundled/utils/web-components.ts +++ b/core/static/bundled/utils/web-components.ts @@ -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; + } }; } diff --git a/counter/forms.py b/counter/forms.py index 84a92512..91e7c3dc 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -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" diff --git a/counter/templates/counter/add_student_card_fragment.jinja b/counter/templates/counter/add_student_card_fragment.jinja new file mode 100644 index 00000000..aa2150e1 --- /dev/null +++ b/counter/templates/counter/add_student_card_fragment.jinja @@ -0,0 +1,25 @@ +
+

{% trans %}Add a student card{% endtrans %}

+
+ {% csrf_token %} + {{ form.as_p() }} + + +
+
{% trans %}Registered cards{% endtrans %}
+ {% if student_cards %} + +
    + {% for card in student_cards %} +
  • {{ card.uid }}
  • + {% endfor %} +
+ {% else %} + {% trans %}No card registered{% endtrans %} + {% endif %} +
diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index cb6bb9cf..323a5905 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -29,26 +29,15 @@ {{ user_mini_profile(customer.user) }} {{ user_subscription(customer.user) }}

{% trans %}Amount: {% endtrans %}{{ customer.amount }} €

-
- {% csrf_token %} - - {% trans %}Add a student card{% endtrans %} - {{ student_card_input.student_card_uid }} - {% if request.session['not_valid_student_card_uid'] %} -

{% trans %}This is not a valid student card UID{% endtrans %}

- {% endif %} - -
-
{% trans %}Registered cards{% endtrans %}
- {% if student_cards %} -
    - {% for card in student_cards %} -
  • {{ card.uid }}
  • - {% endfor %} -
- {% else %} - {% trans %}No card registered{% endtrans %} + {% if counter.type == 'BAR' %} +
+
+
{% endif %} diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py index 2d7e1c60..dd7a5ed6 100644 --- a/counter/tests/test_customer.py +++ b/counter/tests/test_customer.py @@ -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( diff --git a/counter/urls.py b/counter/urls.py index d5247478..ab93b586 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -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("/", CounterMain.as_view(), name="details"), @@ -77,6 +81,11 @@ urlpatterns = [ StudentCardFormView.as_view(), name="add_student_card", ), + path( + "customer//card/add/counter//", + StudentCardFormFragmentView.as_view(), + name="add_student_card_fragment", + ), path( "customer//card/delete//", StudentCardDeleteView.as_view(), diff --git a/counter/views/click.py b/counter/views/click.py index 88875fe4..1bdc4b3c 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -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 diff --git a/counter/views/student_card.py b/counter/views/student_card.py index b39cc519..a79fa8fd 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -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, + }, + ) From d4b9c3afb1b83d3e0c143fadb590197b3150599e Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 7 Dec 2024 17:28:34 +0100 Subject: [PATCH 28/53] Make StudentCardFormView fragment only --- core/templates/core/user_preferences.jinja | 37 +-- core/views/user.py | 4 - counter/templates/counter/counter_click.jinja | 2 +- .../create_student_card.jinja} | 10 +- counter/tests/test_customer.py | 280 ++++++++++++------ counter/urls.py | 6 - counter/utils.py | 12 +- counter/views/student_card.py | 71 ++--- 8 files changed, 234 insertions(+), 188 deletions(-) rename counter/templates/counter/{add_student_card_fragment.jinja => fragments/create_student_card.jinja} (61%) diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index 0cf4bd57..38749fde 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -38,32 +38,17 @@ {% if profile.customer %}

{% trans %}Student cards{% endtrans %}

- {% if profile.customer.student_cards.exists() %} - - {% else %} - {% trans %}No student card registered.{% endtrans %} -

- {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually - add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} -

- {% endif %} - -
- {% csrf_token %} - {{ student_card_form.as_p() }} - -
+

+ {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually + add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} +

+
+
+
{% endif %} {% endblock %} \ No newline at end of file diff --git a/core/views/user.py b/core/views/user.py index e9694a92..5a797620 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -70,7 +70,6 @@ from core.views.forms import ( UserGodfathersForm, UserProfileForm, ) -from counter.forms import StudentCardForm from counter.models import Refilling, Selling from eboutic.models import Invoice from subscription.models import Subscription @@ -576,9 +575,6 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): hasattr(self.object, "trombi_user") and self.request.user.trombi_user.trombi ): kwargs["trombi_form"] = UserTrombiForm() - - if hasattr(self.object, "customer"): - kwargs["student_card_form"] = StudentCardForm() return kwargs diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 323a5905..9c95292e 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -32,7 +32,7 @@ {% if counter.type == 'BAR' %}
diff --git a/counter/templates/counter/add_student_card_fragment.jinja b/counter/templates/counter/fragments/create_student_card.jinja similarity index 61% rename from counter/templates/counter/add_student_card_fragment.jinja rename to counter/templates/counter/fragments/create_student_card.jinja index aa2150e1..7cd05ba9 100644 --- a/counter/templates/counter/add_student_card_fragment.jinja +++ b/counter/templates/counter/fragments/create_student_card.jinja @@ -1,7 +1,6 @@

{% trans %}Add a student card{% endtrans %}

{% for card in student_cards %} -
  • {{ card.uid }}
  • +
  • + {{ card.uid }} + + {% trans %}Delete{% endtrans %} + +
  • {% endfor %} {% else %} - {% trans %}No card registered{% endtrans %} + {% trans %}No student card registered.{% endtrans %} {% endif %}
    diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py index dd7a5ed6..f7e599e6 100644 --- a/counter/tests/test_customer.py +++ b/counter/tests/test_customer.py @@ -1,15 +1,27 @@ import json import string +from datetime import timedelta import pytest +from django.conf import settings +from django.contrib.auth.base_user import make_password from django.test import Client, TestCase from django.urls import reverse +from django.utils.timezone import now from model_bakery import baker -from core.baker_recipes import subscriber_user +from club.models import Membership +from core.baker_recipes import board_user, subscriber_user from core.models import User from counter.baker_recipes import refill_recipe, sale_recipe -from counter.models import BillingInfo, Counter, Customer, Refilling, Selling +from counter.models import ( + BillingInfo, + Counter, + Customer, + Refilling, + Selling, + StudentCard, +) @pytest.mark.django_db @@ -162,43 +174,65 @@ class TestStudentCard(TestCase): @classmethod def setUpTestData(cls): - cls.krophil = User.objects.get(username="krophil") - cls.sli = User.objects.get(username="sli") - cls.skia = User.objects.get(username="skia") - cls.root = User.objects.get(username="root") + cls.customer = subscriber_user.make() + cls.customer.save() + cls.barmen = subscriber_user.make(password=make_password("plop")) + cls.board_admin = board_user.make() + cls.club_admin = baker.make(User) + cls.root = baker.make(User, is_superuser=True) + cls.subscriber = subscriber_user.make() - cls.counter = Counter.objects.get(id=2) - cls.ae_counter = Counter.objects.get(name="AE") + cls.counter = baker.make(Counter, type="BAR") + cls.counter.sellers.add(cls.barmen) + + cls.club_counter = baker.make(Counter) + baker.make( + Membership, + start_date=now() - timedelta(days=30), + club=cls.club_counter.club, + role=settings.SITH_CLUB_ROLES_ID["Board member"], + user=cls.club_admin, + ) + + cls.valid_card = baker.make( + StudentCard, customer=cls.customer.customer, uid="8A89B82018B0A0" + ) def setUp(self): # Auto login on counter self.client.post( reverse("counter:login", args=[self.counter.id]), - {"username": "krophil", "password": "plop"}, + {"username": self.barmen.username, "password": "plop"}, ) def test_search_user_with_student_card(self): response = self.client.post( reverse("counter:details", args=[self.counter.id]), - {"code": "9A89B82018B0A0"}, + {"code": self.valid_card.uid}, ) assert response.url == reverse( "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + kwargs={"counter_id": self.counter.id, "user_id": self.customer.id}, ) def test_add_student_card_from_counter(self): # Test card with mixed letters and numbers response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8B90734A802A8F"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) assert response.status_code == 302 self.assertContains(self.client.get(response.url), text="8B90734A802A8F") @@ -206,13 +240,19 @@ class TestStudentCard(TestCase): # Test card with only numbers response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "04786547890123"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) assert response.status_code == 302 self.assertContains(self.client.get(response.url), text="04786547890123") @@ -220,13 +260,19 @@ class TestStudentCard(TestCase): # Test card with only letters response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "ABCAAAFAAFAAAB"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) assert response.status_code == 302 self.assertContains(self.client.get(response.url), text="ABCAAAFAAFAAAB") @@ -235,26 +281,38 @@ class TestStudentCard(TestCase): # UID too short response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8B90734A802A8"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) self.assertContains(response, text="Cet UID est invalide") # UID too long response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8B90734A802A8FA"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) self.assertContains(response, text="Cet UID est invalide") self.assertContains( @@ -265,13 +323,19 @@ class TestStudentCard(TestCase): # Test with already existing card response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, + }, + ), + {"uid": self.valid_card.uid}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, }, ), - {"uid": "9A89B82018B0A0"}, ) self.assertContains(response, text="Cet UID est invalide") self.assertContains( @@ -281,26 +345,38 @@ class TestStudentCard(TestCase): # Test with lowercase response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8b90734a802a9f"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) self.assertContains(response, text="Cet UID est invalide") # Test with white spaces response = self.client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": self.counter.id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": " "}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), ) self.assertContains(response, text="Cet UID est invalide") self.assertContains(response, text="Ce champ est obligatoire.") @@ -309,52 +385,58 @@ class TestStudentCard(TestCase): # 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}, + {"user_id": self.barmen.id}, ) def send_valid_request(client, counter_id): return client.post( reverse( - "counter:add_student_card_fragment", + "counter:add_student_card", kwargs={ - "counter_id": counter_id, - "customer_id": self.sli.customer.pk, + "customer_id": self.customer.customer.pk, }, ), {"uid": "8B90734A802A8F"}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={ + "counter_id": counter_id, + "user_id": self.customer.customer.pk, + }, + ), ) 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) + self.client.force_login(self.club_admin) + assert send_valid_request(self.client, self.club_counter.id).status_code == 403 def test_delete_student_card_with_owner(self): - self.client.force_login(self.sli) + self.client.force_login(self.customer) self.client.post( reverse( "counter:delete_student_card", kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, + "customer_id": self.customer.customer.pk, + "card_id": self.customer.customer.student_cards.first().id, }, ) ) - assert not self.sli.customer.student_cards.exists() + assert not self.customer.customer.student_cards.exists() def test_delete_student_card_with_board_member(self): - self.client.force_login(self.skia) + self.client.force_login(self.board_admin) self.client.post( reverse( "counter:delete_student_card", kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, + "customer_id": self.customer.customer.pk, + "card_id": self.customer.customer.student_cards.first().id, }, ) ) - assert not self.sli.customer.student_cards.exists() + assert not self.customer.customer.student_cards.exists() def test_delete_student_card_with_root(self): self.client.force_login(self.root) @@ -362,100 +444,107 @@ class TestStudentCard(TestCase): reverse( "counter:delete_student_card", kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, + "customer_id": self.customer.customer.pk, + "card_id": self.customer.customer.student_cards.first().id, }, ) ) - assert not self.sli.customer.student_cards.exists() + assert not self.customer.customer.student_cards.exists() def test_delete_student_card_fail(self): - self.client.force_login(self.krophil) + self.client.force_login(self.subscriber) response = self.client.post( reverse( "counter:delete_student_card", kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, + "customer_id": self.customer.customer.pk, + "card_id": self.customer.customer.student_cards.first().id, }, ) ) assert response.status_code == 403 - assert self.sli.customer.student_cards.exists() + assert self.customer.customer.student_cards.exists() def test_add_student_card_from_user_preferences(self): # Test with owner of the card - self.client.force_login(self.sli) - self.client.post( + self.client.force_login(self.customer) + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8F"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="8B90734A802A8F") # Test with board member - self.client.force_login(self.skia) - self.client.post( + self.client.force_login(self.board_admin) + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8A"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="8B90734A802A8A") # Test card with only numbers - self.client.post( + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "04786547890123"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="04786547890123") # Test card with only letters - self.client.post( + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "ABCAAAFAAFAAAB"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="ABCAAAFAAFAAAB") # Test with root self.client.force_login(self.root) - self.client.post( + response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8B"}, ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) + assert response.status_code == 302 + + response = self.client.get(response.url) self.assertContains(response, text="8B90734A802A8B") def test_add_student_card_from_user_preferences_fail(self): - self.client.force_login(self.sli) + self.client.force_login(self.customer) # UID too short response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8"}, ) @@ -465,7 +554,8 @@ class TestStudentCard(TestCase): # UID too long response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8FA"}, ) @@ -474,9 +564,10 @@ class TestStudentCard(TestCase): # Test with already existing card response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), - {"uid": "9A89B82018B0A0"}, + {"uid": self.valid_card.uid}, ) self.assertContains( response, text="Un objet Student card avec ce champ Uid existe déjà." @@ -485,7 +576,8 @@ class TestStudentCard(TestCase): # Test with lowercase response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8b90734a802a9f"}, ) @@ -494,17 +586,19 @@ class TestStudentCard(TestCase): # Test with white spaces response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": " " * 14}, ) self.assertContains(response, text="Cet UID est invalide") # Test with unauthorized user - self.client.force_login(self.krophil) + self.client.force_login(self.subscriber) response = self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": "8B90734A802A8F"}, ) diff --git a/counter/urls.py b/counter/urls.py index ab93b586..e196894f 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -54,7 +54,6 @@ from counter.views.home import ( from counter.views.invoice import InvoiceCallView from counter.views.student_card import ( StudentCardDeleteView, - StudentCardFormFragmentView, StudentCardFormView, ) @@ -81,11 +80,6 @@ urlpatterns = [ StudentCardFormView.as_view(), name="add_student_card", ), - path( - "customer//card/add/counter//", - StudentCardFormFragmentView.as_view(), - name="add_student_card_fragment", - ), path( "customer//card/delete//", StudentCardDeleteView.as_view(), diff --git a/counter/utils.py b/counter/utils.py index 2b9b6fd6..499b2d8e 100644 --- a/counter/utils.py +++ b/counter/utils.py @@ -22,14 +22,22 @@ def is_logged_in_counter(request: HttpRequest) -> bool: to the counter) - The current session has a counter token associated with it. - A counter with this token exists. + - The counter is open """ referer_ok = ( "HTTP_REFERER" in request.META and resolve(urlparse(request.META["HTTP_REFERER"]).path).app_name == "counter" ) - return ( + has_token = ( (referer_ok or request.resolver_match.app_name == "counter") and "counter_token" in request.session and request.session["counter_token"] - and Counter.objects.filter(token=request.session["counter_token"]).exists() + ) + if not has_token: + return False + + return ( + Counter.objects.annotate_is_open() + .filter(token=request.session["counter_token"], is_open=True) + .exists() ) diff --git a/counter/views/student_card.py b/counter/views/student_card.py index a79fa8fd..882069da 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -13,14 +13,17 @@ # # + from django.core.exceptions import PermissionDenied +from django.http import HttpRequest 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 AllowFragment, CanEditMixin +from core.views import CanEditMixin from counter.forms import StudentCardForm -from counter.models import Counter, Customer, StudentCard +from counter.models import Customer, StudentCard +from counter.utils import is_logged_in_counter class StudentCardDeleteView(DeleteView, CanEditMixin): @@ -40,16 +43,22 @@ class StudentCardDeleteView(DeleteView, CanEditMixin): ) -class StudentCardFormView(AllowFragment, FormView): - """Add a new student card.""" +class StudentCardFormView(FormView): + """Add a new student card. This is a fragment view !""" form_class = StudentCardForm - template_name = "core/create.jinja" + template_name = "counter/fragments/create_student_card.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): + def dispatch(self, request: HttpRequest, *args, **kwargs): + self.customer = get_object_or_404( + Customer.objects.prefetch_related("student_cards"), pk=kwargs["customer_id"] + ) + + if not is_logged_in_counter(request) and not StudentCard.can_create( + self.customer, request.user + ): raise PermissionDenied + return super().dispatch(request, *args, **kwargs) def form_valid(self, form): @@ -58,56 +67,12 @@ class StudentCardFormView(AllowFragment, FormView): 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} - ) - - -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, - }, - ) + return self.request.path From 2f613607afd534b93996fcf866b31fdfd3e4d672 Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 7 Dec 2024 22:48:18 +0100 Subject: [PATCH 29/53] Update number of queries in test_num_queries --- sas/tests/test_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sas/tests/test_api.py b/sas/tests/test_api.py index ebc33638..733838d2 100644 --- a/sas/tests/test_api.py +++ b/sas/tests/test_api.py @@ -1,10 +1,10 @@ from django.conf import settings +from django.core.cache import cache from django.db import transaction from django.test import TestCase from django.urls import reverse from model_bakery import baker from model_bakery.recipe import Recipe -from pytest_django.asserts import assertNumQueries from core.baker_recipes import old_subscriber_user, subscriber_user from core.models import RealGroup, SithFile, User @@ -128,9 +128,11 @@ class TestPictureSearch(TestSas): def test_num_queries(self): """Test that the number of queries is stable.""" self.client.force_login(subscriber_user.make()) - with assertNumQueries(5): + cache.clear() + with self.assertNumQueries(7): + # 2 requests to create the session # 1 request to fetch the user from the db - # 2 requests to check the user permissions + # 2 requests to check the user permissions, depends on the db engine # 1 request to fetch the pictures # 1 request to count the total number of items in the pagination self.client.get(self.url) From 66d2dc74e79618db5b8c5199723a0190229c8583 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 8 Dec 2024 00:32:28 +0100 Subject: [PATCH 30/53] Pre-fetch forms for student card --- core/templates/core/user_preferences.jinja | 30 +- core/views/user.py | 5 + counter/templates/counter/counter_click.jinja | 231 ++++----- counter/views/click.py | 4 + counter/views/student_card.py | 35 +- locale/fr/LC_MESSAGES/django.po | 455 +++++++++--------- 6 files changed, 391 insertions(+), 369 deletions(-) diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index 38749fde..d70371a2 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -35,20 +35,20 @@ {% endif %} - {% if profile.customer %} -

    {% trans %}Student cards{% endtrans %}

    + {% if student_card %} + {% with + form=student_card.form, + action=student_card.context.action, + customer=student_card.context.customer, + student_cards=student_card.context.student_cards + %} + {% include student_card.template %} + {% endwith %} -

    - {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually - add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} -

    -
    -
    -
    - {% endif %} -
    +

    + {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually + add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} +

    +{% endif %} + {% endblock %} \ No newline at end of file diff --git a/core/views/user.py b/core/views/user.py index 5a797620..83916fb1 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -71,6 +71,7 @@ from core.views.forms import ( UserProfileForm, ) from counter.models import Refilling, Selling +from counter.views.student_card import StudentCardFormView from eboutic.models import Invoice from subscription.models import Subscription from trombi.views import UserTrombiForm @@ -575,6 +576,10 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): hasattr(self.object, "trombi_user") and self.request.user.trombi_user.trombi ): kwargs["trombi_form"] = UserTrombiForm() + if hasattr(self.object, "customer"): + kwargs["student_card"] = StudentCardFormView.get_template_data( + self.request, self.object.customer + ).as_dict() return kwargs diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 9c95292e..ed89b100 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -31,130 +31,131 @@

    {% trans %}Amount: {% endtrans %}{{ customer.amount }} €

    {% if counter.type == 'BAR' %} -
    -
    -
    - {% endif %} - + {% with + form=student_card.form, + action=student_card.context.action, + customer=student_card.context.customer, + student_cards=student_card.context.student_cards + %} + {% include student_card.template %} + {% endwith %} +{% endif %} + -
    -
    {% trans %}Selling{% endtrans %}
    -
    - {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} +
    +
    {% trans %}Selling{% endtrans %}
    +
    + {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} {# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #} - - {% csrf_token %} - - - - - +
    + {% csrf_token %} + + + + +
    - -

    {% trans %}Basket: {% endtrans %}

    - -
      - -
    -

    - Total: - - -

    - -
    - {% csrf_token %} - - -
    -
    - {% csrf_token %} - - -
    + +

    {% trans %}Basket: {% endtrans %}

    -
    -
      - {% for category in categories.keys() -%} -
    • {{ category }}
    • - {%- endfor %} -
    - {% for category in categories.keys() -%} -
    -
    {{ category }}
    - {% for p in categories[category] -%} -
    - {% csrf_token %} - - - -
    - {%- endfor %} +
      + +
    +

    + Total: + + +

    + +
    + {% csrf_token %} + + +
    +
    + {% csrf_token %} + + +
    +
    + {% if (counter.type == 'BAR' and barmens_can_refill) %} +
    {% trans %}Refilling{% endtrans %}
    +
    +
    + {% csrf_token %} + {{ refill_form.as_p() }} + + +
    +
    + {% endif %} +
    + +
    +
      + {% for category in categories.keys() -%} +
    • {{ category }}
    • + {%- endfor %} +
    + {% for category in categories.keys() -%} +
    +
    {{ category }}
    + {% for p in categories[category] -%} +
    + {% csrf_token %} + + + +
    {%- endfor %}
    -
    + {%- endfor %} +
    +
    {% endblock content %} {% block script %} diff --git a/counter/views/click.py b/counter/views/click.py index 1bdc4b3c..c0845aaf 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -30,6 +30,7 @@ from core.views import CanViewMixin from counter.forms import RefillForm from counter.models import Counter, Customer, Product, Selling from counter.views.mixins import CounterTabsMixin +from counter.views.student_card import StudentCardFormView if TYPE_CHECKING: from core.models import User @@ -414,4 +415,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): 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"] = StudentCardFormView.get_template_data( + self.request, self.customer + ).as_dict() return kwargs diff --git a/counter/views/student_card.py b/counter/views/student_card.py index 882069da..fb919ce2 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -14,6 +14,9 @@ # +from dataclasses import asdict, dataclass +from typing import Any + from django.core.exceptions import PermissionDenied from django.http import HttpRequest from django.shortcuts import get_object_or_404 @@ -26,6 +29,16 @@ from counter.models import Customer, StudentCard from counter.utils import is_logged_in_counter +@dataclass +class StudentCardTemplateData: + form: StudentCardForm + template: str + context: dict[str, Any] + + def as_dict(self) -> dict[str, Any]: + return asdict(self) + + class StudentCardDeleteView(DeleteView, CanEditMixin): """View used to delete a card from a user.""" @@ -49,6 +62,23 @@ class StudentCardFormView(FormView): form_class = StudentCardForm template_name = "counter/fragments/create_student_card.jinja" + @classmethod + def get_template_data( + cls, request: HttpRequest, customer: Customer + ) -> StudentCardTemplateData: + """Get necessary data to pre-render the fragment""" + return StudentCardTemplateData( + form=cls.form_class(), + template=cls.template_name, + context={ + "action": reverse_lazy( + "counter:add_student_card", kwargs={"customer_id": customer.pk} + ), + "customer": customer, + "student_cards": customer.student_cards.all(), + }, + ) + def dispatch(self, request: HttpRequest, *args, **kwargs): self.customer = get_object_or_404( Customer.objects.prefetch_related("student_cards"), pk=kwargs["customer_id"] @@ -69,9 +99,8 @@ class StudentCardFormView(FormView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["customer"] = self.customer - context["action"] = self.request.path - context["student_cards"] = self.customer.student_cards.all() + data = self.get_template_data(self.request, self.customer) + context.update(data.context) return context def get_success_url(self, **kwargs): diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 0a7ce5fe..9fd39f03 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-29 18:04+0100\n" +"POT-Creation-Date: 2024-12-08 00:29+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -218,7 +218,7 @@ msgstr "Compte" msgid "Company" msgstr "Entreprise" -#: accounting/models.py:307 core/models.py:338 sith/settings.py:421 +#: accounting/models.py:307 core/models.py:338 sith/settings.py:423 msgid "Other" msgstr "Autre" @@ -369,7 +369,7 @@ msgstr "Compte en banque : " #: core/templates/core/user_clubs.jinja:34 #: core/templates/core/user_clubs.jinja:63 #: core/templates/core/user_edit.jinja:62 -#: core/templates/core/user_preferences.jinja:48 +#: counter/templates/counter/fragments/create_student_card.jinja:21 #: counter/templates/counter/last_ops.jinja:35 #: counter/templates/counter/last_ops.jinja:65 #: election/templates/election/election_detail.jinja:191 @@ -517,7 +517,7 @@ msgid "Effective amount" msgstr "Montant effectif" #: accounting/templates/accounting/club_account_details.jinja:36 -#: sith/settings.py:467 +#: sith/settings.py:469 msgid "Closed" msgstr "Fermé" @@ -650,8 +650,8 @@ msgid "Done" msgstr "Effectuées" #: accounting/templates/accounting/journal_details.jinja:41 -#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:955 -#: pedagogy/templates/pedagogy/moderation.jinja:13 +#: counter/templates/counter/cash_summary_list.jinja:37 +#: counter/views/cash.py:87 pedagogy/templates/pedagogy/moderation.jinja:13 #: pedagogy/templates/pedagogy/uv_detail.jinja:142 #: trombi/templates/trombi/comment.jinja:4 #: trombi/templates/trombi/comment.jinja:8 @@ -771,7 +771,6 @@ msgstr "Opération liée : " #: core/templates/core/user_godfathers_tree.jinja:85 #: core/templates/core/user_preferences.jinja:18 #: core/templates/core/user_preferences.jinja:27 -#: core/templates/core/user_preferences.jinja:65 #: counter/templates/counter/cash_register_summary.jinja:28 #: forum/templates/forum/reply.jinja:39 #: subscription/templates/subscription/fragments/creation_form.jinja:9 @@ -951,11 +950,11 @@ msgstr "Une action est requise" msgid "You must specify at least an user or an email address" msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" -#: club/forms.py:149 counter/forms.py:203 +#: club/forms.py:149 counter/forms.py:193 msgid "Begin date" msgstr "Date de début" -#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:206 +#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:196 #: election/views.py:170 subscription/forms.py:21 msgid "End date" msgstr "Date de fin" @@ -963,15 +962,16 @@ msgstr "Date de fin" #: club/forms.py:156 club/templates/club/club_sellings.jinja:49 #: core/templates/core/user_account_detail.jinja:17 #: core/templates/core/user_account_detail.jinja:56 -#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:137 +#: counter/templates/counter/cash_summary_list.jinja:33 +#: counter/views/mixins.py:58 msgid "Counter" msgstr "Comptoir" -#: club/forms.py:163 counter/views.py:683 +#: club/forms.py:163 counter/views/mixins.py:94 msgid "Products" msgstr "Produits" -#: club/forms.py:168 counter/views.py:688 +#: club/forms.py:168 counter/views/mixins.py:99 msgid "Archived products" msgstr "Produits archivés" @@ -1334,7 +1334,7 @@ msgid "No mailing list existing for this club" msgstr "Aucune mailing liste n'existe pour ce club" #: club/templates/club/mailing.jinja:72 -#: subscription/templates/subscription/subscription.jinja:39 +#: subscription/templates/subscription/subscription.jinja:38 msgid "New member" msgstr "Nouveau membre" @@ -1426,7 +1426,8 @@ msgstr "Hebdomadaire" msgid "Call" msgstr "Appel" -#: com/models.py:67 com/models.py:174 com/models.py:248 election/models.py:12 +#: com/models.py:67 com/models.py:174 com/models.py:248 +#: core/templates/core/macros.jinja:301 election/models.py:12 #: election/models.py:114 election/models.py:152 forum/models.py:256 #: forum/models.py:310 pedagogy/models.py:97 msgid "title" @@ -1835,6 +1836,7 @@ msgid "Articles in no weekmail yet" msgstr "Articles dans aucun weekmail" #: com/templates/com/weekmail.jinja:20 com/templates/com/weekmail.jinja:49 +#: core/templates/core/macros.jinja:301 msgid "Content" msgstr "Contenu" @@ -2505,7 +2507,7 @@ msgstr "Photos" #: eboutic/templates/eboutic/eboutic_main.jinja:22 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:16 #: eboutic/templates/eboutic/eboutic_payment_result.jinja:4 -#: sith/settings.py:420 sith/settings.py:428 +#: sith/settings.py:422 sith/settings.py:430 msgid "Eboutic" msgstr "Eboutic" @@ -2583,7 +2585,7 @@ msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/file_delete_confirm.jinja:46 -#: counter/templates/counter/counter_click.jinja:121 +#: counter/templates/counter/counter_click.jinja:111 #: sas/templates/sas/ask_picture_removal.jinja:20 msgid "Cancel" msgstr "Annuler" @@ -3042,11 +3044,11 @@ msgid "Eboutic invoices" msgstr "Facture eboutic" #: core/templates/core/user_account.jinja:54 -#: core/templates/core/user_tools.jinja:58 counter/views.py:708 +#: core/templates/core/user_tools.jinja:58 counter/views/mixins.py:119 msgid "Etickets" msgstr "Etickets" -#: core/templates/core/user_account.jinja:69 core/views/user.py:638 +#: core/templates/core/user_account.jinja:69 core/views/user.py:639 msgid "User has no account" msgstr "L'utilisateur n'a pas de compte" @@ -3137,7 +3139,7 @@ msgstr "Non cotisant" #: core/templates/core/user_detail.jinja:162 #: subscription/templates/subscription/subscription.jinja:6 -#: subscription/templates/subscription/subscription.jinja:37 +#: subscription/templates/subscription/subscription.jinja:36 msgid "New subscription" msgstr "Nouvelle cotisation" @@ -3295,19 +3297,11 @@ msgstr "Vous avez déjà choisi ce Trombi: %(trombi)s." msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" -#: core/templates/core/user_preferences.jinja:39 -msgid "Student cards" -msgstr "Cartes étudiante" - -#: core/templates/core/user_preferences.jinja:54 -msgid "No student card registered." -msgstr "Aucune carte étudiante enregistrée." - -#: core/templates/core/user_preferences.jinja:56 +#: core/templates/core/user_preferences.jinja:49 msgid "" "You can add a card by asking at a counter or add it yourself here. If you " "want to manually\n" -" add a student card yourself, you'll need a NFC reader. We store " +" add a student card yourself, you'll need a NFC reader. We store " "the UID of the card which is 14 characters long." msgstr "" "Vous pouvez ajouter une carte en demandant à un comptoir ou en l'ajoutant " @@ -3377,8 +3371,8 @@ msgstr "Cotisations" msgid "Subscription stats" msgstr "Statistiques de cotisation" -#: core/templates/core/user_tools.jinja:48 counter/forms.py:176 -#: counter/views.py:678 +#: core/templates/core/user_tools.jinja:48 counter/forms.py:166 +#: counter/views/mixins.py:89 msgid "Counters" msgstr "Comptoirs" @@ -3395,12 +3389,13 @@ msgid "Product types management" msgstr "Gestion des types de produit" #: core/templates/core/user_tools.jinja:56 -#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:698 +#: counter/templates/counter/cash_summary_list.jinja:23 +#: counter/views/mixins.py:109 msgid "Cash register summaries" msgstr "Relevés de caisse" #: core/templates/core/user_tools.jinja:57 -#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:703 +#: counter/templates/counter/invoices_call.jinja:4 counter/views/mixins.py:114 msgid "Invoices call" msgstr "Appels à facture" @@ -3548,7 +3543,7 @@ msgstr "Parrain / Marraine" msgid "Godchild" msgstr "Fillot / Fillote" -#: core/views/forms.py:310 counter/forms.py:82 trombi/views.py:151 +#: core/views/forms.py:310 counter/forms.py:80 trombi/views.py:151 msgid "Select user" msgstr "Choisir un utilisateur" @@ -3601,15 +3596,15 @@ msgstr "Galaxie" msgid "counter" msgstr "comptoir" -#: counter/forms.py:63 +#: counter/forms.py:61 msgid "This UID is invalid" msgstr "Cet UID est invalide" -#: counter/forms.py:111 +#: counter/forms.py:109 msgid "User not found" msgstr "Utilisateur non trouvé" -#: counter/management/commands/dump_accounts.py:141 +#: counter/management/commands/dump_accounts.py:148 msgid "Your AE account has been emptied" msgstr "Votre compte AE a été vidé" @@ -3637,7 +3632,7 @@ msgstr "client" msgid "customers" msgstr "clients" -#: counter/models.py:110 counter/views.py:261 +#: counter/models.py:110 counter/views/click.py:66 msgid "Not enough money" msgstr "Solde insuffisant" @@ -3777,8 +3772,8 @@ msgstr "quantité" msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:797 sith/settings.py:413 sith/settings.py:418 -#: sith/settings.py:438 +#: counter/models.py:797 sith/settings.py:415 sith/settings.py:420 +#: sith/settings.py:440 msgid "Credit card" msgstr "Carte bancaire" @@ -3910,7 +3905,8 @@ msgstr "Liste des relevés de caisse" msgid "Theoric sums" msgstr "Sommes théoriques" -#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:956 +#: counter/templates/counter/cash_summary_list.jinja:36 +#: counter/views/cash.py:88 msgid "Emptied" msgstr "Coffre vidé" @@ -3922,17 +3918,14 @@ msgstr "oui" msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." -#: counter/templates/counter/counter_click.jinja:35 -msgid "Add a student card" -msgstr "Ajouter une carte étudiante" +#: counter/templates/counter/counter_click.jinja:46 +#: launderette/templates/launderette/launderette_admin.jinja:8 +msgid "Selling" +msgstr "Vente" -#: counter/templates/counter/counter_click.jinja:38 -msgid "This is not a valid student card UID" -msgstr "Ce n'est pas un UID de carte étudiante valide" - -#: counter/templates/counter/counter_click.jinja:40 -#: counter/templates/counter/counter_click.jinja:67 -#: counter/templates/counter/counter_click.jinja:132 +#: counter/templates/counter/counter_click.jinja:57 +#: counter/templates/counter/counter_click.jinja:122 +#: counter/templates/counter/fragments/create_student_card.jinja:10 #: counter/templates/counter/invoices_call.jinja:16 #: launderette/templates/launderette/launderette_admin.jinja:35 #: launderette/templates/launderette/launderette_click.jinja:13 @@ -3941,29 +3934,16 @@ msgstr "Ce n'est pas un UID de carte étudiante valide" msgid "Go" msgstr "Valider" -#: counter/templates/counter/counter_click.jinja:42 -msgid "Registered cards" -msgstr "Cartes enregistrées" - -#: counter/templates/counter/counter_click.jinja:51 -msgid "No card registered" -msgstr "Aucune carte enregistrée" - -#: counter/templates/counter/counter_click.jinja:56 -#: launderette/templates/launderette/launderette_admin.jinja:8 -msgid "Selling" -msgstr "Vente" - -#: counter/templates/counter/counter_click.jinja:74 +#: counter/templates/counter/counter_click.jinja:64 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:19 msgid "Basket: " msgstr "Panier : " -#: counter/templates/counter/counter_click.jinja:115 +#: counter/templates/counter/counter_click.jinja:105 msgid "Finish" msgstr "Terminer" -#: counter/templates/counter/counter_click.jinja:125 +#: counter/templates/counter/counter_click.jinja:115 #: counter/templates/counter/refilling_list.jinja:9 msgid "Refilling" msgstr "Rechargement" @@ -4047,6 +4027,18 @@ msgstr "Nouveau eticket" msgid "There is no eticket in this website." msgstr "Il n'y a pas de eticket sur ce site web." +#: counter/templates/counter/fragments/create_student_card.jinja:2 +msgid "Add a student card" +msgstr "Ajouter une carte étudiante" + +#: counter/templates/counter/fragments/create_student_card.jinja:13 +msgid "Registered cards" +msgstr "Cartes enregistrées" + +#: counter/templates/counter/fragments/create_student_card.jinja:27 +msgid "No student card registered." +msgstr "Aucune carte étudiante enregistrée." + #: counter/templates/counter/invoices_call.jinja:8 #, python-format msgid "Invoices call for %(date)s" @@ -4219,104 +4211,104 @@ msgstr "Temps" msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" -#: counter/views.py:147 -msgid "Cash summary" -msgstr "Relevé de caisse" - -#: counter/views.py:156 -msgid "Last operations" -msgstr "Dernières opérations" - -#: counter/views.py:203 -msgid "Bad credentials" -msgstr "Mauvais identifiants" - -#: counter/views.py:205 -msgid "User is not barman" -msgstr "L'utilisateur n'est pas barman." - -#: counter/views.py:210 -msgid "Bad location, someone is already logged in somewhere else" -msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" - -#: counter/views.py:252 -msgid "Too young for that product" -msgstr "Trop jeune pour ce produit" - -#: counter/views.py:255 -msgid "Not allowed for that product" -msgstr "Non autorisé pour ce produit" - -#: counter/views.py:258 -msgid "No date of birth provided" -msgstr "Pas de date de naissance renseignée" - -#: counter/views.py:546 -msgid "You have not enough money to buy all the basket" -msgstr "Vous n'avez pas assez d'argent pour acheter le panier" - -#: counter/views.py:673 -msgid "Counter administration" -msgstr "Administration des comptoirs" - -#: counter/views.py:693 -msgid "Product types" -msgstr "Types de produit" - -#: counter/views.py:913 +#: counter/views/cash.py:45 msgid "10 cents" msgstr "10 centimes" -#: counter/views.py:914 +#: counter/views/cash.py:46 msgid "20 cents" msgstr "20 centimes" -#: counter/views.py:915 +#: counter/views/cash.py:47 msgid "50 cents" msgstr "50 centimes" -#: counter/views.py:916 +#: counter/views/cash.py:48 msgid "1 euro" msgstr "1 €" -#: counter/views.py:917 +#: counter/views/cash.py:49 msgid "2 euros" msgstr "2 €" -#: counter/views.py:918 +#: counter/views/cash.py:50 msgid "5 euros" msgstr "5 €" -#: counter/views.py:919 +#: counter/views/cash.py:51 msgid "10 euros" msgstr "10 €" -#: counter/views.py:920 +#: counter/views/cash.py:52 msgid "20 euros" msgstr "20 €" -#: counter/views.py:921 +#: counter/views/cash.py:53 msgid "50 euros" msgstr "50 €" -#: counter/views.py:923 +#: counter/views/cash.py:55 msgid "100 euros" msgstr "100 €" -#: counter/views.py:926 counter/views.py:932 counter/views.py:938 -#: counter/views.py:944 counter/views.py:950 +#: counter/views/cash.py:58 counter/views/cash.py:64 counter/views/cash.py:70 +#: counter/views/cash.py:76 counter/views/cash.py:82 msgid "Check amount" msgstr "Montant du chèque" -#: counter/views.py:929 counter/views.py:935 counter/views.py:941 -#: counter/views.py:947 counter/views.py:953 +#: counter/views/cash.py:61 counter/views/cash.py:67 counter/views/cash.py:73 +#: counter/views/cash.py:79 counter/views/cash.py:85 msgid "Check quantity" msgstr "Nombre de chèque" -#: counter/views.py:1473 +#: counter/views/click.py:57 +msgid "Too young for that product" +msgstr "Trop jeune pour ce produit" + +#: counter/views/click.py:60 +msgid "Not allowed for that product" +msgstr "Non autorisé pour ce produit" + +#: counter/views/click.py:63 +msgid "No date of birth provided" +msgstr "Pas de date de naissance renseignée" + +#: counter/views/click.py:331 +msgid "You have not enough money to buy all the basket" +msgstr "Vous n'avez pas assez d'argent pour acheter le panier" + +#: counter/views/eticket.py:120 msgid "people(s)" msgstr "personne(s)" +#: counter/views/home.py:74 +msgid "Bad credentials" +msgstr "Mauvais identifiants" + +#: counter/views/home.py:76 +msgid "User is not barman" +msgstr "L'utilisateur n'est pas barman." + +#: counter/views/home.py:81 +msgid "Bad location, someone is already logged in somewhere else" +msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" + +#: counter/views/mixins.py:68 +msgid "Cash summary" +msgstr "Relevé de caisse" + +#: counter/views/mixins.py:77 +msgid "Last operations" +msgstr "Dernières opérations" + +#: counter/views/mixins.py:84 +msgid "Counter administration" +msgstr "Administration des comptoirs" + +#: counter/views/mixins.py:104 +msgid "Product types" +msgstr "Types de produit" + #: eboutic/forms.py:88 msgid "The request was badly formatted." msgstr "La requête a été mal formatée." @@ -4894,12 +4886,12 @@ msgid "Washing and drying" msgstr "Lavage et séchage" #: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:656 +#: sith/settings.py:658 msgid "Washing" msgstr "Lavage" #: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:656 +#: sith/settings.py:658 msgid "Drying" msgstr "Séchage" @@ -5414,380 +5406,380 @@ msgstr "Personne(s)" msgid "Identify users on pictures" msgstr "Identifiez les utilisateurs sur les photos" -#: sith/settings.py:254 sith/settings.py:475 +#: sith/settings.py:253 sith/settings.py:477 msgid "English" msgstr "Anglais" -#: sith/settings.py:254 sith/settings.py:474 +#: sith/settings.py:253 sith/settings.py:476 msgid "French" msgstr "Français" -#: sith/settings.py:394 +#: sith/settings.py:396 msgid "TC" msgstr "TC" -#: sith/settings.py:395 +#: sith/settings.py:397 msgid "IMSI" msgstr "IMSI" -#: sith/settings.py:396 +#: sith/settings.py:398 msgid "IMAP" msgstr "IMAP" -#: sith/settings.py:397 +#: sith/settings.py:399 msgid "INFO" msgstr "INFO" -#: sith/settings.py:398 +#: sith/settings.py:400 msgid "GI" msgstr "GI" -#: sith/settings.py:399 sith/settings.py:485 +#: sith/settings.py:401 sith/settings.py:487 msgid "E" msgstr "E" -#: sith/settings.py:400 +#: sith/settings.py:402 msgid "EE" msgstr "EE" -#: sith/settings.py:401 +#: sith/settings.py:403 msgid "GESC" msgstr "GESC" -#: sith/settings.py:402 +#: sith/settings.py:404 msgid "GMC" msgstr "GMC" -#: sith/settings.py:403 +#: sith/settings.py:405 msgid "MC" msgstr "MC" -#: sith/settings.py:404 +#: sith/settings.py:406 msgid "EDIM" msgstr "EDIM" -#: sith/settings.py:405 +#: sith/settings.py:407 msgid "Humanities" msgstr "Humanités" -#: sith/settings.py:406 +#: sith/settings.py:408 msgid "N/A" msgstr "N/A" -#: sith/settings.py:410 sith/settings.py:417 sith/settings.py:436 +#: sith/settings.py:412 sith/settings.py:419 sith/settings.py:438 msgid "Check" msgstr "Chèque" -#: sith/settings.py:411 sith/settings.py:419 sith/settings.py:437 +#: sith/settings.py:413 sith/settings.py:421 sith/settings.py:439 msgid "Cash" msgstr "Espèces" -#: sith/settings.py:412 +#: sith/settings.py:414 msgid "Transfert" msgstr "Virement" -#: sith/settings.py:425 +#: sith/settings.py:427 msgid "Belfort" msgstr "Belfort" -#: sith/settings.py:426 +#: sith/settings.py:428 msgid "Sevenans" msgstr "Sevenans" -#: sith/settings.py:427 +#: sith/settings.py:429 msgid "Montbéliard" msgstr "Montbéliard" -#: sith/settings.py:455 +#: sith/settings.py:457 msgid "Free" msgstr "Libre" -#: sith/settings.py:456 +#: sith/settings.py:458 msgid "CS" msgstr "CS" -#: sith/settings.py:457 +#: sith/settings.py:459 msgid "TM" msgstr "TM" -#: sith/settings.py:458 +#: sith/settings.py:460 msgid "OM" msgstr "OM" -#: sith/settings.py:459 +#: sith/settings.py:461 msgid "QC" msgstr "QC" -#: sith/settings.py:460 +#: sith/settings.py:462 msgid "EC" msgstr "EC" -#: sith/settings.py:461 +#: sith/settings.py:463 msgid "RN" msgstr "RN" -#: sith/settings.py:462 +#: sith/settings.py:464 msgid "ST" msgstr "ST" -#: sith/settings.py:463 +#: sith/settings.py:465 msgid "EXT" msgstr "EXT" -#: sith/settings.py:468 +#: sith/settings.py:470 msgid "Autumn" msgstr "Automne" -#: sith/settings.py:469 +#: sith/settings.py:471 msgid "Spring" msgstr "Printemps" -#: sith/settings.py:470 +#: sith/settings.py:472 msgid "Autumn and spring" msgstr "Automne et printemps" -#: sith/settings.py:476 +#: sith/settings.py:478 msgid "German" msgstr "Allemand" -#: sith/settings.py:477 +#: sith/settings.py:479 msgid "Spanish" msgstr "Espagnol" -#: sith/settings.py:481 +#: sith/settings.py:483 msgid "A" msgstr "A" -#: sith/settings.py:482 +#: sith/settings.py:484 msgid "B" msgstr "B" -#: sith/settings.py:483 +#: sith/settings.py:485 msgid "C" msgstr "C" -#: sith/settings.py:484 +#: sith/settings.py:486 msgid "D" msgstr "D" -#: sith/settings.py:486 +#: sith/settings.py:488 msgid "FX" msgstr "FX" -#: sith/settings.py:487 +#: sith/settings.py:489 msgid "F" msgstr "F" -#: sith/settings.py:488 +#: sith/settings.py:490 msgid "Abs" msgstr "Abs" -#: sith/settings.py:492 +#: sith/settings.py:494 msgid "Selling deletion" msgstr "Suppression de vente" -#: sith/settings.py:493 +#: sith/settings.py:495 msgid "Refilling deletion" msgstr "Suppression de rechargement" -#: sith/settings.py:537 +#: sith/settings.py:539 msgid "One semester" msgstr "Un semestre, 20 €" -#: sith/settings.py:538 +#: sith/settings.py:540 msgid "Two semesters" msgstr "Deux semestres, 35 €" -#: sith/settings.py:540 +#: sith/settings.py:542 msgid "Common core cursus" msgstr "Cursus tronc commun, 60 €" -#: sith/settings.py:544 +#: sith/settings.py:546 msgid "Branch cursus" msgstr "Cursus branche, 60 €" -#: sith/settings.py:545 +#: sith/settings.py:547 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:546 +#: sith/settings.py:548 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:547 +#: sith/settings.py:549 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:548 +#: sith/settings.py:550 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:549 +#: sith/settings.py:551 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:550 +#: sith/settings.py:552 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:551 +#: sith/settings.py:553 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 20 €" -#: sith/settings.py:553 +#: sith/settings.py:555 msgid "One semester Welcome Week" msgstr "Un semestre Welcome Week" -#: sith/settings.py:557 +#: sith/settings.py:559 msgid "One month for free" msgstr "Un mois gratuit" -#: sith/settings.py:558 +#: sith/settings.py:560 msgid "Two months for free" msgstr "Deux mois gratuits" -#: sith/settings.py:559 +#: sith/settings.py:561 msgid "Eurok's volunteer" msgstr "Bénévole Eurockéennes" -#: sith/settings.py:561 +#: sith/settings.py:563 msgid "Six weeks for free" msgstr "6 semaines gratuites" -#: sith/settings.py:565 +#: sith/settings.py:567 msgid "One day" msgstr "Un jour" -#: sith/settings.py:566 +#: sith/settings.py:568 msgid "GA staff member" msgstr "Membre staff GA (2 semaines), 1 €" -#: sith/settings.py:569 +#: sith/settings.py:571 msgid "One semester (-20%)" msgstr "Un semestre (-20%), 12 €" -#: sith/settings.py:574 +#: sith/settings.py:576 msgid "Two semesters (-20%)" msgstr "Deux semestres (-20%), 22 €" -#: sith/settings.py:579 +#: sith/settings.py:581 msgid "Common core cursus (-20%)" msgstr "Cursus tronc commun (-20%), 36 €" -#: sith/settings.py:584 +#: sith/settings.py:586 msgid "Branch cursus (-20%)" msgstr "Cursus branche (-20%), 36 €" -#: sith/settings.py:589 +#: sith/settings.py:591 msgid "Alternating cursus (-20%)" msgstr "Cursus alternant (-20%), 24 €" -#: sith/settings.py:595 +#: sith/settings.py:597 msgid "One year for free(CA offer)" msgstr "Une année offerte (Offre CA)" -#: sith/settings.py:615 +#: sith/settings.py:617 msgid "President" msgstr "Président⸱e" -#: sith/settings.py:616 +#: sith/settings.py:618 msgid "Vice-President" msgstr "Vice-Président⸱e" -#: sith/settings.py:617 +#: sith/settings.py:619 msgid "Treasurer" msgstr "Trésorier⸱e" -#: sith/settings.py:618 +#: sith/settings.py:620 msgid "Communication supervisor" msgstr "Responsable communication" -#: sith/settings.py:619 +#: sith/settings.py:621 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:620 +#: sith/settings.py:622 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:621 +#: sith/settings.py:623 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:622 +#: sith/settings.py:624 msgid "Active member" msgstr "Membre actif⸱ve" -#: sith/settings.py:623 +#: sith/settings.py:625 msgid "Curious" msgstr "Curieux⸱euse" -#: sith/settings.py:660 +#: sith/settings.py:662 msgid "A new poster needs to be moderated" msgstr "Une nouvelle affiche a besoin d'être modérée" -#: sith/settings.py:661 +#: sith/settings.py:663 msgid "A new mailing list needs to be moderated" msgstr "Une nouvelle mailing list a besoin d'être modérée" -#: sith/settings.py:664 +#: sith/settings.py:666 msgid "A new pedagogy comment has been signaled for moderation" msgstr "" "Un nouveau commentaire de la pédagogie a été signalé pour la modération" -#: sith/settings.py:666 +#: sith/settings.py:668 #, python-format msgid "There are %s fresh news to be moderated" msgstr "Il y a %s nouvelles toutes fraîches à modérer" -#: sith/settings.py:667 +#: sith/settings.py:669 msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:668 +#: sith/settings.py:670 #, python-format msgid "There are %s pictures to be moderated in the SAS" msgstr "Il y a %s photos à modérer dans le SAS" -#: sith/settings.py:669 +#: sith/settings.py:671 msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:670 +#: sith/settings.py:672 #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s€" -#: sith/settings.py:671 +#: sith/settings.py:673 #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:672 +#: sith/settings.py:674 msgid "You have a notification" msgstr "Vous avez une notification" -#: sith/settings.py:684 +#: sith/settings.py:686 msgid "Success!" msgstr "Succès !" -#: sith/settings.py:685 +#: sith/settings.py:687 msgid "Fail!" msgstr "Échec !" -#: sith/settings.py:686 +#: sith/settings.py:688 msgid "You successfully posted an article in the Weekmail" msgstr "Article posté avec succès dans le Weekmail" -#: sith/settings.py:687 +#: sith/settings.py:689 msgid "You successfully edited an article in the Weekmail" msgstr "Article édité avec succès dans le Weekmail" -#: sith/settings.py:688 +#: sith/settings.py:690 msgid "You successfully sent the Weekmail" msgstr "Weekmail envoyé avec succès" -#: sith/settings.py:696 +#: sith/settings.py:698 msgid "AE tee-shirt" msgstr "Tee-shirt AE" @@ -5828,27 +5820,14 @@ msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période" msgid "Subscription created for %(user)s" msgstr "Cotisation créée pour %(user)s" -#: subscription/templates/subscription/fragments/creation_success.jinja:8 -#, python-format -msgid "" -"%(user)s received its new %(type)s subscription. It will be active until " -"%(end)s included." -msgstr "" -"%(user)s a reçu sa nouvelle cotisaton %(type)s. Elle sert active jusqu'au " -"%(end)s inclu." - -#: subscription/templates/subscription/fragments/creation_success.jinja:16 +#: subscription/templates/subscription/fragments/creation_success.jinja:19 msgid "Go to user profile" msgstr "Voir le profil de l'utilisateur" -#: subscription/templates/subscription/fragments/creation_success.jinja:24 +#: subscription/templates/subscription/fragments/creation_success.jinja:27 msgid "Create another subscription" msgstr "Créer une nouvelle cotisation" -#: subscription/templates/subscription/subscription.jinja -msgid "Existing member" -msgstr "Membre existant" - #: subscription/templates/subscription/stats.jinja:27 msgid "Total subscriptions" msgstr "Cotisations totales" @@ -5857,6 +5836,10 @@ msgstr "Cotisations totales" msgid "Subscriptions by type" msgstr "Cotisations par type" +#: subscription/templates/subscription/subscription.jinja:38 +msgid "Existing member" +msgstr "Membre existant" + #: trombi/models.py:55 msgid "subscription deadline" msgstr "fin des inscriptions" From 9acb421b2ebc33f5d82dc8f1e71430b20eda2190 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 8 Dec 2024 11:17:27 +0100 Subject: [PATCH 31/53] deps: update ruff --- .pre-commit-config.yaml | 2 +- launderette/views.py | 6 ++---- poetry.lock | 42 ++++++++++++++++++++--------------------- pyproject.toml | 2 +- sith/tests.py | 5 +++-- 5 files changed, 28 insertions(+), 29 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d0b57f5..9635f891 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.6.9 + rev: v0.8.2 hooks: - id: ruff # just check the code, and print the errors - id: ruff # actually fix the fixable errors, but print nothing diff --git a/launderette/views.py b/launderette/views.py index efb001e0..7886d1e7 100644 --- a/launderette/views.py +++ b/launderette/views.py @@ -145,10 +145,8 @@ class LaunderetteBookView(CanViewMixin, DetailView): and self.check_slot("WASHING", h) and self.check_slot("DRYING", h + timedelta(hours=1)) ) - or self.slot_type == "WASHING" - and self.check_slot("WASHING", h) - or self.slot_type == "DRYING" - and self.check_slot("DRYING", h) + or (self.slot_type == "WASHING" and self.check_slot("WASHING", h)) + or (self.slot_type == "DRYING" and self.check_slot("DRYING", h)) ): free = True if free and datetime.now().replace(tzinfo=tz.utc) < h: diff --git a/poetry.lock b/poetry.lock index b622f423..57eb15e1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "alabaster" @@ -2250,29 +2250,29 @@ files = [ [[package]] name = "ruff" -version = "0.6.9" +version = "0.8.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, - {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, - {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, - {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, - {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, - {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, - {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, - {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, - {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, + {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, + {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, + {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, + {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, + {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, + {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, + {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, ] [[package]] @@ -2681,4 +2681,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "e64ed169395d2c32672a2f2ad6a40d0910e4a51941b564fbdc505db6332084d2" +content-hash = "0588ca7bc2672d53d4f1a624fd2db47b040cd27f48fa56e9a9728d2b53e81295" diff --git a/pyproject.toml b/pyproject.toml index 40086159..b527393c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ optional = true django-debug-toolbar = "^4.4.6" ipython = "^8.26.0" pre-commit = "^4.0.1" -ruff = "^0.6.9" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml +ruff = "^0.8.2" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml djhtml = "^3.0.6" faker = "^30.3.0" rjsmin = "^1.2.2" diff --git a/sith/tests.py b/sith/tests.py index c205fe93..e99eeff2 100644 --- a/sith/tests.py +++ b/sith/tests.py @@ -26,7 +26,8 @@ def test_sentry_debug_endpoint( expected_error: RaisesContext[ZeroDivisionError] | does_not_raise[None], expected_return_code: int | None, ): - with expected_error, override_settings( - SENTRY_DSN=sentry_dsn, SENTRY_ENV=sentry_env + with ( + expected_error, + override_settings(SENTRY_DSN=sentry_dsn, SENTRY_ENV=sentry_env), ): assert client.get(reverse("sentry-debug")).status_code == expected_return_code From de7aa6f6a6e08bd790ca59a3e3461924f47eda17 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 8 Dec 2024 11:45:16 +0100 Subject: [PATCH 32/53] Create a generic form fragment renderer --- core/templates/core/user_preferences.jinja | 22 +- core/utils.py | 25 +- core/views/user.py | 4 +- counter/templates/counter/counter_click.jinja | 229 +++++++++--------- .../fragments/create_student_card.jinja | 2 +- counter/views/click.py | 4 +- counter/views/student_card.py | 22 +- 7 files changed, 150 insertions(+), 158 deletions(-) diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index d70371a2..722e7c44 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -36,19 +36,11 @@ {% if student_card %} - {% with - form=student_card.form, - action=student_card.context.action, - customer=student_card.context.customer, - student_cards=student_card.context.student_cards - %} - {% include student_card.template %} - {% endwith %} - -

    - {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually - add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} -

    -{% endif %} -
    + {{ student_card }} +

    + {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually + add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} +

    + {% endif %} +
    {% endblock %} \ No newline at end of file diff --git a/core/utils.py b/core/utils.py index 5b6191f6..cdd72fa6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -13,22 +13,41 @@ # # +from dataclasses import dataclass from datetime import date # Image utils from io import BytesIO -from typing import Optional +from typing import Any import PIL from django.conf import settings from django.core.files.base import ContentFile +from django.forms import BaseForm from django.http import HttpRequest +from django.template.loader import render_to_string +from django.utils.html import SafeString from django.utils.timezone import localdate from PIL import ExifTags from PIL.Image import Image, Resampling -def get_start_of_semester(today: Optional[date] = None) -> date: +@dataclass +class FormFragmentTemplateData[T: BaseForm]: + """Dataclass used to pre-render form fragments""" + + form: T + template: str + context: dict[str, Any] + + def render(self, request: HttpRequest) -> SafeString: + # Request is needed for csrf_tokens + return render_to_string( + self.template, context={"form": self.form, **self.context}, request=request + ) + + +def get_start_of_semester(today: date | None = None) -> date: """Return the date of the start of the semester of the given date. If no date is given, return the start date of the current semester. @@ -58,7 +77,7 @@ def get_start_of_semester(today: Optional[date] = None) -> date: return autumn.replace(year=autumn.year - 1) -def get_semester_code(d: Optional[date] = None) -> str: +def get_semester_code(d: date | None = None) -> str: """Return the semester code of the given date. If no date is given, return the semester code of the current semester. diff --git a/core/views/user.py b/core/views/user.py index 83916fb1..2c6b01fc 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -578,8 +578,8 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): kwargs["trombi_form"] = UserTrombiForm() if hasattr(self.object, "customer"): kwargs["student_card"] = StudentCardFormView.get_template_data( - self.request, self.object.customer - ).as_dict() + self.object.customer + ).render(self.request) return kwargs diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index ed89b100..7c36b01b 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -31,131 +31,124 @@

    {% trans %}Amount: {% endtrans %}{{ customer.amount }} €

    {% if counter.type == 'BAR' %} - {% with - form=student_card.form, - action=student_card.context.action, - customer=student_card.context.customer, - student_cards=student_card.context.student_cards - %} - {% include student_card.template %} - {% endwith %} -{% endif %} - + {{ student_card }} + {% endif %} + -
    -
    {% trans %}Selling{% endtrans %}
    -
    - {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} +
    +
    {% trans %}Selling{% endtrans %}
    +
    + {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} {# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #} -
    - {% csrf_token %} - - - - -
    - - -

    {% trans %}Basket: {% endtrans %}

    - -
      - -
    -

    - Total: - - -

    - -
    - {% csrf_token %} - - -
    -
    - {% csrf_token %} - - -
    -
    - {% if (counter.type == 'BAR' and barmens_can_refill) %} -
    {% trans %}Refilling{% endtrans %}
    -
    -
    - {% csrf_token %} - {{ refill_form.as_p() }} - - -
    -
    - {% endif %} -
    - -
    -
      - {% for category in categories.keys() -%} -
    • {{ category }}
    • - {%- endfor %} -
    - {% for category in categories.keys() -%} -
    -
    {{ category }}
    - {% for p in categories[category] -%} -
    + {% csrf_token %} - - - + + + +
    + + +

    {% trans %}Basket: {% endtrans %}

    + +
      + +
    +

    + Total: + + +

    + +
    + {% csrf_token %} + + +
    +
    + {% csrf_token %} + + +
    +
    + {% if (counter.type == 'BAR' and barmens_can_refill) %} +
    {% trans %}Refilling{% endtrans %}
    +
    +
    + {% csrf_token %} + {{ refill_form.as_p() }} + + +
    +
    + {% endif %} +
    + +
    +
      + {% for category in categories.keys() -%} +
    • {{ category }}
    • + {%- endfor %} +
    + {% for category in categories.keys() -%} +
    +
    {{ category }}
    + {% for p in categories[category] -%} +
    + {% csrf_token %} + + + +
    + {%- endfor %} +
    {%- endfor %}
    - {%- endfor %} -
    -
    + {% endblock content %} {% block script %} diff --git a/counter/templates/counter/fragments/create_student_card.jinja b/counter/templates/counter/fragments/create_student_card.jinja index 7cd05ba9..ab846c55 100644 --- a/counter/templates/counter/fragments/create_student_card.jinja +++ b/counter/templates/counter/fragments/create_student_card.jinja @@ -7,7 +7,7 @@ > {% csrf_token %} {{ form.as_p() }} - +
    {% trans %}Registered cards{% endtrans %}
    diff --git a/counter/views/click.py b/counter/views/click.py index c0845aaf..2fa9684d 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -416,6 +416,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): kwargs["refill_form"] = self.refill_form or RefillForm() kwargs["barmens_can_refill"] = self.object.can_refill() kwargs["student_card"] = StudentCardFormView.get_template_data( - self.request, self.customer - ).as_dict() + self.customer + ).render(self.request) return kwargs diff --git a/counter/views/student_card.py b/counter/views/student_card.py index fb919ce2..99f67316 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -14,31 +14,19 @@ # -from dataclasses import asdict, dataclass -from typing import Any - from django.core.exceptions import PermissionDenied from django.http import HttpRequest from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy from django.views.generic.edit import DeleteView, FormView +from core.utils import FormFragmentTemplateData from core.views import CanEditMixin from counter.forms import StudentCardForm from counter.models import Customer, StudentCard from counter.utils import is_logged_in_counter -@dataclass -class StudentCardTemplateData: - form: StudentCardForm - template: str - context: dict[str, Any] - - def as_dict(self) -> dict[str, Any]: - return asdict(self) - - class StudentCardDeleteView(DeleteView, CanEditMixin): """View used to delete a card from a user.""" @@ -64,10 +52,10 @@ class StudentCardFormView(FormView): @classmethod def get_template_data( - cls, request: HttpRequest, customer: Customer - ) -> StudentCardTemplateData: + cls, customer: Customer + ) -> FormFragmentTemplateData[form_class]: """Get necessary data to pre-render the fragment""" - return StudentCardTemplateData( + return FormFragmentTemplateData[cls.form_class]( form=cls.form_class(), template=cls.template_name, context={ @@ -99,7 +87,7 @@ class StudentCardFormView(FormView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - data = self.get_template_data(self.request, self.customer) + data = self.get_template_data(self.customer) context.update(data.context) return context From e2a34c75ea0566c90605f1cc05da530bbef0ca6f Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 8 Dec 2024 11:54:58 +0100 Subject: [PATCH 33/53] deps: update dependencies --- poetry.lock | 1373 +++++++++++++++++++++++++----------------------- pyproject.toml | 59 +-- 2 files changed, 737 insertions(+), 695 deletions(-) diff --git a/poetry.lock b/poetry.lock index 57eb15e1..d27e23f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -38,21 +38,18 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "asttokens" -version = "2.4.1" +version = "3.0.0" description = "Annotate AST trees with source code positions" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, ] -[package.dependencies] -six = ">=1.12.0" - [package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "babel" @@ -343,73 +340,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.3" +version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"}, - {file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c"}, - {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc"}, - {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e"}, - {file = "coverage-7.6.3-cp310-cp310-win32.whl", hash = "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007"}, - {file = "coverage-7.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd"}, - {file = "coverage-7.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b"}, - {file = "coverage-7.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549"}, - {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b"}, - {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f"}, - {file = "coverage-7.6.3-cp311-cp311-win32.whl", hash = "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97"}, - {file = "coverage-7.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6"}, - {file = "coverage-7.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6"}, - {file = "coverage-7.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f"}, - {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83"}, - {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167"}, - {file = "coverage-7.6.3-cp312-cp312-win32.whl", hash = "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd"}, - {file = "coverage-7.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6"}, - {file = "coverage-7.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6"}, - {file = "coverage-7.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4"}, - {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b"}, - {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4"}, - {file = "coverage-7.6.3-cp313-cp313-win32.whl", hash = "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f"}, - {file = "coverage-7.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce"}, - {file = "coverage-7.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3"}, - {file = "coverage-7.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38"}, - {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5"}, - {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91"}, - {file = "coverage-7.6.3-cp313-cp313t-win32.whl", hash = "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43"}, - {file = "coverage-7.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0"}, - {file = "coverage-7.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2"}, - {file = "coverage-7.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40"}, - {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb"}, - {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13"}, - {file = "coverage-7.6.3-cp39-cp39-win32.whl", hash = "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3"}, - {file = "coverage-7.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d"}, - {file = "coverage-7.6.3-pp39.pp310-none-any.whl", hash = "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181"}, - {file = "coverage-7.6.3.tar.gz", hash = "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, + {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, + {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, + {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, + {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, + {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, + {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, + {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, + {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, + {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, + {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, + {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, + {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, + {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, + {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, ] [package.extras] @@ -417,51 +414,53 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.1" +version = "44.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" +python-versions = "!=3.9.0,!=3.9.1,>=3.7" files = [ - {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, - {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, - {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, - {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, - {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, - {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, - {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -502,13 +501,13 @@ files = [ [[package]] name = "django" -version = "4.2.16" +version = "4.2.17" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"}, - {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"}, + {file = "Django-4.2.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0"}, + {file = "Django-4.2.17.tar.gz", hash = "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc"}, ] [package.dependencies] @@ -625,13 +624,13 @@ test = ["django-stubs", "mypy (==1.7.1)", "psycopg2-binary", "pytest", "pytest-a [[package]] name = "django-ninja-extra" -version = "0.21.4" +version = "0.21.8" description = "Django Ninja Extra - Class Based Utility and more for Django Ninja(Fast Django REST framework)" optional = false python-versions = ">=3.7" files = [ - {file = "django_ninja_extra-0.21.4-py3-none-any.whl", hash = "sha256:732317bea626ac8323b863f3f52332de011c1c1597e91104dc98f6cb68a78a5b"}, - {file = "django_ninja_extra-0.21.4.tar.gz", hash = "sha256:519db624542f300699a043bfa3d436d6d41dae55bcec5e37f871fe3797279833"}, + {file = "django_ninja_extra-0.21.8-py3-none-any.whl", hash = "sha256:3cc765d03dc10f9daba57cfd17e3da60d1471eea36078825aa9bc61f8fd1a0e2"}, + {file = "django_ninja_extra-0.21.8.tar.gz", hash = "sha256:f2df496540bcad3a1364258d62a506050893bd6528ea1fea99e10def84077de6"}, ] [package.dependencies] @@ -704,12 +703,12 @@ test = ["testfixtures"] [[package]] name = "djhtml" -version = "3.0.6" +version = "3.0.7" description = "Django/Jinja template indenter" optional = false python-versions = "*" files = [ - {file = "djhtml-3.0.6.tar.gz", hash = "sha256:abfc4d7b4730432ca6a98322fbdf8ae9d6ba254ea57ba3759a10ecb293bc57de"}, + {file = "djhtml-3.0.7.tar.gz", hash = "sha256:558c905b092a0c8afcbed27dea2f50aa6eb853a658b309e4e0f2bb378bdf6178"}, ] [package.extras] @@ -742,13 +741,13 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "faker" -version = "30.3.0" +version = "33.1.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-30.3.0-py3-none-any.whl", hash = "sha256:e8a15fd1b0f72992b008f5ea94c70d3baa0cb51b0d5a0e899c17b1d1b23d2771"}, - {file = "faker-30.3.0.tar.gz", hash = "sha256:8760fbb34564fbb2f394345eef24aec5b8f6506b6cfcefe8195ed66dd1032bdb"}, + {file = "Faker-33.1.0-py3-none-any.whl", hash = "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d"}, + {file = "faker-33.1.0.tar.gz", hash = "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4"}, ] [package.dependencies] @@ -804,13 +803,13 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "1.4.1" +version = "1.5.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.9" files = [ - {file = "griffe-1.4.1-py3-none-any.whl", hash = "sha256:84295ee0b27743bd880aea75632830ef02ded65d16124025e4c263bb826ab645"}, - {file = "griffe-1.4.1.tar.gz", hash = "sha256:911a201b01dc92e08c0e84c38a301e9da5ec067f00e7d9f2e39bc24dbfa3c176"}, + {file = "griffe-1.5.1-py3-none-any.whl", hash = "sha256:ad6a7980f8c424c9102160aafa3bcdf799df0e75f7829d75af9ee5aef656f860"}, + {file = "griffe-1.5.1.tar.gz", hash = "sha256:72964f93e08c553257706d6cd2c42d1c172213feb48b2be386f243380b405d4b"}, ] [package.dependencies] @@ -818,116 +817,131 @@ colorama = ">=0.4" [[package]] name = "hiredis" -version = "3.0.0" +version = "3.1.0" description = "Python wrapper for hiredis" optional = false python-versions = ">=3.8" files = [ - {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae"}, - {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa"}, - {file = "hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb"}, - {file = "hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543"}, - {file = "hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882"}, - {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978"}, - {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6"}, - {file = "hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126"}, - {file = "hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718"}, - {file = "hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f"}, - {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a"}, - {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1"}, - {file = "hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232"}, - {file = "hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281"}, - {file = "hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8"}, - {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895"}, - {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd"}, - {file = "hiredis-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd"}, - {file = "hiredis-3.0.0-cp38-cp38-win32.whl", hash = "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605"}, - {file = "hiredis-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4"}, - {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06"}, - {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414"}, - {file = "hiredis-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11"}, - {file = "hiredis-3.0.0-cp39-cp39-win32.whl", hash = "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83"}, - {file = "hiredis-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a"}, - {file = "hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441"}, + {file = "hiredis-3.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:2892db9db21f0cf7cc298d09f85d3e1f6dc4c4c24463ab67f79bc7a006d51867"}, + {file = "hiredis-3.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:93cfa6cc25ee2ceb0be81dc61eca9995160b9e16bdb7cca4a00607d57e998918"}, + {file = "hiredis-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2af62070aa9433802cae7be7364d5e82f76462c6a2ae34e53008b637aaa9a156"}, + {file = "hiredis-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:072c162260ebb1d892683107da22d0d5da7a1414739eae4e185cac22fe89627f"}, + {file = "hiredis-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6b232c43e89755ba332c2745ddab059c0bc1a0f01448a3a14d506f8448b1ce6"}, + {file = "hiredis-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb5316c9a65c4dde80796aa245b76011bab64eb84461a77b0a61c1bf2970bcc9"}, + {file = "hiredis-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e812a4e656bbd1c1c15c844b28259c49e26bb384837e44e8d2aa55412c91d2f7"}, + {file = "hiredis-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93a6c9230e5a5565847130c0e1005c8d3aa5ca681feb0ed542c4651323d32feb"}, + {file = "hiredis-3.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a5f65e89ce50a94d9490d5442a649c6116f53f216c8c14eb37cf9637956482b2"}, + {file = "hiredis-3.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b2d6e33601c67c074c367fdccdd6033e642284e7a56adc130f18f724c378ca8"}, + {file = "hiredis-3.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bad3b1e0c83849910f28c95953417106f539277035a4b515d1425f93947bc28f"}, + {file = "hiredis-3.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9646de31f5994e6218311dcf216e971703dbf804c510fd3f84ddb9813c495824"}, + {file = "hiredis-3.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59a9230f3aa38a33d09d8171400de202f575d7a38869e5ce2947829bca6fe359"}, + {file = "hiredis-3.1.0-cp310-cp310-win32.whl", hash = "sha256:0322d70f3328b97da14b6e98b18f0090a12ed8a8bf7ae20932e2eb9d1bb0aa2c"}, + {file = "hiredis-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:802474c18e878b3f9905e160a8b7df87d57885758083eda76c5978265acb41aa"}, + {file = "hiredis-3.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:c339ff4b4739b2a40da463763dd566129762f72926bca611ad9a457a9fe64abd"}, + {file = "hiredis-3.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:0ffa2552f704a45954627697a378fc2f559004e53055b82f00daf30bd4305330"}, + {file = "hiredis-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9acf7f0e7106f631cd618eb60ec9bbd6e43045addd5310f66ba1177209567e59"}, + {file = "hiredis-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea4f5ecf9dbea93c827486f59c606684c3496ea71c7ba9a8131932780696e61a"}, + {file = "hiredis-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39efab176fca3d5111075f6ba56cd864f18db46d858289d39360c5672e0e5c3e"}, + {file = "hiredis-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1110eae007f30e70a058d743e369c24430327cd01fd97d99519d6794a58dd587"}, + {file = "hiredis-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b390f63191bcccbb6044d4c118acdf4fa55f38e5658ac4cfd5a33a6f0c07659"}, + {file = "hiredis-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72a98ccc7b8ec9ce0100ecf59f45f05d2023606e8e3676b07a316d1c1c364072"}, + {file = "hiredis-3.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c76e751fd1e2f221dec09cdc24040ee486886e943d5d7ffc256e8cf15c75e51"}, + {file = "hiredis-3.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7d3880f213b6f14e9c69ce52beffd1748eecc8669698c4782761887273b6e1bd"}, + {file = "hiredis-3.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:87c2b3fe7e7c96eba376506a76e11514e07e848f737b254e0973e4b5c3a491e9"}, + {file = "hiredis-3.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d3cfb4089e96f8f8ee9554da93148a9261aa6612ad2cc202c1a494c7b712e31f"}, + {file = "hiredis-3.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f12018e5c5f866a1c3f7017cb2d88e5c6f9440df2281e48865a2b6c40f247f4"}, + {file = "hiredis-3.1.0-cp311-cp311-win32.whl", hash = "sha256:107b66ce977bb2dff8f2239e68344360a75d05fed3d9fa0570ac4d3020ce2396"}, + {file = "hiredis-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f1240bde53d3d1676f0aba61b3661560dc9a681cae24d9de33e650864029aa4"}, + {file = "hiredis-3.1.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:f7c7f89e0bc4246115754e2eda078a111282f6d6ecc6fb458557b724fe6f2aac"}, + {file = "hiredis-3.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:3dbf9163296fa45fbddcfc4c5900f10e9ddadda37117dbfb641e327e536b53e0"}, + {file = "hiredis-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af46a4be0e82df470f68f35316fa16cd1e134d1c5092fc1082e1aad64cce716d"}, + {file = "hiredis-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc63d698c43aea500a84d8b083f830c03808b6cf3933ae4d35a27f0a3d881652"}, + {file = "hiredis-3.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:676b3d88674134bfaaf70dac181d1790b0f33b3187bfb9da9221e17e0e624f83"}, + {file = "hiredis-3.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aed10d9df1e2fb0011db2713ac64497462e9c2c0208b648c97569da772b959ca"}, + {file = "hiredis-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b5bd8adfe8742e331a94cccd782bffea251fa70d9a709e71f4510f50794d700"}, + {file = "hiredis-3.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fc4e35b4afb0af6da55495dd0742ad32ab88150428a6ecdbb3085cbd60714e8"}, + {file = "hiredis-3.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:89b83e76eb00ab0464e7b0752a3ffcb02626e742e9509bc141424a9c3202e8dc"}, + {file = "hiredis-3.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:98ebf08c907836b70a8f40e030df8ab6f174dc7f6fa765251d813e89f14069d8"}, + {file = "hiredis-3.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6c840b9cec086328f2ee2cfee0038b5d6bbb514bac7b5e579da6e346eaac056c"}, + {file = "hiredis-3.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c5c44e9fa6f4462d0330cb5f5d46fa652512fc86b41d4d1974d0356f263e9105"}, + {file = "hiredis-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e665b14ab50aa175cfa306fcb00fffd4e3ff02ceb36ca6a4df00b1246d6a73c4"}, + {file = "hiredis-3.1.0-cp312-cp312-win32.whl", hash = "sha256:bd33db977ac7af97e8d035ffadb163b00546be22e5f1297b2123f5f9bf0f8a21"}, + {file = "hiredis-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:37aed4aa9348600145e2d019c7be27855e503ecc4906c6976ff2f3b52e3d5d97"}, + {file = "hiredis-3.1.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:b87cddd8107487863fed6994de51e5594a0be267b0b19e213694e99cdd614623"}, + {file = "hiredis-3.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:d302deff8cb63a7feffc1844e4dafc8076e566bbf10c5aaaf0f4fe791b8a6bd0"}, + {file = "hiredis-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a018340c073cf88cb635b2bedff96619df2f666018c655e7911f46fa2c1c178"}, + {file = "hiredis-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1e8ba6414ac1ae536129e18c069f3eb497df5a74e136e3566471620a4fa5f95"}, + {file = "hiredis-3.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a86b9fef256c2beb162244791fdc025aa55f936d6358e86e2020e512fe2e4972"}, + {file = "hiredis-3.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7acdc68e29a446ad17aadaff19c981a36b3bd8c894c3520412c8a7ab1c3e0de7"}, + {file = "hiredis-3.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7e06baea05de57e1e7548064f505a6964e992674fe61b8f274afe2ac93b6371"}, + {file = "hiredis-3.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35b5fc061c8a0dbfdb440053280504d6aaa8d9726bd4d1d0e1cfcbbdf0d60b73"}, + {file = "hiredis-3.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c89d2dcb271d24c44f02264233b75d5db8c58831190fa92456a90b87fa17b748"}, + {file = "hiredis-3.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:aa36688c10a08f626fddcf68c2b1b91b0e90b070c26e550a4151a877f5c2d431"}, + {file = "hiredis-3.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3982a9c16c1c4bc05a00b65d01ffb8d80ea1a7b6b533be2f1a769d3e989d2c0"}, + {file = "hiredis-3.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d1a6f889514ee2452300c9a06862fceedef22a2891f1c421a27b1ba52ef130b2"}, + {file = "hiredis-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8a45ff7915392a55d9386bb235ea1d1eb9960615f301979f02143fc20036b699"}, + {file = "hiredis-3.1.0-cp313-cp313-win32.whl", hash = "sha256:539e5bb725b62b76a5319a4e68fc7085f01349abc2316ef3df608ea0883c51d2"}, + {file = "hiredis-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9020fd7e58f489fda6a928c31355add0e665fd6b87b21954e675cf9943eafa32"}, + {file = "hiredis-3.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:b621a89fc29b3f4b01be6640ec81a6a94b5382bc78fecb876408d57a071e45aa"}, + {file = "hiredis-3.1.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:363e21fba55e1a26349dc9ca7da6b14332123879b6359bcee4a9acecb40ca33b"}, + {file = "hiredis-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c156156798729eadc9ab76ffee96c88b93cc1c3b493f4dd0a4341f53939194ee"}, + {file = "hiredis-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e38d8a325f9a6afac1b1c72d996d1add9e1b99696ce9410538ba5e9aa8fdba02"}, + {file = "hiredis-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3004ef7436feb7bfa61c0b36d422b8fb8c29aaa1a514c9405f0fdee5e9694dd3"}, + {file = "hiredis-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13f5b16f97d0bbd1c04ce367c49097d1214d60e11f9fee7ef2a9b54e0a6645c8"}, + {file = "hiredis-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:230dd0e77cb0f525f58a1306a7b4aaf078037fc5229110922332ca46f90821bb"}, + {file = "hiredis-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d968116caddd19d63120d1298e62b1bbc694db3360ed0d5df8c3a97edbc12552"}, + {file = "hiredis-3.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:511e36a6fa41d3efab3cd5cd70ac388ed825993b9e66fa3b0e47cf27a2f5ffee"}, + {file = "hiredis-3.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c5cd20804e3cb0d31e7d899d8dd091f569c33fe40d4bade670a067ab7d31c2ac"}, + {file = "hiredis-3.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:09e89e7d34cfe5ca8f7a869fca827d1af0afe8aaddb26b38c01058730edb79ad"}, + {file = "hiredis-3.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:570cbf31413c77fe5e7c157f2943ca4400493ddd9cf2184731cfcafc753becd7"}, + {file = "hiredis-3.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b9b4da8162cf289781732d6a5ba01d820c42c05943fcdb7de307d03639961db3"}, + {file = "hiredis-3.1.0-cp38-cp38-win32.whl", hash = "sha256:bc117a04bcb461d3bb1b2c5b417aee3442e1e8aa33ebc800481431f4c09fe0c5"}, + {file = "hiredis-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:34f3f5f0354db2d6797a6fb08d2c036a50af62a1d919d122c1c784304ef49347"}, + {file = "hiredis-3.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:a26fa888025badb5563f283cc19594c215a413e905729e59a5f7cf3f46d66c32"}, + {file = "hiredis-3.1.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:f50763cd819d4a52a47b5966d4bb47dee34b637c5fa6402509800eee6ecb61e6"}, + {file = "hiredis-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b6d1c9e1fce5e0a94072667ae2bf0142b89ebbb1917d3531184e060a43f3ee11"}, + {file = "hiredis-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e38d7a56b1a79ed0bbb9e6fe376d82e3f4dcc646ae47472f2c858e19a597c112"}, + {file = "hiredis-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ef5ad8b91530e4d10a68562b0a380ea22705a60e88cecee086d7c63a38564ce"}, + {file = "hiredis-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf3d2299b054e57a9f97ca08704c2843e44f29b57dc69b76a2592ecd212efe1a"}, + {file = "hiredis-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93811d60b0f73d0f049c86f4373a3833b4a38fce374ab151074d929553eb4304"}, + {file = "hiredis-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e703ff860c1d83abbcf57012b309ead02b56b60e85150c6c3bfb37cbb16ebf"}, + {file = "hiredis-3.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f9ea0678806c53d96758e74c6a898f9d506a2e3367a344757f768bef9e069366"}, + {file = "hiredis-3.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cf6844035abf47d52a1c3f4257255af3bf3b0f14d559b08eaa45885418c6c55d"}, + {file = "hiredis-3.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7acf35cfa7ec9e1e7559c04e7095628f7d06049b5f24dcb58c1a55ef6dc689f8"}, + {file = "hiredis-3.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b885695dce7a39b1fd9a609ed9c4cf312e53df2ec028d5a78af7a891b5fbea4d"}, + {file = "hiredis-3.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c22fa74ddd063396b19fe8445a1ae8b4190eff755d5750dda48e860a45b2ee7"}, + {file = "hiredis-3.1.0-cp39-cp39-win32.whl", hash = "sha256:0614e16339f1784df3bbd2800322e20b4127d3f3a3509f00a5562efddb2521aa"}, + {file = "hiredis-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:c2bc713ee73ab9de4a0d68b0ab0f29612342b63173714742437b977584adb2d8"}, + {file = "hiredis-3.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:07ab990d0835f36bf358dbb84db4541ac0a8f533128ec09af8f80a576eef2e88"}, + {file = "hiredis-3.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c54a88eb9d8ebc4e5eefaadbe2102a4f7499f9e413654172f40aefd25350959"}, + {file = "hiredis-3.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8095ef159896e5999a795b0f80e4d64281301a109e442a8d29cd750ca6bd8303"}, + {file = "hiredis-3.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f8ca13e2476ffd6d5be4763f5868133506ddcfa5ce54b4dac231ebdc19be6c6"}, + {file = "hiredis-3.1.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d25aa25c10f966d5415795ed271da84605044dbf436c054966cea5442451b3"}, + {file = "hiredis-3.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4180dc5f646b426e5fa1212e1348c167ee2a864b3a70d56579163d64a847dd1e"}, + {file = "hiredis-3.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d92144e0cd6e6e841a6ad343e9d58631626eeb4ac96b0322649379b5d4527447"}, + {file = "hiredis-3.1.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb91ba42903de637b94a1b64477f381f94ad82c0742c264f9245be76a7a3cbc"}, + {file = "hiredis-3.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ce71a797b5bc02c51da082428c00251ed6a7a67a03acbda5fbf9e8d028725f6"}, + {file = "hiredis-3.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e04c7feb9467e3170cd4d5bee381775783d81bbc45d6147c1c0ce3b50dc04f9"}, + {file = "hiredis-3.1.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a31806306a60f3565c04c964d6bee0e9d4a5120e1da589e41976b53972edf635"}, + {file = "hiredis-3.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bc51f594c2c0863ded6501642dc96701ca8bbea9ced4fa3af0a1aeda8aa634cb"}, + {file = "hiredis-3.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4663a319ab7d22c597b9421e5ea384fd583e044f2f1ca9a1b98d4fef8a0fea2f"}, + {file = "hiredis-3.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8060fa256862b0c3de64a73ab45bc1ccf381caca464f2647af9075b200828948"}, + {file = "hiredis-3.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e9445b7f117a9c8c8ccad97cb44daa55ddccff3cbc9079984eac56d982ba01f"}, + {file = "hiredis-3.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:732cf1c5cf1324f7bf3b6086976fe62a2ca98f0bf6316f31063c2c67be8797bc"}, + {file = "hiredis-3.1.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2102a94063d878c40df92f55199637a74f535e3a0b79ceba4a00538853a21be3"}, + {file = "hiredis-3.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d968dde69e3fe903bf9ef00667669dcf04a3e096e33aaf138775106ead138bc8"}, + {file = "hiredis-3.1.0.tar.gz", hash = "sha256:51d40ac3611091020d7dea6b05ed62cb152bff595fa4f931e7b6479d777acf7c"}, ] [[package]] name = "identify" -version = "2.6.1" +version = "2.6.3" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, ] [package.extras] @@ -985,13 +999,13 @@ dev = ["black (==24.3.0)", "build (==1.0.3)", "check-manifest (==0.49)", "click [[package]] name = "ipython" -version = "8.28.0" +version = "8.30.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" files = [ - {file = "ipython-8.28.0-py3-none-any.whl", hash = "sha256:530ef1e7bb693724d3cdc37287c80b07ad9b25986c007a53aa1857272dac3f35"}, - {file = "ipython-8.28.0.tar.gz", hash = "sha256:0d0d15ca1e01faeb868ef56bc7ee5a0de5bd66885735682e8a322ae289a13d1a"}, + {file = "ipython-8.30.0-py3-none-any.whl", hash = "sha256:85ec56a7e20f6c38fce7727dcca699ae4ffc85985aa7b23635a8008f918ae321"}, + {file = "ipython-8.30.0.tar.gz", hash = "sha256:cb0a405a306d2995a5cbb9901894d240784a9f341394c6ba3f4fe8c6eb89ff6e"}, ] [package.dependencies] @@ -1000,15 +1014,15 @@ decorator = "*" jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} -prompt-toolkit = ">=3.0.41,<3.1.0" +prompt_toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" -stack-data = "*" +stack_data = "*" traitlets = ">=5.13.0" [package.extras] all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] black = ["black"] -doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing_extensions"] kernel = ["ipykernel"] matplotlib = ["matplotlib"] nbconvert = ["nbconvert"] @@ -1021,22 +1035,22 @@ test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "num [[package]] name = "jedi" -version = "0.19.1" +version = "0.19.2" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, ] [package.dependencies] -parso = ">=0.8.3,<0.9.0" +parso = ">=0.8.4,<0.9.0" [package.extras] docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] [[package]] name = "jinja2" @@ -1087,72 +1101,72 @@ testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "3.0.1" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" files = [ - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, - {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -1255,13 +1269,13 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-include-markdown-plugin" -version = "6.2.2" +version = "7.1.2" description = "Mkdocs Markdown includer plugin." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "mkdocs_include_markdown_plugin-6.2.2-py3-none-any.whl", hash = "sha256:d293950f6499d2944291ca7b9bc4a60e652bbfd3e3a42b564f6cceee268694e7"}, - {file = "mkdocs_include_markdown_plugin-6.2.2.tar.gz", hash = "sha256:f2bd5026650492a581d2fd44be6c22f90391910d76582b96a34c264f2d17875d"}, + {file = "mkdocs_include_markdown_plugin-7.1.2-py3-none-any.whl", hash = "sha256:ff1175d1b4f83dea6a38e200d6f0c3db10308975bf60c197d31172671753dbc4"}, + {file = "mkdocs_include_markdown_plugin-7.1.2.tar.gz", hash = "sha256:1b393157b1aa231b0e6c59ba80f52b723f4b7827bb7a1264b505334f8542aaf1"}, ] [package.dependencies] @@ -1273,13 +1287,13 @@ cache = ["platformdirs"] [[package]] name = "mkdocs-material" -version = "9.5.40" +version = "9.5.47" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.40-py3-none-any.whl", hash = "sha256:8e7a16ada34e79a7b6459ff2602584222f522c738b6a023d1bea853d5049da6f"}, - {file = "mkdocs_material-9.5.40.tar.gz", hash = "sha256:b69d70e667ec51fc41f65e006a3184dd00d95b2439d982cb1586e4c018943156"}, + {file = "mkdocs_material-9.5.47-py3-none-any.whl", hash = "sha256:53fb9c9624e7865da6ec807d116cd7be24b3cb36ab31b1d1d1a9af58c56009a2"}, + {file = "mkdocs_material-9.5.47.tar.gz", hash = "sha256:fc3b7a8e00ad896660bd3a5cc12ca0cb28bdc2bcbe2a946b5714c23ac91b0ede"}, ] [package.dependencies] @@ -1313,13 +1327,13 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.26.2" +version = "0.27.0" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.9" files = [ - {file = "mkdocstrings-0.26.2-py3-none-any.whl", hash = "sha256:1248f3228464f3b8d1a15bd91249ce1701fe3104ac517a5f167a0e01ca850ba5"}, - {file = "mkdocstrings-0.26.2.tar.gz", hash = "sha256:34a8b50f1e6cfd29546c6c09fbe02154adfb0b361bb758834bf56aa284ba876e"}, + {file = "mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332"}, + {file = "mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657"}, ] [package.dependencies] @@ -1339,13 +1353,13 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.12.1" +version = "1.12.2" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.9" files = [ - {file = "mkdocstrings_python-1.12.1-py3-none-any.whl", hash = "sha256:205244488199c9aa2a39787ad6a0c862d39b74078ea9aa2be817bc972399563f"}, - {file = "mkdocstrings_python-1.12.1.tar.gz", hash = "sha256:60d6a5ca912c9af4ad431db6d0111ce9f79c6c48d33377dde6a05a8f5f48d792"}, + {file = "mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a"}, + {file = "mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3"}, ] [package.dependencies] @@ -1384,13 +1398,13 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -1450,106 +1464,101 @@ ptyprocess = ">=0.5" [[package]] name = "phonenumbers" -version = "8.13.47" +version = "8.13.51" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." optional = false python-versions = "*" files = [ - {file = "phonenumbers-8.13.47-py2.py3-none-any.whl", hash = "sha256:5d3c0142ef7055ca5551884352e3b6b93bfe002a0bc95b8eaba39b0e2184541b"}, - {file = "phonenumbers-8.13.47.tar.gz", hash = "sha256:53c5e7c6d431cafe4efdd44956078404ae9bc8b0eacc47be3105d3ccc88aaffa"}, + {file = "phonenumbers-8.13.51-py2.py3-none-any.whl", hash = "sha256:3bdacc0a155c8761c2a0ba7fc5632fe1541e5291ab70a4f345ab80a5742874b6"}, + {file = "phonenumbers-8.13.51.tar.gz", hash = "sha256:e8f4969841a163a3df3cb3ed8c499f0e00d58b2a1ecaa661e84e1d5fee67335f"}, ] [[package]] name = "pillow" -version = "10.4.0" +version = "11.0.0" description = "Python Imaging Library (Fork)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, - {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, - {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, - {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, - {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, - {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, - {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, - {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, - {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, - {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, - {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, - {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, - {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, - {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, - {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, - {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, - {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, - {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, - {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, - {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, + {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, + {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, + {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, + {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, + {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, + {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, + {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, + {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, + {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, + {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, + {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, + {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, + {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, + {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, + {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, + {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, + {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, + {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, + {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] @@ -1691,22 +1700,19 @@ files = [ [[package]] name = "pydantic" -version = "2.9.2" +version = "2.10.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, - {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, + {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, + {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.23.4" -typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, -] +pydantic-core = "2.27.1" +typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -1714,100 +1720,111 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.23.4" +version = "2.27.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, - {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, - {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, - {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, - {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, - {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, - {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, - {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, - {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, - {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, - {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, - {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, - {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, - {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, + {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, + {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, + {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, + {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, ] [package.dependencies] @@ -1815,31 +1832,27 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-extra-types" -version = "2.9.0" +version = "2.10.1" description = "Extra Pydantic types." optional = false python-versions = ">=3.8" -files = [] -develop = false +files = [ + {file = "pydantic_extra_types-2.10.1-py3-none-any.whl", hash = "sha256:db2c86c04a837bbac0d2d79bbd6f5d46c4c9253db11ca3fdd36a2b282575f1e2"}, + {file = "pydantic_extra_types-2.10.1.tar.gz", hash = "sha256:e4f937af34a754b8f1fa228a2fac867091a51f56ed0e8a61d5b3a6719b13c923"}, +] [package.dependencies] pydantic = ">=2.5.2" typing-extensions = "*" [package.extras] -all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "python-ulid (>=1,<2)", "python-ulid (>=1,<3)", "pytz (>=2024.1)", "semver (>=3.0.2)", "semver (>=3.0.2,<3.1.0)", "tzdata (>=2024.1)"] +all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "python-ulid (>=1,<2)", "python-ulid (>=1,<4)", "pytz (>=2024.1)", "semver (>=3.0.2)", "semver (>=3.0.2,<3.1.0)", "tzdata (>=2024.1)"] pendulum = ["pendulum (>=3.0.0,<4.0.0)"] phonenumbers = ["phonenumbers (>=8,<9)"] pycountry = ["pycountry (>=23)"] -python-ulid = ["python-ulid (>=1,<2)", "python-ulid (>=1,<3)"] +python-ulid = ["python-ulid (>=1,<2)", "python-ulid (>=1,<4)"] semver = ["semver (>=3.0.2)"] -[package.source] -type = "git" -url = "https://github.com/pydantic/pydantic-extra-types.git" -reference = "58db4b0" -resolved_reference = "58db4b096d7c90566d3d48d51b4665c01a591df6" - [[package]] name = "pygments" version = "2.18.0" @@ -1856,13 +1869,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.11.2" +version = "10.12" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.11.2-py3-none-any.whl", hash = "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf"}, - {file = "pymdown_extensions-10.11.2.tar.gz", hash = "sha256:bc8847ecc9e784a098efd35e20cba772bc5a1b529dfcef9dc1972db9021a1049"}, + {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, + {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, ] [package.dependencies] @@ -1874,13 +1887,13 @@ extra = ["pygments (>=2.12)"] [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -1894,17 +1907,17 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-cov" -version = "5.0.0" +version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} +coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] @@ -2020,13 +2033,13 @@ pyyaml = "*" [[package]] name = "redis" -version = "5.1.1" +version = "5.2.1" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.8" files = [ - {file = "redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24"}, - {file = "redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72"}, + {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, + {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, ] [package.dependencies] @@ -2038,105 +2051,105 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)" [[package]] name = "regex" -version = "2024.9.11" +version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" files = [ - {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"}, - {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"}, - {file = "regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8"}, - {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca"}, - {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a"}, - {file = "regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0"}, - {file = "regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623"}, - {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df"}, - {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268"}, - {file = "regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, - {file = "regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96"}, - {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1"}, - {file = "regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9"}, - {file = "regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf"}, - {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7"}, - {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231"}, - {file = "regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a"}, - {file = "regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a"}, - {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a"}, - {file = "regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776"}, - {file = "regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009"}, - {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784"}, - {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36"}, - {file = "regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6"}, - {file = "regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554"}, - {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8"}, - {file = "regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8"}, - {file = "regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f"}, - {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4"}, - {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e"}, - {file = "regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4"}, - {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c"}, - {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd"}, - {file = "regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771"}, - {file = "regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508"}, - {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066"}, - {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62"}, - {file = "regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9"}, - {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89"}, - {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35"}, - {file = "regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142"}, - {file = "regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919"}, - {file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, ] [[package]] @@ -2277,13 +2290,13 @@ files = [ [[package]] name = "sentry-sdk" -version = "2.16.0" +version = "2.19.2" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ - {file = "sentry_sdk-2.16.0-py2.py3-none-any.whl", hash = "sha256:49139c31ebcd398f4f6396b18910610a0c1602f6e67083240c33019d1f6aa30c"}, - {file = "sentry_sdk-2.16.0.tar.gz", hash = "sha256:90f733b32e15dfc1999e6b7aca67a38688a567329de4d6e184154a73f96c6892"}, + {file = "sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84"}, + {file = "sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d"}, ] [package.dependencies] @@ -2309,14 +2322,16 @@ grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] http2 = ["httpcore[http2] (==1.*)"] httpx = ["httpx (>=0.16.0)"] huey = ["huey (>=2)"] -huggingface-hub = ["huggingface-hub (>=0.22)"] +huggingface-hub = ["huggingface_hub (>=0.22)"] langchain = ["langchain (>=0.0.210)"] +launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] litestar = ["litestar (>=2.0.0)"] loguru = ["loguru (>=0.5)"] openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] +openfeature = ["openfeature-sdk (>=0.7.1)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] opentelemetry-experimental = ["opentelemetry-distro"] -pure-eval = ["asttokens", "executing", "pure-eval"] +pure-eval = ["asttokens", "executing", "pure_eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] @@ -2329,13 +2344,13 @@ tornado = ["tornado (>=6)"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -2479,13 +2494,13 @@ test = ["pytest"] [[package]] name = "sqlparse" -version = "0.5.1" +version = "0.5.2" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, - {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, + {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"}, + {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"}, ] [package.extras] @@ -2513,13 +2528,43 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "tomli" -version = "2.0.2" +version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -2578,13 +2623,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.6" +version = "20.28.0" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, - {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, ] [package.dependencies] @@ -2598,41 +2643,41 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "watchdog" -version = "5.0.3" +version = "6.0.0" description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" files = [ - {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea"}, - {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb"}, - {file = "watchdog-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b"}, - {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818"}, - {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490"}, - {file = "watchdog-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e"}, - {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8"}, - {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926"}, - {file = "watchdog-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e"}, - {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7"}, - {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906"}, - {file = "watchdog-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1"}, - {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:752fb40efc7cc8d88ebc332b8f4bcbe2b5cc7e881bccfeb8e25054c00c994ee3"}, - {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2e8f3f955d68471fa37b0e3add18500790d129cc7efe89971b8a4cc6fdeb0b2"}, - {file = "watchdog-5.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8ca4d854adcf480bdfd80f46fdd6fb49f91dd020ae11c89b3a79e19454ec627"}, - {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7"}, - {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8"}, - {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:223160bb359281bb8e31c8f1068bf71a6b16a8ad3d9524ca6f523ac666bb6a1e"}, - {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:560135542c91eaa74247a2e8430cf83c4342b29e8ad4f520ae14f0c8a19cfb5b"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_i686.whl", hash = "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97"}, - {file = "watchdog-5.0.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7"}, - {file = "watchdog-5.0.3-py3-none-win32.whl", hash = "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49"}, - {file = "watchdog-5.0.3-py3-none-win_amd64.whl", hash = "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9"}, - {file = "watchdog-5.0.3-py3-none-win_ia64.whl", hash = "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45"}, - {file = "watchdog-5.0.3.tar.gz", hash = "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, ] [package.extras] @@ -2681,4 +2726,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "0588ca7bc2672d53d4f1a624fd2db47b040cd27f48fa56e9a9728d2b53e81295" +content-hash = "52bd8bb78f6dd2e8a65ffe46a6dadedff9d61f0e66cc5d2c8639e5f0fb28a9c4" diff --git a/pyproject.toml b/pyproject.toml index b527393c..5d6806ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,39 +23,36 @@ license = "GPL-3.0-only" python = "^3.12" Django = "^4.2.14" django-ninja = "^1.3.0" -django-ninja-extra = "^0.21.4" -Pillow = "^10.4.0" +django-ninja-extra = "^0.21.8" +Pillow = "^11.0.0" mistune = "^3.0.2" -django-jinja = "^2.11" -cryptography = "^43.0.0" +django-jinja = "^2.11.0" +cryptography = "^44.0.0" django-phonenumber-field = "^8.0.0" -phonenumbers = "^8.13" -reportlab = "^4.2" -django-haystack = "^3.2.1" -xapian-haystack = "^3.0.1" -libsass = "^0.23" -django-ordered-model = "^3.7" +phonenumbers = "^8.13.51" +reportlab = "^4.2.5" +django-haystack = "^3.3.0" +xapian-haystack = "^3.1.0" +libsass = "^0.23.0" +django-ordered-model = "^3.7.4" django-simple-captcha = "^0.6.0" -python-dateutil = "^2.8.2" -sentry-sdk = "^2.16.0" -Jinja2 = "^3.1" +python-dateutil = "^2.9.0.post0" +sentry-sdk = "^2.19.2" +Jinja2 = "^3.1.4" django-countries = "^7.6.1" -dict2xml = "^1.7.3" +dict2xml = "^1.7.6" Sphinx = "^5" # Needed for building xapian -tomli = "^2.0.1" +tomli = "^2.2.1" django-honeypot = "^1.2.1" -# When I introduced pydantic-extra-types, I needed *right now* -# the PhoneNumberValidator class which was on the master branch but not released yet. -# Once it's released, switch this to a regular version. -pydantic-extra-types = { git = "https://github.com/pydantic/pydantic-extra-types.git", rev = "58db4b0" } +pydantic-extra-types = "^2.10.1" [tool.poetry.group.prod.dependencies] # deps used in prod, but unnecessary for development # The C extra triggers compilation against sytem libs during install. # Removing it would switch psycopg to a slower full-python implementation -psycopg = {extras = ["c"], version = "^3.2.1"} -redis = {extras = ["hiredis"], version = "^5.0.8"} +psycopg = {extras = ["c"], version = "^3.2.3"} +redis = {extras = ["hiredis"], version = "^5.2.0"} [tool.poetry.group.prod] optional = true @@ -63,28 +60,28 @@ optional = true [tool.poetry.group.dev.dependencies] # deps used for development purposes, but unneeded in prod django-debug-toolbar = "^4.4.6" -ipython = "^8.26.0" +ipython = "^8.30.0" pre-commit = "^4.0.1" ruff = "^0.8.2" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml -djhtml = "^3.0.6" -faker = "^30.3.0" -rjsmin = "^1.2.2" +djhtml = "^3.0.7" +faker = "^33.1.0" +rjsmin = "^1.2.3" [tool.poetry.group.tests.dependencies] # deps used for testing purposes freezegun = "^1.5.1" # used to test time-dependent code -pytest = "^8.3.2" -pytest-cov = "^5.0.0" +pytest = "^8.3.4" +pytest-cov = "^6.0.0" pytest-django = "^4.9.0" model-bakery = "^1.20.0" [tool.poetry.group.docs.dependencies] # deps used to work on the documentation mkdocs = "^1.6.1" -mkdocs-material = "^9.5.40" -mkdocstrings = "^0.26.2" -mkdocstrings-python = "^1.12.0" -mkdocs-include-markdown-plugin = "^6.2.2" +mkdocs-material = "^9.5.47" +mkdocstrings = "^0.27.0" +mkdocstrings-python = "^1.12.2" +mkdocs-include-markdown-plugin = "^7.1.2" [tool.poetry.group.docs] optional = true From 29a5425259bc1fbf61175ca61fe43c8cd888256d Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 8 Dec 2024 12:31:35 +0100 Subject: [PATCH 34/53] Add spinner to student card form --- core/static/bundled/htmx-index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/static/bundled/htmx-index.js b/core/static/bundled/htmx-index.js index 56edea4a..474617ac 100644 --- a/core/static/bundled/htmx-index.js +++ b/core/static/bundled/htmx-index.js @@ -1,3 +1,11 @@ import htmx from "htmx.org"; +document.body.addEventListener("htmx:beforeRequest", (event) => { + event.target.ariaBusy = true; +}); + +document.body.addEventListener("htmx:afterRequest", (event) => { + event.originalTarget.ariaBusy = null; +}); + Object.assign(window, { htmx }); From f0bc502ec98bf91863856aaf55a2421733096f01 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 9 Dec 2024 12:15:57 +0100 Subject: [PATCH 35/53] fix translation in subscription creation success fragment --- .../subscription/fragments/creation_success.jinja | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/subscription/templates/subscription/fragments/creation_success.jinja b/subscription/templates/subscription/fragments/creation_success.jinja index 6a50c2e3..7e6e1659 100644 --- a/subscription/templates/subscription/fragments/creation_success.jinja +++ b/subscription/templates/subscription/fragments/creation_success.jinja @@ -4,13 +4,9 @@ {% trans user=subscription.member %}Subscription created for {{ user }}{% endtrans %}

    - {% trans trimmed - user=subscription.member.get_short_name(), - type=subscription.subscription_type, - end=subscription.subscription_end - %} - {{ user }} received its new {{ type }} subscription. - It will be active until {{ end }} included. + {% trans trimmed user=subscription.member.get_short_name(), type=subscription.subscription_type, end=subscription.subscription_end %} + {{ user }} received its new {{ type }} subscription. + It will be active until {{ end }} included. {% endtrans %}

    From c51e5eb6cb68621166c7b4ce9613d33c681b30e5 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 9 Dec 2024 12:09:12 +0100 Subject: [PATCH 36/53] remove `parent_product` column in the Product table --- counter/forms.py | 2 - ..._remove_product_parent_product_and_more.py | 38 ++++ counter/models.py | 22 +-- locale/fr/LC_MESSAGES/django.po | 175 ++++++++++-------- 4 files changed, 142 insertions(+), 95 deletions(-) create mode 100644 counter/migrations/0025_remove_product_parent_product_and_more.py diff --git a/counter/forms.py b/counter/forms.py index 91e7c3dc..538b387c 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -143,7 +143,6 @@ class ProductEditForm(forms.ModelForm): "description", "product_type", "code", - "parent_product", "buying_groups", "purchase_price", "selling_price", @@ -155,7 +154,6 @@ class ProductEditForm(forms.ModelForm): "archived", ] widgets = { - "parent_product": AutoCompleteSelectMultipleProduct, "product_type": AutoCompleteSelect, "buying_groups": AutoCompleteSelectMultipleGroup, "club": AutoCompleteSelectClub, diff --git a/counter/migrations/0025_remove_product_parent_product_and_more.py b/counter/migrations/0025_remove_product_parent_product_and_more.py new file mode 100644 index 00000000..64a2129c --- /dev/null +++ b/counter/migrations/0025_remove_product_parent_product_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.17 on 2024-12-09 11:07 + +from django.db import migrations, models + +import accounting.models + + +class Migration(migrations.Migration): + dependencies = [("counter", "0024_accountdump_accountdump_unique_ongoing_dump")] + + operations = [ + migrations.RemoveField(model_name="product", name="parent_product"), + migrations.AlterField( + model_name="product", + name="description", + field=models.TextField(default="", verbose_name="description"), + ), + migrations.AlterField( + model_name="product", + name="purchase_price", + field=accounting.models.CurrencyField( + decimal_places=2, + help_text="Initial cost of purchasing the product", + max_digits=12, + verbose_name="purchase price", + ), + ), + migrations.AlterField( + model_name="product", + name="special_selling_price", + field=accounting.models.CurrencyField( + decimal_places=2, + help_text="Price for barmen during their permanence", + max_digits=12, + verbose_name="special selling price", + ), + ), + ] diff --git a/counter/models.py b/counter/models.py index 9c83b6c1..cf285839 100644 --- a/counter/models.py +++ b/counter/models.py @@ -326,7 +326,7 @@ class Product(models.Model): """A product, with all its related information.""" name = models.CharField(_("name"), max_length=64) - description = models.TextField(_("description"), blank=True) + description = models.TextField(_("description"), default="") product_type = models.ForeignKey( ProductType, related_name="products", @@ -336,9 +336,15 @@ class Product(models.Model): on_delete=models.SET_NULL, ) code = models.CharField(_("code"), max_length=16, blank=True) - purchase_price = CurrencyField(_("purchase price")) + purchase_price = CurrencyField( + _("purchase price"), + help_text=_("Initial cost of purchasing the product"), + ) selling_price = CurrencyField(_("selling price")) - special_selling_price = CurrencyField(_("special selling price")) + special_selling_price = CurrencyField( + _("special selling price"), + help_text=_("Price for barmen during their permanence"), + ) icon = ResizedImageField( height=70, force_format="WEBP", @@ -352,14 +358,6 @@ class Product(models.Model): ) limit_age = models.IntegerField(_("limit age"), default=0) tray = models.BooleanField(_("tray price"), default=False) - parent_product = models.ForeignKey( - "self", - related_name="children_products", - verbose_name=_("parent product"), - null=True, - blank=True, - on_delete=models.SET_NULL, - ) buying_groups = models.ManyToManyField( Group, related_name="products", verbose_name=_("buying groups"), blank=True ) @@ -369,7 +367,7 @@ class Product(models.Model): verbose_name = _("product") def __str__(self): - return "%s (%s)" % (self.name, self.code) + return f"{self.name} ({self.code})" def get_absolute_url(self): return reverse("counter:product_list") diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 9fd39f03..e122343d 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-08 00:29+0100\n" +"POT-Creation-Date: 2024-12-09 12:28+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -19,7 +19,7 @@ msgstr "" #: accounting/models.py:62 accounting/models.py:101 accounting/models.py:132 #: accounting/models.py:190 club/models.py:55 com/models.py:274 #: com/models.py:293 counter/models.py:297 counter/models.py:328 -#: counter/models.py:481 forum/models.py:60 launderette/models.py:29 +#: counter/models.py:479 forum/models.py:60 launderette/models.py:29 #: launderette/models.py:80 launderette/models.py:116 msgid "name" msgstr "nom" @@ -65,8 +65,8 @@ msgid "account number" msgstr "numéro de compte" #: accounting/models.py:107 accounting/models.py:136 club/models.py:345 -#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:351 -#: counter/models.py:483 trombi/models.py:209 +#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:357 +#: counter/models.py:481 trombi/models.py:209 msgid "club" msgstr "club" @@ -87,12 +87,12 @@ msgstr "Compte club" msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" -#: accounting/models.py:188 club/models.py:351 counter/models.py:961 +#: accounting/models.py:188 club/models.py:351 counter/models.py:959 #: election/models.py:16 launderette/models.py:165 msgid "start date" msgstr "date de début" -#: accounting/models.py:189 club/models.py:352 counter/models.py:962 +#: accounting/models.py:189 club/models.py:352 counter/models.py:960 #: election/models.py:17 msgid "end date" msgstr "date de fin" @@ -106,7 +106,7 @@ msgid "club account" msgstr "compte club" #: accounting/models.py:199 accounting/models.py:255 counter/models.py:91 -#: counter/models.py:679 +#: counter/models.py:677 msgid "amount" msgstr "montant" @@ -128,18 +128,18 @@ msgstr "classeur" #: accounting/models.py:256 core/models.py:956 core/models.py:1467 #: core/models.py:1512 core/models.py:1541 core/models.py:1565 -#: counter/models.py:689 counter/models.py:793 counter/models.py:997 +#: counter/models.py:687 counter/models.py:791 counter/models.py:995 #: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312 #: forum/models.py:413 msgid "date" msgstr "date" -#: accounting/models.py:257 counter/models.py:299 counter/models.py:998 +#: accounting/models.py:257 counter/models.py:299 counter/models.py:996 #: pedagogy/models.py:208 msgid "comment" msgstr "commentaire" -#: accounting/models.py:259 counter/models.py:691 counter/models.py:795 +#: accounting/models.py:259 counter/models.py:689 counter/models.py:793 #: subscription/models.py:56 msgid "payment method" msgstr "méthode de paiement" @@ -166,7 +166,7 @@ msgstr "type comptable" #: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460 #: accounting/models.py:492 core/models.py:1540 core/models.py:1566 -#: counter/models.py:759 +#: counter/models.py:757 msgid "label" msgstr "étiquette" @@ -375,7 +375,7 @@ msgstr "Compte en banque : " #: election/templates/election/election_detail.jinja:191 #: forum/templates/forum/macros.jinja:21 #: launderette/templates/launderette/launderette_admin.jinja:16 -#: launderette/views.py:210 pedagogy/templates/pedagogy/guide.jinja:99 +#: launderette/views.py:208 pedagogy/templates/pedagogy/guide.jinja:99 #: pedagogy/templates/pedagogy/guide.jinja:114 #: pedagogy/templates/pedagogy/uv_detail.jinja:189 #: sas/templates/sas/album.jinja:36 sas/templates/sas/moderation.jinja:18 @@ -929,7 +929,7 @@ msgstr "S'abonner" msgid "Remove" msgstr "Retirer" -#: club/forms.py:71 launderette/views.py:212 +#: club/forms.py:71 launderette/views.py:210 #: pedagogy/templates/pedagogy/moderation.jinja:15 msgid "Action" msgstr "Action" @@ -1041,7 +1041,7 @@ msgstr "Vous ne pouvez pas faire de boucles dans les clubs" msgid "A club with that unix_name already exists" msgstr "Un club avec ce nom UNIX existe déjà." -#: club/models.py:337 counter/models.py:952 counter/models.py:988 +#: club/models.py:337 counter/models.py:950 counter/models.py:986 #: eboutic/models.py:53 eboutic/models.py:189 election/models.py:183 #: launderette/models.py:130 launderette/models.py:184 sas/models.py:273 #: trombi/models.py:205 @@ -1147,7 +1147,7 @@ msgstr "Il n'y a pas de membres dans ce club." #: club/templates/club/club_members.jinja:80 #: core/templates/core/file_detail.jinja:19 core/views/forms.py:307 -#: launderette/views.py:210 trombi/templates/trombi/detail.jinja:19 +#: launderette/views.py:208 trombi/templates/trombi/detail.jinja:19 msgid "Add" msgstr "Ajouter" @@ -1573,7 +1573,7 @@ msgstr "Informations affichées" #: com/templates/com/news_admin_list.jinja:248 #: com/templates/com/news_admin_list.jinja:285 #: launderette/templates/launderette/launderette_admin.jinja:42 -#: launderette/views.py:217 +#: launderette/views.py:215 msgid "Type" msgstr "Type" @@ -1862,7 +1862,7 @@ msgstr "Supprimer du Weekmail" #: com/templates/com/weekmail_preview.jinja:9 #: core/templates/core/user_account_detail.jinja:10 -#: core/templates/core/user_account_detail.jinja:116 launderette/views.py:210 +#: core/templates/core/user_account_detail.jinja:116 launderette/views.py:208 #: pedagogy/templates/pedagogy/uv_detail.jinja:16 #: pedagogy/templates/pedagogy/uv_detail.jinja:25 #: trombi/templates/trombi/comment_moderation.jinja:10 @@ -2501,7 +2501,7 @@ msgstr "Forum" msgid "Gallery" msgstr "Photos" -#: core/templates/core/base/navbar.jinja:22 counter/models.py:491 +#: core/templates/core/base/navbar.jinja:22 counter/models.py:489 #: counter/templates/counter/counter_list.jinja:11 #: eboutic/templates/eboutic/eboutic_main.jinja:4 #: eboutic/templates/eboutic/eboutic_main.jinja:22 @@ -2585,7 +2585,7 @@ msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/file_delete_confirm.jinja:46 -#: counter/templates/counter/counter_click.jinja:111 +#: counter/templates/counter/counter_click.jinja:104 #: sas/templates/sas/ask_picture_removal.jinja:20 msgid "Cancel" msgstr "Annuler" @@ -3297,7 +3297,7 @@ msgstr "Vous avez déjà choisi ce Trombi: %(trombi)s." msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" -#: core/templates/core/user_preferences.jinja:49 +#: core/templates/core/user_preferences.jinja:41 msgid "" "You can add a card by asking at a counter or add it yourself here. If you " "want to manually\n" @@ -3591,8 +3591,8 @@ msgstr "Photos" msgid "Galaxy" msgstr "Galaxie" -#: counter/apps.py:30 counter/models.py:507 counter/models.py:958 -#: counter/models.py:994 launderette/models.py:32 +#: counter/apps.py:30 counter/models.py:505 counter/models.py:956 +#: counter/models.py:992 launderette/models.py:32 msgid "counter" msgstr "comptoir" @@ -3684,113 +3684,117 @@ msgstr "L'opération qui a vidé le compte." msgid "product type" msgstr "type du produit" -#: counter/models.py:339 +#: counter/models.py:340 msgid "purchase price" msgstr "prix d'achat" -#: counter/models.py:340 +#: counter/models.py:341 +msgid "Initial cost of purchasing the product" +msgstr "Coût initial d'achat du produit" + +#: counter/models.py:343 msgid "selling price" msgstr "prix de vente" -#: counter/models.py:341 +#: counter/models.py:345 msgid "special selling price" msgstr "prix de vente spécial" -#: counter/models.py:348 +#: counter/models.py:346 +msgid "Price for barmen during their permanence" +msgstr "Prix pour les barmen durant leur permanence" + +#: counter/models.py:354 msgid "icon" msgstr "icône" -#: counter/models.py:353 +#: counter/models.py:359 msgid "limit age" msgstr "âge limite" -#: counter/models.py:354 +#: counter/models.py:360 msgid "tray price" msgstr "prix plateau" -#: counter/models.py:358 -msgid "parent product" -msgstr "produit parent" - -#: counter/models.py:364 +#: counter/models.py:362 msgid "buying groups" msgstr "groupe d'achat" -#: counter/models.py:366 election/models.py:50 +#: counter/models.py:364 election/models.py:50 msgid "archived" msgstr "archivé" -#: counter/models.py:369 counter/models.py:1092 +#: counter/models.py:367 counter/models.py:1090 msgid "product" msgstr "produit" -#: counter/models.py:486 +#: counter/models.py:484 msgid "products" msgstr "produits" -#: counter/models.py:489 +#: counter/models.py:487 msgid "counter type" msgstr "type de comptoir" -#: counter/models.py:491 +#: counter/models.py:489 msgid "Bar" msgstr "Bar" -#: counter/models.py:491 +#: counter/models.py:489 msgid "Office" msgstr "Bureau" -#: counter/models.py:494 +#: counter/models.py:492 msgid "sellers" msgstr "vendeurs" -#: counter/models.py:502 launderette/models.py:178 +#: counter/models.py:500 launderette/models.py:178 msgid "token" msgstr "jeton" -#: counter/models.py:697 +#: counter/models.py:695 msgid "bank" msgstr "banque" -#: counter/models.py:699 counter/models.py:800 +#: counter/models.py:697 counter/models.py:798 msgid "is validated" msgstr "est validé" -#: counter/models.py:704 +#: counter/models.py:702 msgid "refilling" msgstr "rechargement" -#: counter/models.py:777 eboutic/models.py:249 +#: counter/models.py:775 eboutic/models.py:249 msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:778 counter/models.py:1072 eboutic/models.py:250 +#: counter/models.py:776 counter/models.py:1070 eboutic/models.py:250 msgid "quantity" msgstr "quantité" -#: counter/models.py:797 +#: counter/models.py:795 msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:797 sith/settings.py:415 sith/settings.py:420 +#: counter/models.py:795 sith/settings.py:415 sith/settings.py:420 #: sith/settings.py:440 msgid "Credit card" msgstr "Carte bancaire" -#: counter/models.py:805 +#: counter/models.py:803 msgid "selling" msgstr "vente" -#: counter/models.py:909 +#: counter/models.py:907 msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:910 +#: counter/models.py:908 #, python-format msgid "Eticket bought for the event %(event)s" msgstr "Eticket acheté pour l'événement %(event)s" -#: counter/models.py:912 counter/models.py:925 +#: counter/models.py:910 counter/models.py:923 #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -3802,63 +3806,63 @@ msgstr "" "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "%(url)s." -#: counter/models.py:963 +#: counter/models.py:961 msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:966 +#: counter/models.py:964 msgid "permanency" msgstr "permanence" -#: counter/models.py:999 +#: counter/models.py:997 msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:1002 +#: counter/models.py:1000 msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:1068 +#: counter/models.py:1066 msgid "cash summary" msgstr "relevé" -#: counter/models.py:1071 +#: counter/models.py:1069 msgid "value" msgstr "valeur" -#: counter/models.py:1074 +#: counter/models.py:1072 msgid "check" msgstr "chèque" -#: counter/models.py:1076 +#: counter/models.py:1074 msgid "True if this is a bank check, else False" msgstr "Vrai si c'est un chèque, sinon Faux." -#: counter/models.py:1080 +#: counter/models.py:1078 msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:1096 +#: counter/models.py:1094 msgid "banner" msgstr "bannière" -#: counter/models.py:1098 +#: counter/models.py:1096 msgid "event date" msgstr "date de l'événement" -#: counter/models.py:1100 +#: counter/models.py:1098 msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:1102 +#: counter/models.py:1100 msgid "secret" msgstr "secret" -#: counter/models.py:1141 +#: counter/models.py:1139 msgid "uid" msgstr "uid" -#: counter/models.py:1146 +#: counter/models.py:1144 msgid "student cards" msgstr "cartes étudiante" @@ -3918,13 +3922,13 @@ msgstr "oui" msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." -#: counter/templates/counter/counter_click.jinja:46 +#: counter/templates/counter/counter_click.jinja:39 #: launderette/templates/launderette/launderette_admin.jinja:8 msgid "Selling" msgstr "Vente" -#: counter/templates/counter/counter_click.jinja:57 -#: counter/templates/counter/counter_click.jinja:122 +#: counter/templates/counter/counter_click.jinja:50 +#: counter/templates/counter/counter_click.jinja:115 #: counter/templates/counter/fragments/create_student_card.jinja:10 #: counter/templates/counter/invoices_call.jinja:16 #: launderette/templates/launderette/launderette_admin.jinja:35 @@ -3934,16 +3938,16 @@ msgstr "Vente" msgid "Go" msgstr "Valider" -#: counter/templates/counter/counter_click.jinja:64 +#: counter/templates/counter/counter_click.jinja:57 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:19 msgid "Basket: " msgstr "Panier : " -#: counter/templates/counter/counter_click.jinja:105 +#: counter/templates/counter/counter_click.jinja:98 msgid "Finish" msgstr "Terminer" -#: counter/templates/counter/counter_click.jinja:115 +#: counter/templates/counter/counter_click.jinja:108 #: counter/templates/counter/refilling_list.jinja:9 msgid "Refilling" msgstr "Rechargement" @@ -4853,7 +4857,7 @@ msgstr "date d'emprunt" msgid "Token" msgstr "Jeton" -#: launderette/models.py:149 launderette/views.py:262 +#: launderette/models.py:149 launderette/views.py:260 msgid "Token name can not be blank" msgstr "Le nom du jeton ne peut pas être vide" @@ -4916,25 +4920,25 @@ msgstr "Éditer la page de présentation" msgid "Book launderette slot" msgstr "Réserver un créneau de laverie" -#: launderette/views.py:224 +#: launderette/views.py:222 msgid "Tokens, separated by spaces" msgstr "Jetons, séparés par des espaces" -#: launderette/views.py:246 +#: launderette/views.py:244 #, python-format msgid "Token %(token_name)s does not exists" msgstr "Le jeton %(token_name)s n'existe pas" -#: launderette/views.py:258 +#: launderette/views.py:256 #, python-format msgid "Token %(token_name)s already exists" msgstr "Un jeton %(token_name)s existe déjà" -#: launderette/views.py:309 +#: launderette/views.py:307 msgid "User has booked no slot" msgstr "L'utilisateur n'a pas réservé de créneau" -#: launderette/views.py:417 +#: launderette/views.py:415 msgid "Token not found" msgstr "Jeton non trouvé" @@ -5820,11 +5824,20 @@ msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période" msgid "Subscription created for %(user)s" msgstr "Cotisation créée pour %(user)s" -#: subscription/templates/subscription/fragments/creation_success.jinja:19 +#: subscription/templates/subscription/fragments/creation_success.jinja:7 +#, python-format +msgid "" +"%(user)s received its new %(type)s subscription. It will be active until " +"%(end)s included." +msgstr "" +"%(user)s a reçu sa nouvelle cotisaton %(type)s. Elle sert active jusqu'au " +"%(end)s inclu." + +#: subscription/templates/subscription/fragments/creation_success.jinja:15 msgid "Go to user profile" msgstr "Voir le profil de l'utilisateur" -#: subscription/templates/subscription/fragments/creation_success.jinja:27 +#: subscription/templates/subscription/fragments/creation_success.jinja:23 msgid "Create another subscription" msgstr "Créer une nouvelle cotisation" From fa60ecb25a9458ddc4276aa6d69dd5cb41bb10cf Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 15 Dec 2024 00:59:55 +0100 Subject: [PATCH 37/53] Upgrade dependencies --- .pre-commit-config.yaml | 4 +-- poetry.lock | 68 ++++++++++++++++++++--------------------- pyproject.toml | 8 ++--- 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9635f891..e480eda0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.8.2 + rev: v0.8.3 hooks: - id: ruff # just check the code, and print the errors - id: ruff # actually fix the fixable errors, but print nothing @@ -14,7 +14,7 @@ repos: - id: biome-check additional_dependencies: ["@biomejs/biome@1.9.3"] - repo: https://github.com/rtts/djhtml - rev: 3.0.6 + rev: 3.0.7 hooks: - id: djhtml name: format templates diff --git a/poetry.lock b/poetry.lock index d27e23f3..311df18a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -78,13 +78,13 @@ files = [ [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -425,7 +425,6 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -436,7 +435,6 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -1287,13 +1285,13 @@ cache = ["platformdirs"] [[package]] name = "mkdocs-material" -version = "9.5.47" +version = "9.5.48" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.47-py3-none-any.whl", hash = "sha256:53fb9c9624e7865da6ec807d116cd7be24b3cb36ab31b1d1d1a9af58c56009a2"}, - {file = "mkdocs_material-9.5.47.tar.gz", hash = "sha256:fc3b7a8e00ad896660bd3a5cc12ca0cb28bdc2bcbe2a946b5714c23ac91b0ede"}, + {file = "mkdocs_material-9.5.48-py3-none-any.whl", hash = "sha256:b695c998f4b939ce748adbc0d3bff73fa886a670ece948cf27818fa115dc16f8"}, + {file = "mkdocs_material-9.5.48.tar.gz", hash = "sha256:a582531e8b34f4c7ed38c29d5c44763053832cf2a32f7409567e0c74749a47db"}, ] [package.dependencies] @@ -1464,13 +1462,13 @@ ptyprocess = ">=0.5" [[package]] name = "phonenumbers" -version = "8.13.51" +version = "8.13.52" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." optional = false python-versions = "*" files = [ - {file = "phonenumbers-8.13.51-py2.py3-none-any.whl", hash = "sha256:3bdacc0a155c8761c2a0ba7fc5632fe1541e5291ab70a4f345ab80a5742874b6"}, - {file = "phonenumbers-8.13.51.tar.gz", hash = "sha256:e8f4969841a163a3df3cb3ed8c499f0e00d58b2a1ecaa661e84e1d5fee67335f"}, + {file = "phonenumbers-8.13.52-py2.py3-none-any.whl", hash = "sha256:e803210038ece9d208b129e3023dc20e656a820d6bf6f1cb0471d4164f54bada"}, + {file = "phonenumbers-8.13.52.tar.gz", hash = "sha256:fdc371ea6a4da052beb1225de63963d5a2fddbbff2bb53e3a957f360e0185f80"}, ] [[package]] @@ -2263,29 +2261,29 @@ files = [ [[package]] name = "ruff" -version = "0.8.2" +version = "0.8.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, - {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, - {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, - {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, - {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, - {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, - {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, + {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, ] [[package]] @@ -2494,13 +2492,13 @@ test = ["pytest"] [[package]] name = "sqlparse" -version = "0.5.2" +version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.2-py3-none-any.whl", hash = "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"}, - {file = "sqlparse-0.5.2.tar.gz", hash = "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f"}, + {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, + {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, ] [package.extras] @@ -2726,4 +2724,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "52bd8bb78f6dd2e8a65ffe46a6dadedff9d61f0e66cc5d2c8639e5f0fb28a9c4" +content-hash = "5836c1a8ad42645d7d045194c8c371754b19957ebdcd2aaa902a2fb3dc97cc53" diff --git a/pyproject.toml b/pyproject.toml index 5d6806ca..be892cdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ license = "GPL-3.0-only" [tool.poetry.dependencies] python = "^3.12" -Django = "^4.2.14" +Django = "^4.2.17" django-ninja = "^1.3.0" django-ninja-extra = "^0.21.8" Pillow = "^11.0.0" @@ -29,7 +29,7 @@ mistune = "^3.0.2" django-jinja = "^2.11.0" cryptography = "^44.0.0" django-phonenumber-field = "^8.0.0" -phonenumbers = "^8.13.51" +phonenumbers = "^8.13.52" reportlab = "^4.2.5" django-haystack = "^3.3.0" xapian-haystack = "^3.1.0" @@ -49,7 +49,7 @@ pydantic-extra-types = "^2.10.1" [tool.poetry.group.prod.dependencies] # deps used in prod, but unnecessary for development -# The C extra triggers compilation against sytem libs during install. +# The C extra triggers compilation against system libs during install. # Removing it would switch psycopg to a slower full-python implementation psycopg = {extras = ["c"], version = "^3.2.3"} redis = {extras = ["hiredis"], version = "^5.2.0"} @@ -62,7 +62,7 @@ optional = true django-debug-toolbar = "^4.4.6" ipython = "^8.30.0" pre-commit = "^4.0.1" -ruff = "^0.8.2" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml +ruff = "^0.8.3" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml djhtml = "^3.0.7" faker = "^33.1.0" rjsmin = "^1.2.3" From 3b7e3388089e35c872b3983c8ec1c57f31160cba Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 15 Nov 2024 14:47:39 +0100 Subject: [PATCH 38/53] fix 500 when accessing preferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand on tente d'accéder aux préférences d'un utilisateur relié à un trombi, sans être soi-même dans un trombi, on a une erreur. --- core/views/user.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/core/views/user.py b/core/views/user.py index 2c6b01fc..b758590a 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -559,10 +559,6 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): context_object_name = "profile" current_tab = "prefs" - def get_object(self, queryset=None): - user = get_object_or_404(User, pk=self.kwargs["user_id"]) - return user - def get_form_kwargs(self): kwargs = super().get_form_kwargs() pref = self.object.preferences @@ -572,9 +568,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) - if not ( - hasattr(self.object, "trombi_user") and self.request.user.trombi_user.trombi - ): + if not hasattr(self.object, "trombi_user"): kwargs["trombi_form"] = UserTrombiForm() if hasattr(self.object, "customer"): kwargs["student_card"] = StudentCardFormView.get_template_data( From 466fe587633845965e597aa5b04ea0c6cb2a25f0 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 8 Dec 2024 16:07:25 +0100 Subject: [PATCH 39/53] feat: make student card unique per user --- core/management/commands/populate.py | 9 +- core/templates/core/user_preferences.jinja | 4 +- core/views/user.py | 2 +- counter/forms.py | 4 +- .../0026_alter_studentcard_customer.py | 53 ++ counter/models.py | 14 +- counter/templates/counter/counter_click.jinja | 2 +- .../fragments/create_student_card.jinja | 43 +- counter/tests/test_customer.py | 460 +++++------------- counter/urls.py | 2 +- counter/views/click.py | 2 +- counter/views/student_card.py | 36 +- locale/fr/LC_MESSAGES/django.po | 45 +- 13 files changed, 250 insertions(+), 426 deletions(-) create mode 100644 counter/migrations/0026_alter_studentcard_customer.py diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 9e261bba..7098101a 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -69,7 +69,7 @@ class Command(BaseCommand): # sqlite doesn't support this operation return sqlcmd = StringIO() - call_command("sqlsequencereset", *args, stdout=sqlcmd) + call_command("sqlsequencereset", "--no-color", *args, stdout=sqlcmd) cursor = connection.cursor() cursor.execute(sqlcmd.getvalue()) @@ -137,11 +137,10 @@ class Command(BaseCommand): ) self.reset_index("club") + for bar_id, bar_name in settings.SITH_COUNTER_BARS: + Counter(id=bar_id, name=bar_name, club=bar_club, type="BAR").save() + self.reset_index("counter") counters = [ - *[ - Counter(id=bar_id, name=bar_name, club=bar_club, type="BAR") - for bar_id, bar_name in settings.SITH_COUNTER_BARS - ], Counter(name="Eboutic", club=main_club, type="EBOUTIC"), Counter(name="AE", club=main_club, type="OFFICE"), Counter(name="Vidage comptes AE", club=main_club, type="OFFICE"), diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index 722e7c44..bf5189ae 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -35,8 +35,8 @@ {% endif %} - {% if student_card %} - {{ student_card }} + {% if student_card_fragment %} + {{ student_card_fragment }}

    {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %} diff --git a/core/views/user.py b/core/views/user.py index b758590a..9f724fca 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -571,7 +571,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): if not hasattr(self.object, "trombi_user"): kwargs["trombi_form"] = UserTrombiForm() if hasattr(self.object, "customer"): - kwargs["student_card"] = StudentCardFormView.get_template_data( + kwargs["student_card_fragment"] = StudentCardFormView.get_template_data( self.object.customer ).render(self.request) return kwargs diff --git a/counter/forms.py b/counter/forms.py index 538b387c..0a8bb3be 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -50,9 +50,7 @@ class StudentCardForm(forms.ModelForm): class Meta: model = StudentCard fields = ["uid"] - widgets = { - "uid": NFCTextInput, - } + widgets = {"uid": NFCTextInput} def clean(self): cleaned_data = super().clean() diff --git a/counter/migrations/0026_alter_studentcard_customer.py b/counter/migrations/0026_alter_studentcard_customer.py new file mode 100644 index 00000000..f1f5cd49 --- /dev/null +++ b/counter/migrations/0026_alter_studentcard_customer.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.17 on 2024-12-08 13:30 +from operator import attrgetter + +import django.db.models.deletion +from django.db import migrations, models +from django.db.migrations.state import StateApps +from django.db.models import Count + + +def delete_duplicates(apps: StateApps, schema_editor): + """Delete cards of users with more than one student cards. + + For all users who have more than one registered student card, all + the cards except the last one are deleted. + """ + Customer = apps.get_model("counter", "Customer") + StudentCard = apps.get_model("counter", "StudentCard") + customers = ( + Customer.objects.annotate(nb_cards=Count("student_cards")) + .filter(nb_cards__gt=1) + .prefetch_related("student_cards") + ) + to_delete = [ + card.id + for customer in customers + for card in sorted(customer.student_cards.all(), key=attrgetter("id"))[:-1] + ] + StudentCard.objects.filter(id__in=to_delete).delete() + + +class Migration(migrations.Migration): + dependencies = [("counter", "0025_remove_product_parent_product_and_more")] + + operations = [ + migrations.RunPython(delete_duplicates, migrations.RunPython.noop), + migrations.AlterField( + model_name="studentcard", + name="customer", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="student_card", + to="counter.customer", + verbose_name="student card", + ), + ), + migrations.AlterModelOptions( + name="studentcard", + options={ + "verbose_name": "student card", + "verbose_name_plural": "student cards", + }, + ), + ] diff --git a/counter/models.py b/counter/models.py index cf285839..292cd59f 100644 --- a/counter/models.py +++ b/counter/models.py @@ -1138,20 +1138,22 @@ class StudentCard(models.Model): uid = models.CharField( _("uid"), max_length=UID_SIZE, unique=True, validators=[MinLengthValidator(4)] ) - customer = models.ForeignKey( + customer = models.OneToOneField( Customer, - related_name="student_cards", - verbose_name=_("student cards"), - null=False, - blank=False, + related_name="student_card", + verbose_name=_("student card"), on_delete=models.CASCADE, ) + class Meta: + verbose_name = _("student card") + verbose_name_plural = _("student cards") + def __str__(self): return self.uid @staticmethod - def is_valid(uid): + def is_valid(uid: str) -> bool: return ( (uid.isupper() or uid.isnumeric()) and len(uid) == StudentCard.UID_SIZE diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 7c36b01b..4b9e1898 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -31,7 +31,7 @@

    {% trans %}Amount: {% endtrans %}{{ customer.amount }} €

    {% if counter.type == 'BAR' %} - {{ student_card }} + {{ student_card_fragment }} {% endif %} diff --git a/counter/templates/counter/fragments/create_student_card.jinja b/counter/templates/counter/fragments/create_student_card.jinja index ab846c55..f8e59d24 100644 --- a/counter/templates/counter/fragments/create_student_card.jinja +++ b/counter/templates/counter/fragments/create_student_card.jinja @@ -1,29 +1,22 @@
    -

    {% trans %}Add a student card{% endtrans %}

    -
    - {% csrf_token %} - {{ form.as_p() }} - - -
    -
    {% trans %}Registered cards{% endtrans %}
    - {% if student_cards %} - - - {% else %} +

    {% trans %}Student card{% endtrans %}

    + {% if not customer.student_card %} +
    + {% csrf_token %} + {{ form.as_p() }} + +
    {% trans %}No student card registered.{% endtrans %} + {% else %} +

    + {% trans %}Registered{% endtrans %}   -   + + {% trans %}Delete{% endtrans %} + +

    {% endif %}
    diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py index f7e599e6..b861a97e 100644 --- a/counter/tests/test_customer.py +++ b/counter/tests/test_customer.py @@ -1,3 +1,4 @@ +import itertools import json import string from datetime import timedelta @@ -175,7 +176,6 @@ class TestStudentCard(TestCase): @classmethod def setUpTestData(cls): cls.customer = subscriber_user.make() - cls.customer.save() cls.barmen = subscriber_user.make(password=make_password("plop")) cls.board_admin = board_user.make() cls.club_admin = baker.make(User) @@ -205,6 +205,22 @@ class TestStudentCard(TestCase): {"username": self.barmen.username, "password": "plop"}, ) + def invalid_uids(self) -> list[tuple[str, str]]: + """Return a list of invalid uids, with the associated error message""" + return [ + ("8B90734A802A8", ""), # too short + ( + "8B90734A802A8FA", + "Assurez-vous que cette valeur comporte au plus 14 caractères (actuellement 15).", + ), # too long + ("8b90734a802a9f", ""), # has lowercases + (" " * 14, "Ce champ est obligatoire."), # empty + ( + self.customer.customer.student_card.uid, + "Un objet Carte étudiante avec ce champ Uid existe déjà.", + ), + ] + def test_search_user_with_student_card(self): response = self.client.post( reverse("counter:details", args=[self.counter.id]), @@ -213,396 +229,144 @@ class TestStudentCard(TestCase): assert response.url == reverse( "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.customer.id}, + kwargs={"counter_id": self.counter.id, "user_id": self.customer.pk}, ) def test_add_student_card_from_counter(self): - # Test card with mixed letters and numbers - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - }, - ), - {"uid": "8B90734A802A8F"}, - HTTP_REFERER=reverse( - "counter:click", - kwargs={ - "counter_id": self.counter.id, - "user_id": self.customer.customer.pk, - }, - ), - ) - 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:add_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - }, - ), - {"uid": "04786547890123"}, - HTTP_REFERER=reverse( - "counter:click", - kwargs={ - "counter_id": self.counter.id, - "user_id": self.customer.customer.pk, - }, - ), - ) - 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:add_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - }, - ), - {"uid": "ABCAAAFAAFAAAB"}, - HTTP_REFERER=reverse( - "counter:click", - kwargs={ - "counter_id": self.counter.id, - "user_id": self.customer.customer.pk, - }, - ), - ) - assert response.status_code == 302 - self.assertContains(self.client.get(response.url), text="ABCAAAFAAFAAAB") + for uid in ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"]: + customer = subscriber_user.make().customer + response = self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": customer.pk} + ), + {"uid": uid}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": customer.pk}, + ), + ) + assert response.status_code == 302 + customer.refresh_from_db() + assert hasattr(customer, "student_card") + assert customer.student_card.uid == uid def test_add_student_card_from_counter_fail(self): - # UID too short - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - }, - ), - {"uid": "8B90734A802A8"}, - HTTP_REFERER=reverse( - "counter:click", - kwargs={ - "counter_id": self.counter.id, - "user_id": self.customer.customer.pk, - }, - ), - ) - self.assertContains(response, text="Cet UID est invalide") - - # UID too long - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - }, - ), - {"uid": "8B90734A802A8FA"}, - HTTP_REFERER=reverse( - "counter:click", - kwargs={ - "counter_id": self.counter.id, - "user_id": self.customer.customer.pk, - }, - ), - ) - self.assertContains(response, text="Cet UID est invalide") - self.assertContains( - 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:add_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - }, - ), - {"uid": self.valid_card.uid}, - HTTP_REFERER=reverse( - "counter:click", - kwargs={ - "counter_id": self.counter.id, - "user_id": self.customer.customer.pk, - }, - ), - ) - self.assertContains(response, text="Cet UID est invalide") - self.assertContains( - response, text="Un objet Student card avec ce champ Uid existe déjà." - ) - - # Test with lowercase - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - }, - ), - {"uid": "8b90734a802a9f"}, - HTTP_REFERER=reverse( - "counter:click", - kwargs={ - "counter_id": self.counter.id, - "user_id": self.customer.customer.pk, - }, - ), - ) - self.assertContains(response, text="Cet UID est invalide") - - # Test with white spaces - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - }, - ), - {"uid": " "}, - HTTP_REFERER=reverse( - "counter:click", - kwargs={ - "counter_id": self.counter.id, - "user_id": self.customer.customer.pk, - }, - ), - ) - self.assertContains(response, text="Cet UID est invalide") - self.assertContains(response, text="Ce champ est obligatoire.") + customer = subscriber_user.make().customer + for uid, error_msg in self.invalid_uids(): + response = self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": customer.pk} + ), + {"uid": uid}, + HTTP_REFERER=reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": customer.pk}, + ), + ) + self.assertContains(response, text="Cet UID est invalide") + self.assertContains(response, text=error_msg) + customer.refresh_from_db() + assert not hasattr(customer, "student_card") 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.barmen.id}, - ) + barman = subscriber_user.make() + self.counter.sellers.add(barman) + customer = self.customer.customer + # There is someone logged to a counter + # with the client of this TestCase instance, + # so we create a new client, in order to check + # that using a client not logged to a counter + # where another client is logged still isn't authorized. + client = Client() - def send_valid_request(client, counter_id): + def send_valid_request(counter_id): return client.post( reverse( - "counter:add_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - }, + "counter:add_student_card", kwargs={"customer_id": customer.pk} ), {"uid": "8B90734A802A8F"}, HTTP_REFERER=reverse( "counter:click", - kwargs={ - "counter_id": counter_id, - "user_id": self.customer.customer.pk, - }, + kwargs={"counter_id": counter_id, "user_id": customer.pk}, ), ) - assert send_valid_request(self.client, self.counter.id).status_code == 403 + # Send to a counter where you aren't logged in + assert send_valid_request(self.counter.id).status_code == 403 # Send to a non bar counter - self.client.force_login(self.club_admin) - assert send_valid_request(self.client, self.club_counter.id).status_code == 403 + client.force_login(self.club_admin) + assert send_valid_request(self.club_counter.id).status_code == 403 def test_delete_student_card_with_owner(self): self.client.force_login(self.customer) self.client.post( reverse( "counter:delete_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - "card_id": self.customer.customer.student_cards.first().id, - }, + kwargs={"customer_id": self.customer.customer.pk}, ) ) - assert not self.customer.customer.student_cards.exists() + self.customer.customer.refresh_from_db() + assert not hasattr(self.customer.customer, "student_card") - def test_delete_student_card_with_board_member(self): - self.client.force_login(self.board_admin) - self.client.post( - reverse( - "counter:delete_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - "card_id": self.customer.customer.student_cards.first().id, - }, + def test_delete_student_card_with_admin_user(self): + """Test that AE board members and root users can delete student cards""" + for user in self.board_admin, self.root: + self.client.force_login(user) + self.client.post( + reverse( + "counter:delete_student_card", + kwargs={"customer_id": self.customer.customer.pk}, + ) ) - ) - assert not self.customer.customer.student_cards.exists() - - def test_delete_student_card_with_root(self): - self.client.force_login(self.root) - self.client.post( - reverse( - "counter:delete_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - "card_id": self.customer.customer.student_cards.first().id, - }, - ) - ) - assert not self.customer.customer.student_cards.exists() + self.customer.customer.refresh_from_db() + assert not hasattr(self.customer.customer, "student_card") def test_delete_student_card_fail(self): + """Test that non-admin users cannot delete student cards""" self.client.force_login(self.subscriber) response = self.client.post( reverse( "counter:delete_student_card", - kwargs={ - "customer_id": self.customer.customer.pk, - "card_id": self.customer.customer.student_cards.first().id, - }, + kwargs={"customer_id": self.customer.customer.pk}, ) ) assert response.status_code == 403 - assert self.customer.customer.student_cards.exists() + self.subscriber.customer.refresh_from_db() + assert not hasattr(self.subscriber.customer, "student_card") def test_add_student_card_from_user_preferences(self): - # Test with owner of the card - self.client.force_login(self.customer) - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": "8B90734A802A8F"}, - ) + users = [self.subscriber, self.board_admin, self.root] + uids = ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"] + for user, uid in itertools.product(users, uids): + self.customer.customer.student_card.delete() + self.client.force_login(user) + response = self.client.post( + reverse( + "counter:add_student_card", + kwargs={"customer_id": self.customer.customer.pk}, + ), + {"uid": uid}, + ) + assert response.status_code == 302 + response = self.client.get(response.url) - assert response.status_code == 302 - - response = self.client.get(response.url) - self.assertContains(response, text="8B90734A802A8F") - - # Test with board member - self.client.force_login(self.board_admin) - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": "8B90734A802A8A"}, - ) - - assert response.status_code == 302 - - response = self.client.get(response.url) - self.assertContains(response, text="8B90734A802A8A") - - # Test card with only numbers - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": "04786547890123"}, - ) - assert response.status_code == 302 - - response = self.client.get(response.url) - self.assertContains(response, text="04786547890123") - - # Test card with only letters - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": "ABCAAAFAAFAAAB"}, - ) - - assert response.status_code == 302 - - response = self.client.get(response.url) - self.assertContains(response, text="ABCAAAFAAFAAAB") - - # Test with root - self.client.force_login(self.root) - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": "8B90734A802A8B"}, - ) - - assert response.status_code == 302 - - response = self.client.get(response.url) - self.assertContains(response, text="8B90734A802A8B") + self.customer.customer.refresh_from_db() + assert self.customer.customer.student_card.uid == uid + self.assertContains(response, text="Enregistré") def test_add_student_card_from_user_preferences_fail(self): - self.client.force_login(self.customer) - # UID too short - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": "8B90734A802A8"}, - ) - - self.assertContains(response, text="Cet UID est invalide") - - # UID too long - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": "8B90734A802A8FA"}, - ) - self.assertContains(response, text="Cet UID est invalide") - - # Test with already existing card - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": self.valid_card.uid}, - ) - self.assertContains( - response, text="Un objet Student card avec ce champ Uid existe déjà." - ) - - # Test with lowercase - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": "8b90734a802a9f"}, - ) - self.assertContains(response, text="Cet UID est invalide") - - # Test with white spaces - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": " " * 14}, - ) - self.assertContains(response, text="Cet UID est invalide") - - # Test with unauthorized user - self.client.force_login(self.subscriber) - response = self.client.post( - reverse( - "counter:add_student_card", - kwargs={"customer_id": self.customer.customer.pk}, - ), - {"uid": "8B90734A802A8F"}, - ) - assert response.status_code == 403 + customer = subscriber_user.make() + self.client.force_login(customer) + for uid, error_msg in self.invalid_uids(): + url = reverse( + "counter:add_student_card", kwargs={"customer_id": customer.customer.pk} + ) + response = self.client.post(url, {"uid": uid}) + self.assertContains(response, text="Cet UID est invalide") + self.assertContains(response, text=error_msg) + customer.refresh_from_db() + assert not hasattr(customer.customer, "student_card") class TestCustomerAccountId(TestCase): diff --git a/counter/urls.py b/counter/urls.py index e196894f..fa659ba0 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -81,7 +81,7 @@ urlpatterns = [ name="add_student_card", ), path( - "customer//card/delete//", + "customer//card/delete/", StudentCardDeleteView.as_view(), name="delete_student_card", ), diff --git a/counter/views/click.py b/counter/views/click.py index 2fa9684d..65e889ff 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -415,7 +415,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): 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"] = StudentCardFormView.get_template_data( + kwargs["student_card_fragment"] = StudentCardFormView.get_template_data( self.customer ).render(self.request) return kwargs diff --git a/counter/views/student_card.py b/counter/views/student_card.py index 99f67316..070e260d 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -15,9 +15,10 @@ from django.core.exceptions import PermissionDenied -from django.http import HttpRequest +from django.http import Http404, HttpRequest, HttpResponse from django.shortcuts import get_object_or_404 -from django.urls import reverse_lazy +from django.urls import reverse +from django.utils.translation import gettext as _ from django.views.generic.edit import DeleteView, FormView from core.utils import FormFragmentTemplateData @@ -32,16 +33,21 @@ class StudentCardDeleteView(DeleteView, CanEditMixin): 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_object(self, queryset=None): + if not hasattr(self.customer, "student_card"): + raise Http404( + _("%(name)s has no registered student card") + % {"name": self.customer.user.get_full_name()} + ) + return self.customer.student_card + def get_success_url(self, **kwargs): - return reverse_lazy( - "core:user_prefs", kwargs={"user_id": self.customer.user.pk} - ) + return reverse("core:user_prefs", kwargs={"user_id": self.customer.user_id}) class StudentCardFormView(FormView): @@ -53,23 +59,22 @@ class StudentCardFormView(FormView): @classmethod def get_template_data( cls, customer: Customer - ) -> FormFragmentTemplateData[form_class]: + ) -> FormFragmentTemplateData[StudentCardForm]: """Get necessary data to pre-render the fragment""" - return FormFragmentTemplateData[cls.form_class]( + return FormFragmentTemplateData( form=cls.form_class(), template=cls.template_name, context={ - "action": reverse_lazy( + "action": reverse( "counter:add_student_card", kwargs={"customer_id": customer.pk} ), "customer": customer, - "student_cards": customer.student_cards.all(), }, ) def dispatch(self, request: HttpRequest, *args, **kwargs): self.customer = get_object_or_404( - Customer.objects.prefetch_related("student_cards"), pk=kwargs["customer_id"] + Customer.objects.select_related("student_card"), pk=kwargs["customer_id"] ) if not is_logged_in_counter(request) and not StudentCard.can_create( @@ -79,11 +84,12 @@ class StudentCardFormView(FormView): return super().dispatch(request, *args, **kwargs) - def form_valid(self, form): + def form_valid(self, form: StudentCardForm) -> HttpResponse: data = form.clean() - res = super(FormView, self).form_valid(form) - StudentCard(customer=self.customer, uid=data["uid"]).save() - return res + StudentCard.objects.update_or_create( + customer=self.customer, defaults={"uid": data["uid"]} + ) + return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index e122343d..1a2786bc 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-09 12:28+0100\n" +"POT-Creation-Date: 2024-12-11 09:34+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -369,7 +369,7 @@ msgstr "Compte en banque : " #: core/templates/core/user_clubs.jinja:34 #: core/templates/core/user_clubs.jinja:63 #: core/templates/core/user_edit.jinja:62 -#: counter/templates/counter/fragments/create_student_card.jinja:21 +#: counter/templates/counter/fragments/create_student_card.jinja:18 #: counter/templates/counter/last_ops.jinja:35 #: counter/templates/counter/last_ops.jinja:65 #: election/templates/election/election_detail.jinja:191 @@ -950,11 +950,11 @@ msgstr "Une action est requise" msgid "You must specify at least an user or an email address" msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" -#: club/forms.py:149 counter/forms.py:193 +#: club/forms.py:149 counter/forms.py:189 msgid "Begin date" msgstr "Date de début" -#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:196 +#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:192 #: election/views.py:170 subscription/forms.py:21 msgid "End date" msgstr "Date de fin" @@ -3048,7 +3048,7 @@ msgstr "Facture eboutic" msgid "Etickets" msgstr "Etickets" -#: core/templates/core/user_account.jinja:69 core/views/user.py:639 +#: core/templates/core/user_account.jinja:69 core/views/user.py:633 msgid "User has no account" msgstr "L'utilisateur n'a pas de compte" @@ -3371,7 +3371,7 @@ msgstr "Cotisations" msgid "Subscription stats" msgstr "Statistiques de cotisation" -#: core/templates/core/user_tools.jinja:48 counter/forms.py:166 +#: core/templates/core/user_tools.jinja:48 counter/forms.py:162 #: counter/views/mixins.py:89 msgid "Counters" msgstr "Comptoirs" @@ -3543,7 +3543,7 @@ msgstr "Parrain / Marraine" msgid "Godchild" msgstr "Fillot / Fillote" -#: core/views/forms.py:310 counter/forms.py:80 trombi/views.py:151 +#: core/views/forms.py:310 counter/forms.py:78 trombi/views.py:151 msgid "Select user" msgstr "Choisir un utilisateur" @@ -3596,11 +3596,11 @@ msgstr "Galaxie" msgid "counter" msgstr "comptoir" -#: counter/forms.py:61 +#: counter/forms.py:59 msgid "This UID is invalid" msgstr "Cet UID est invalide" -#: counter/forms.py:109 +#: counter/forms.py:107 msgid "User not found" msgstr "Utilisateur non trouvé" @@ -3862,9 +3862,13 @@ msgstr "secret" msgid "uid" msgstr "uid" -#: counter/models.py:1144 +#: counter/models.py:1144 counter/models.py:1149 +msgid "student card" +msgstr "carte étudiante" + +#: counter/models.py:1150 msgid "student cards" -msgstr "cartes étudiante" +msgstr "cartes étudiantes" #: counter/templates/counter/activity.jinja:5 #: counter/templates/counter/activity.jinja:13 @@ -3929,7 +3933,7 @@ msgstr "Vente" #: counter/templates/counter/counter_click.jinja:50 #: counter/templates/counter/counter_click.jinja:115 -#: counter/templates/counter/fragments/create_student_card.jinja:10 +#: counter/templates/counter/fragments/create_student_card.jinja:11 #: counter/templates/counter/invoices_call.jinja:16 #: launderette/templates/launderette/launderette_admin.jinja:35 #: launderette/templates/launderette/launderette_click.jinja:13 @@ -4032,17 +4036,17 @@ msgid "There is no eticket in this website." msgstr "Il n'y a pas de eticket sur ce site web." #: counter/templates/counter/fragments/create_student_card.jinja:2 -msgid "Add a student card" -msgstr "Ajouter une carte étudiante" +msgid "Student card" +msgstr "Carte étudiante" #: counter/templates/counter/fragments/create_student_card.jinja:13 -msgid "Registered cards" -msgstr "Cartes enregistrées" - -#: counter/templates/counter/fragments/create_student_card.jinja:27 msgid "No student card registered." msgstr "Aucune carte étudiante enregistrée." +#: counter/templates/counter/fragments/create_student_card.jinja:16 +msgid "Registered" +msgstr "Enregistré" + #: counter/templates/counter/invoices_call.jinja:8 #, python-format msgid "Invoices call for %(date)s" @@ -4313,6 +4317,11 @@ msgstr "Administration des comptoirs" msgid "Product types" msgstr "Types de produit" +#: counter/views/student_card.py:44 +#, python-format +msgid "%(name)s has no registered student card" +msgstr "%(name)s n'a pas de carte étudiante enregistrée" + #: eboutic/forms.py:88 msgid "The request was badly formatted." msgstr "La requête a été mal formatée." From 4975475e855ca8597aeb10bd34eb7970d4466516 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 10 Dec 2024 23:48:46 +0100 Subject: [PATCH 40/53] Add tooltip on current registered card, allow barmen to delete cards and make card deletion a fragment --- core/static/core/style.scss | 27 +++++++++++ .../fragments/create_student_card.jinja | 11 +++-- .../fragments/delete_student_card.jinja | 15 ++++++ counter/tests/test_customer.py | 46 +++++++++++-------- counter/views/student_card.py | 24 +++++++--- locale/fr/LC_MESSAGES/django.po | 16 +++++-- 6 files changed, 107 insertions(+), 32 deletions(-) create mode 100644 counter/templates/counter/fragments/delete_student_card.jinja diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 50892df3..6973aa75 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -42,6 +42,29 @@ body { } } +[tooltip] { + position: relative; +} + +[tooltip]:before { + opacity: 0; + z-index: 1; + content: attr(tooltip); + background: $white-color; + color: $black-color; + border: 1px solid $black-color; + border-radius: 5px; + padding: 5px; + top: 1em; + position: absolute; + white-space: nowrap; + transition: opacity 500ms ease-out; +} + +[tooltip]:hover:before { + opacity: 1; +} + .ib { display: inline-block; padding: 1px; @@ -308,6 +331,7 @@ body { font-size: 120%; background-color: unset; position: relative; + &:after { content: ''; position: absolute; @@ -318,14 +342,17 @@ body { border-radius: 2px; transition: all 0.2s ease-in-out; } + &:hover:after { border-bottom-color: darken($primary-neutral-light-color, 20%); } + &.active:after { border-bottom-color: $primary-dark-color; } } } + section { padding: 20px; } diff --git a/counter/templates/counter/fragments/create_student_card.jinja b/counter/templates/counter/fragments/create_student_card.jinja index f8e59d24..d972ff33 100644 --- a/counter/templates/counter/fragments/create_student_card.jinja +++ b/counter/templates/counter/fragments/create_student_card.jinja @@ -13,10 +13,13 @@ {% trans %}No student card registered.{% endtrans %} {% else %}

    - {% trans %}Registered{% endtrans %}   -   - - {% trans %}Delete{% endtrans %} - + {% trans %}Card registered{% endtrans %} +   -   +

    {% endif %} diff --git a/counter/templates/counter/fragments/delete_student_card.jinja b/counter/templates/counter/fragments/delete_student_card.jinja new file mode 100644 index 00000000..94be1c47 --- /dev/null +++ b/counter/templates/counter/fragments/delete_student_card.jinja @@ -0,0 +1,15 @@ +
    +
    + {% csrf_token %} +

    {% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}

    + + +
    +
    \ No newline at end of file diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py index b861a97e..3e745ecc 100644 --- a/counter/tests/test_customer.py +++ b/counter/tests/test_customer.py @@ -198,8 +198,7 @@ class TestStudentCard(TestCase): StudentCard, customer=cls.customer.customer, uid="8A89B82018B0A0" ) - def setUp(self): - # Auto login on counter + def login_in_counter(self): self.client.post( reverse("counter:login", args=[self.counter.id]), {"username": self.barmen.username, "password": "plop"}, @@ -222,6 +221,7 @@ class TestStudentCard(TestCase): ] def test_search_user_with_student_card(self): + self.login_in_counter() response = self.client.post( reverse("counter:details", args=[self.counter.id]), {"code": self.valid_card.uid}, @@ -233,6 +233,7 @@ class TestStudentCard(TestCase): ) def test_add_student_card_from_counter(self): + self.login_in_counter() for uid in ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"]: customer = subscriber_user.make().customer response = self.client.post( @@ -251,6 +252,7 @@ class TestStudentCard(TestCase): assert customer.student_card.uid == uid def test_add_student_card_from_counter_fail(self): + self.login_in_counter() customer = subscriber_user.make().customer for uid, error_msg in self.invalid_uids(): response = self.client.post( @@ -269,25 +271,15 @@ class TestStudentCard(TestCase): assert not hasattr(customer, "student_card") def test_add_student_card_from_counter_unauthorized(self): - barman = subscriber_user.make() - self.counter.sellers.add(barman) - customer = self.customer.customer - # There is someone logged to a counter - # with the client of this TestCase instance, - # so we create a new client, in order to check - # that using a client not logged to a counter - # where another client is logged still isn't authorized. - client = Client() - def send_valid_request(counter_id): - return client.post( + return self.client.post( reverse( - "counter:add_student_card", kwargs={"customer_id": customer.pk} + "counter:add_student_card", kwargs={"customer_id": self.customer.pk} ), {"uid": "8B90734A802A8F"}, HTTP_REFERER=reverse( "counter:click", - kwargs={"counter_id": counter_id, "user_id": customer.pk}, + kwargs={"counter_id": counter_id, "user_id": self.customer.pk}, ), ) @@ -295,7 +287,7 @@ class TestStudentCard(TestCase): assert send_valid_request(self.counter.id).status_code == 403 # Send to a non bar counter - client.force_login(self.club_admin) + self.client.force_login(self.club_admin) assert send_valid_request(self.club_counter.id).status_code == 403 def test_delete_student_card_with_owner(self): @@ -322,6 +314,24 @@ class TestStudentCard(TestCase): self.customer.customer.refresh_from_db() assert not hasattr(self.customer.customer, "student_card") + def test_delete_student_card_from_counter(self): + self.login_in_counter() + self.client.post( + reverse( + "counter:delete_student_card", + kwargs={"customer_id": self.customer.customer.pk}, + ), + http_referer=reverse( + "counter:click", + kwargs={ + "counter_id": self.counter.id, + "user_id": self.customer.customer.pk, + }, + ), + ) + self.customer.customer.refresh_from_db() + assert not hasattr(self.customer.customer, "student_card") + def test_delete_student_card_fail(self): """Test that non-admin users cannot delete student cards""" self.client.force_login(self.subscriber) @@ -336,7 +346,7 @@ class TestStudentCard(TestCase): assert not hasattr(self.subscriber.customer, "student_card") def test_add_student_card_from_user_preferences(self): - users = [self.subscriber, self.board_admin, self.root] + users = [self.customer, self.board_admin, self.root] uids = ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"] for user, uid in itertools.product(users, uids): self.customer.customer.student_card.delete() @@ -353,7 +363,7 @@ class TestStudentCard(TestCase): self.customer.customer.refresh_from_db() assert self.customer.customer.student_card.uid == uid - self.assertContains(response, text="Enregistré") + self.assertContains(response, text="Carte enregistrée") def test_add_student_card_from_user_preferences_fail(self): customer = subscriber_user.make() diff --git a/counter/views/student_card.py b/counter/views/student_card.py index 070e260d..35226e95 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -22,22 +22,32 @@ from django.utils.translation import gettext as _ from django.views.generic.edit import DeleteView, FormView from core.utils import FormFragmentTemplateData -from core.views import CanEditMixin +from core.views import can_edit from counter.forms import StudentCardForm from counter.models import Customer, StudentCard from counter.utils import is_logged_in_counter -class StudentCardDeleteView(DeleteView, CanEditMixin): - """View used to delete a card from a user.""" +class StudentCardDeleteView(DeleteView): + """View used to delete a card from a user. This is a fragment view !""" model = StudentCard - template_name = "core/delete_confirm.jinja" + template_name = "counter/fragments/delete_student_card.jinja" - def dispatch(self, request, *args, **kwargs): + def dispatch(self, request: HttpRequest, *args, **kwargs): self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) + if not is_logged_in_counter(request) and not can_edit( + self.get_object(), request.user + ): + raise PermissionDenied() return super().dispatch(request, *args, **kwargs) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["action"] = self.request.path + context["action_cancel"] = self.get_success_url() + return context + def get_object(self, queryset=None): if not hasattr(self.customer, "student_card"): raise Http404( @@ -47,7 +57,9 @@ class StudentCardDeleteView(DeleteView, CanEditMixin): return self.customer.student_card def get_success_url(self, **kwargs): - return reverse("core:user_prefs", kwargs={"user_id": self.customer.user_id}) + return reverse( + "counter:add_student_card", kwargs={"customer_id": self.customer.pk} + ) class StudentCardFormView(FormView): diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 1a2786bc..482befd3 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -369,7 +369,7 @@ msgstr "Compte en banque : " #: core/templates/core/user_clubs.jinja:34 #: core/templates/core/user_clubs.jinja:63 #: core/templates/core/user_edit.jinja:62 -#: counter/templates/counter/fragments/create_student_card.jinja:18 +#: counter/templates/counter/fragments/create_student_card.jinja:22 #: counter/templates/counter/last_ops.jinja:35 #: counter/templates/counter/last_ops.jinja:65 #: election/templates/election/election_detail.jinja:191 @@ -2574,18 +2574,21 @@ msgstr "Confirmation de suppression" #: core/templates/core/delete_confirm.jinja:16 #: core/templates/core/file_delete_confirm.jinja:29 +#: counter/templates/counter/fragments/delete_student_card.jinja:4 #, python-format msgid "Are you sure you want to delete \"%(obj)s\"?" msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?" #: core/templates/core/delete_confirm.jinja:17 #: core/templates/core/file_delete_confirm.jinja:36 +#: counter/templates/counter/fragments/delete_student_card.jinja:5 msgid "Confirm" msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/file_delete_confirm.jinja:46 #: counter/templates/counter/counter_click.jinja:104 +#: counter/templates/counter/fragments/delete_student_card.jinja:12 #: sas/templates/sas/ask_picture_removal.jinja:20 msgid "Cancel" msgstr "Annuler" @@ -4044,8 +4047,13 @@ msgid "No student card registered." msgstr "Aucune carte étudiante enregistrée." #: counter/templates/counter/fragments/create_student_card.jinja:16 -msgid "Registered" -msgstr "Enregistré" +msgid "Card registered" +msgstr "Carte enregistrée" + +#: counter/templates/counter/fragments/create_student_card.jinja:17 +#, python-format +msgid "uid: %(uid)s " +msgstr "uid: %(uid)s" #: counter/templates/counter/invoices_call.jinja:8 #, python-format @@ -4317,7 +4325,7 @@ msgstr "Administration des comptoirs" msgid "Product types" msgstr "Types de produit" -#: counter/views/student_card.py:44 +#: counter/views/student_card.py:54 #, python-format msgid "%(name)s has no registered student card" msgstr "%(name)s n'a pas de carte étudiante enregistrée" From 2cc4308a582d2112fe4469bb7d77b64d1b81499a Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 13 Dec 2024 11:43:34 +0100 Subject: [PATCH 41/53] Fix tooltip shadow and position and improve unittests --- core/static/core/colors.scss | 7 +++++- core/static/core/style.scss | 14 ++++++----- .../fragments/create_student_card.jinja | 19 +++++++++------ counter/tests/test_customer.py | 23 +++++++++++++++---- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/core/static/core/colors.scss b/core/static/core/colors.scss index 5453dd34..35dc6a69 100644 --- a/core/static/core/colors.scss +++ b/core/static/core/colors.scss @@ -29,4 +29,9 @@ $shadow-color: rgb(223, 223, 223); $background-button-color: hsl(0, 0%, 95%); -$deepblue: #354a5f; \ No newline at end of file +$deepblue: #354a5f; + +@mixin shadow { + box-shadow: rgba(60, 64, 67, 0.3) 0 1px 3px 0, + rgba(60, 64, 67, 0.15) 0 4px 8px 3px; +} \ No newline at end of file diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 6973aa75..cbe8d326 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -46,22 +46,25 @@ body { position: relative; } -[tooltip]:before { +[tooltip]::before { + @include shadow; opacity: 0; z-index: 1; content: attr(tooltip); - background: $white-color; + background: hsl(219.6, 20.8%, 96%); color: $black-color; - border: 1px solid $black-color; + border: 0.5px solid hsl(0, 0%, 50%); + ; border-radius: 5px; padding: 5px; top: 1em; position: absolute; + margin-top: 5px; white-space: nowrap; transition: opacity 500ms ease-out; } -[tooltip]:hover:before { +[tooltip]:hover::before { opacity: 1; } @@ -102,8 +105,7 @@ body { } .shadow { - box-shadow: rgba(60, 64, 67, 0.3) 0 1px 3px 0, - rgba(60, 64, 67, 0.15) 0 4px 8px 3px; + @include shadow; } .w_big { diff --git a/counter/templates/counter/fragments/create_student_card.jinja b/counter/templates/counter/fragments/create_student_card.jinja index d972ff33..c0701b7a 100644 --- a/counter/templates/counter/fragments/create_student_card.jinja +++ b/counter/templates/counter/fragments/create_student_card.jinja @@ -13,13 +13,18 @@ {% trans %}No student card registered.{% endtrans %} {% else %}

    - {% trans %}Card registered{% endtrans %} -   -   - + + {% trans %}Card registered{% endtrans %} + + +   -   +

    {% endif %} diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py index 3e745ecc..21f5604c 100644 --- a/counter/tests/test_customer.py +++ b/counter/tests/test_customer.py @@ -271,8 +271,8 @@ class TestStudentCard(TestCase): assert not hasattr(customer, "student_card") def test_add_student_card_from_counter_unauthorized(self): - def send_valid_request(counter_id): - return self.client.post( + def send_valid_request(client, counter_id): + return client.post( reverse( "counter:add_student_card", kwargs={"customer_id": self.customer.pk} ), @@ -284,11 +284,24 @@ class TestStudentCard(TestCase): ) # Send to a counter where you aren't logged in - assert send_valid_request(self.counter.id).status_code == 403 + assert send_valid_request(self.client, self.counter.id).status_code == 403 + + self.login_in_counter() + barman = subscriber_user.make() + self.counter.sellers.add(barman) + # We want to test sending requests from another counter while + # we are currently registered to another counter + # so we connect to a counter and + # we create a new client, in order to check + # that using a client not logged to a counter + # where another client is logged still isn't authorized. + client = Client() + # Send to a counter where you aren't logged in + assert send_valid_request(client, self.counter.id).status_code == 403 # Send to a non bar counter - self.client.force_login(self.club_admin) - assert send_valid_request(self.club_counter.id).status_code == 403 + client.force_login(self.club_admin) + assert send_valid_request(client, self.club_counter.id).status_code == 403 def test_delete_student_card_with_owner(self): self.client.force_login(self.customer) From 0631c77a1ca437fdd0dbb9c6f25d8453b5c80083 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 15 Dec 2024 17:02:44 +0100 Subject: [PATCH 42/53] Apply review comments --- core/templates/core/user_preferences.jinja | 1 + counter/templates/counter/counter_click.jinja | 1 + counter/templates/counter/fragments/create_student_card.jinja | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index bf5189ae..13bee656 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -36,6 +36,7 @@ {% if student_card_fragment %} +

    {% trans %}Student card{% endtrans %}

    {{ student_card_fragment }}

    {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 4b9e1898..3df77555 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -31,6 +31,7 @@

    {% trans %}Amount: {% endtrans %}{{ customer.amount }} €

    {% if counter.type == 'BAR' %} +
    {% trans %}Student card{% endtrans %}
    {{ student_card_fragment }} {% endif %} diff --git a/counter/templates/counter/fragments/create_student_card.jinja b/counter/templates/counter/fragments/create_student_card.jinja index c0701b7a..f15716e4 100644 --- a/counter/templates/counter/fragments/create_student_card.jinja +++ b/counter/templates/counter/fragments/create_student_card.jinja @@ -1,5 +1,4 @@
    -

    {% trans %}Student card{% endtrans %}

    {% if not customer.student_card %}
    Date: Sun, 15 Dec 2024 17:31:41 +0100 Subject: [PATCH 43/53] Enable sentry workflow again --- .github/workflows/deploy.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 141f8e53..db32fa3a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -45,3 +45,21 @@ jobs: poetry run ./manage.py compilemessages sudo systemctl restart uwsgi + + sentry: + runs-on: ubuntu-latest + environment: production + timeout-minutes: 30 + needs: deployment + steps: + - uses: actions/checkout@v4 + + - name: Sentry Release + uses: getsentry/action-release@v1.7.0 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + SENTRY_URL: ${{ secrets.SENTRY_URL }} + with: + environment: production \ No newline at end of file From e9361697f74f05fc1659c88a9151aa9e03518c25 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 15 Dec 2024 21:33:19 +0100 Subject: [PATCH 44/53] Convert customer refill to a fragment view --- counter/api.py | 12 ++- counter/schemas.py | 8 +- .../counter/counter-click-index.ts} | 0 counter/templates/counter/counter_click.jinja | 30 +++--- .../counter/fragments/create_refill.jinja | 9 ++ counter/tests/test_counter.py | 31 +++++-- counter/urls.py | 7 +- counter/views/click.py | 93 +++++++++++++++---- counter/views/student_card.py | 10 +- 9 files changed, 148 insertions(+), 52 deletions(-) rename counter/static/{counter/js/counter_click.js => bundled/counter/counter-click-index.ts} (100%) create mode 100644 counter/templates/counter/fragments/create_refill.jinja diff --git a/counter/api.py b/counter/api.py index f3f0f101..d58f0154 100644 --- a/counter/api.py +++ b/counter/api.py @@ -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( diff --git a/counter/schemas.py b/counter/schemas.py index ec1a842d..4fbbc712 100644 --- a/counter/schemas.py +++ b/counter/schemas.py @@ -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") diff --git a/counter/static/counter/js/counter_click.js b/counter/static/bundled/counter/counter-click-index.ts similarity index 100% rename from counter/static/counter/js/counter_click.js rename to counter/static/bundled/counter/counter-click-index.ts diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 3df77555..65c7d9ca 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -6,7 +6,7 @@ {% endblock %} {% block additional_js %} - + {% endblock %} {% block info_boxes %} @@ -28,7 +28,7 @@
    {% trans %}Customer{% endtrans %}
    {{ user_mini_profile(customer.user) }} {{ user_subscription(customer.user) }} -

    {% trans %}Amount: {% endtrans %}{{ customer.amount }} €

    +

    {% trans %}Amount: {% endtrans %}

    {% if counter.type == 'BAR' %}
    {% trans %}Student card{% endtrans %}
    @@ -105,16 +105,12 @@
    - {% if (counter.type == 'BAR' and barmens_can_refill) %} + {% if refilling_fragment %}
    {% trans %}Refilling{% endtrans %}
    -
    -
    - {% csrf_token %} - {{ refill_form.as_p() }} - - -
    +
    + {{ refilling_fragment }}
    {% endif %}
    @@ -155,9 +151,6 @@ {% block script %} {{ super() }} {% endblock script %} \ No newline at end of file diff --git a/counter/templates/counter/fragments/create_refill.jinja b/counter/templates/counter/fragments/create_refill.jinja new file mode 100644 index 00000000..7de612a3 --- /dev/null +++ b/counter/templates/counter/fragments/create_refill.jinja @@ -0,0 +1,9 @@ +
    + {% csrf_token %} + {{ form.as_p() }} + +
    diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index 855b43f6..2574865f 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -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/", CounterMain.as_view(), name="details"), path("/click//", CounterClick.as_view(), name="click"), + path( + "refill//", + RefillingCreateView.as_view(), + name="refilling_create", + ), path( "/last_ops/", CounterLastOperationsView.as_view(), diff --git a/counter/views/click.py b/counter/views/click.py index 65e889ff..1d87ac8b 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -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 diff --git a/counter/views/student_card.py b/counter/views/student_card.py index 35226e95..952f65eb 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -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 From cde864fdc7d7dcb9fe480a60e525aea0ff86c448 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 15 Dec 2024 21:33:43 +0100 Subject: [PATCH 45/53] Apply review comments --- counter/api.py | 11 +- counter/models.py | 9 ++ counter/schemas.py | 4 +- .../bundled/counter/counter-click-index.ts | 149 ++++++++++++------ counter/tests/test_api.py | 105 ++++++++++++ counter/views/click.py | 18 +-- 6 files changed, 226 insertions(+), 70 deletions(-) create mode 100644 counter/tests/test_api.py diff --git a/counter/api.py b/counter/api.py index d58f0154..e51aea26 100644 --- a/counter/api.py +++ b/counter/api.py @@ -26,7 +26,7 @@ from counter.models import Counter, Customer, Product from counter.schemas import ( CounterFilterSchema, CounterSchema, - CustomerBalance, + CustomerSchema, ProductSchema, SimplifiedCounterSchema, ) @@ -63,8 +63,13 @@ class CounterController(ControllerBase): @api_controller("/customer") class CustomerController(ControllerBase): - @route.get("/balance", response=CustomerBalance, permissions=[IsLoggedInCounter]) - def get_balance(self, customer_id: int): + @route.get( + "{customer_id}", + response=CustomerSchema, + permissions=[IsLoggedInCounter], + url_name="get_customer", + ) + def get_customer(self, customer_id: int): return self.get_object_or_exception(Customer, pk=customer_id) diff --git a/counter/models.py b/counter/models.py index 292cd59f..da2f33f4 100644 --- a/counter/models.py +++ b/counter/models.py @@ -650,6 +650,15 @@ class Counter(models.Model): ) )["total"] + def customer_is_barman(self, customer: Customer | User) -> bool: + """Check if current counter is a `bar` and that the customer is on the barmen_list + + This is useful to compute special prices""" + if isinstance(customer, Customer): + customer: User = customer.user + + return self.type == "BAR" and customer in self.barmen_list + class RefillingQuerySet(models.QuerySet): def annotate_total(self) -> Self: diff --git a/counter/schemas.py b/counter/schemas.py index 4fbbc712..7fbe1a71 100644 --- a/counter/schemas.py +++ b/counter/schemas.py @@ -16,10 +16,10 @@ class CounterSchema(ModelSchema): fields = ["id", "name", "type", "club", "products"] -class CustomerBalance(ModelSchema): +class CustomerSchema(ModelSchema): class Meta: model = Customer - fields = ["amount"] + fields = ["user", "account_id", "amount", "recorded_products"] class CounterFilterSchema(FilterSchema): diff --git a/counter/static/bundled/counter/counter-click-index.ts b/counter/static/bundled/counter/counter-click-index.ts index b0ddb42c..01d7e8f9 100644 --- a/counter/static/bundled/counter/counter-click-index.ts +++ b/counter/static/bundled/counter/counter-click-index.ts @@ -1,74 +1,117 @@ -document.addEventListener("alpine:init", () => { - Alpine.data("counter", () => ({ - // biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja - basket: sessionBasket, - errors: [], +import { exportToHtml } from "#core:utils/globals"; +import { customerGetCustomer } from "#openapi"; - sumBasket() { - if (!this.basket || Object.keys(this.basket).length === 0) { - return 0; - } - const total = Object.values(this.basket).reduce( - (acc, cur) => acc + cur.qty * cur.price, - 0, - ); - return total / 100; - }, +interface CounterConfig { + csrfToken: string; + clickApiUrl: string; + sessionBasket: Record; + customerBalance: number; + customerId: number; +} +interface BasketItem { + // biome-ignore lint/style/useNamingConvention: talking with python + bonus_qty: number; + price: number; + qty: number; +} - async handleCode(event) { - const code = $(event.target).find("#code_field").val().toUpperCase(); - if (["FIN", "ANN"].includes(code)) { - $(event.target).submit(); - } else { - await this.handleAction(event); - } - }, +exportToHtml("loadCounter", (config: CounterConfig) => { + document.addEventListener("alpine:init", () => { + Alpine.data("counter", () => ({ + basket: config.sessionBasket, + errors: [], + customerBalance: config.customerBalance, - async handleAction(event) { - const payload = $(event.target).serialize(); - // biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja - const request = new Request(clickApiUrl, { - method: "POST", - body: payload, - headers: { - // biome-ignore lint/style/useNamingConvention: this goes into http headers - Accept: "application/json", - // biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja - "X-CSRFToken": csrfToken, - }, - }); - const response = await fetch(request); - const json = await response.json(); - this.basket = json.basket; - this.errors = json.errors; - $("form.code_form #code_field").val("").focus(); - }, - })); + sumBasket() { + if (!this.basket || Object.keys(this.basket).length === 0) { + return 0; + } + const total = Object.values(this.basket).reduce( + (acc: number, cur: BasketItem) => acc + cur.qty * cur.price, + 0, + ) as number; + return total / 100; + }, + + async updateBalance() { + this.customerBalance = ( + await customerGetCustomer({ + path: { + // biome-ignore lint/style/useNamingConvention: api is in snake_case + customer_id: config.customerId, + }, + }) + ).data.amount; + }, + + async handleCode(event: SubmitEvent) { + const code = ( + $(event.target).find("#code_field").val() as string + ).toUpperCase(); + if (["FIN", "ANN"].includes(code)) { + $(event.target).submit(); + } else { + await this.handleAction(event); + } + }, + + async handleAction(event: SubmitEvent) { + const payload = $(event.target).serialize(); + const request = new Request(config.clickApiUrl, { + method: "POST", + body: payload, + headers: { + // biome-ignore lint/style/useNamingConvention: this goes into http headers + Accept: "application/json", + "X-CSRFToken": config.csrfToken, + }, + }); + const response = await fetch(request); + const json = await response.json(); + this.basket = json.basket; + this.errors = json.errors; + $("form.code_form #code_field").val("").focus(); + }, + })); + }); }); +interface Product { + value: string; + label: string; + tags: string; +} +declare global { + const productsAutocomplete: Product[]; +} + $(() => { /* Autocompletion in the code field */ - const codeField = $("#code_field"); + // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery + const codeField: any = $("#code_field"); let quantity = ""; codeField.autocomplete({ - select: (event, ui) => { + // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery + select: (event: any, ui: any) => { event.preventDefault(); codeField.val(quantity + ui.item.value); }, - focus: (event, ui) => { + // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery + focus: (event: any, ui: any) => { event.preventDefault(); codeField.val(quantity + ui.item.value); }, - source: (request, response) => { + // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery + source: (request: any, response: any) => { // biome-ignore lint/performance/useTopLevelRegex: performance impact is minimal const res = /^(\d+x)?(.*)/i.exec(request.term); quantity = res[1] || ""; const search = res[2]; - const matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i"); + // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery + const matcher = new RegExp(($ as any).ui.autocomplete.escapeRegex(search), "i"); response( - // biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja - $.grep(productsAutocomplete, (value) => { + $.grep(productsAutocomplete, (value: Product) => { return matcher.test(value.tags); }), ); @@ -76,11 +119,13 @@ $(() => { }); /* Accordion UI between basket and refills */ - $("#click_form").accordion({ + // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery + ($("#click_form") as any).accordion({ heightStyle: "content", activate: () => $(".focus").focus(), }); - $("#products").tabs(); + // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery + ($("#products") as any).tabs(); codeField.focus(); }); diff --git a/counter/tests/test_api.py b/counter/tests/test_api.py new file mode 100644 index 00000000..8a5efdc1 --- /dev/null +++ b/counter/tests/test_api.py @@ -0,0 +1,105 @@ +import pytest +from django.contrib.auth.models import make_password +from django.test.client import Client +from django.urls import reverse +from model_bakery import baker + +from core.baker_recipes import board_user, subscriber_user +from core.models import User +from counter.models import Counter + + +@pytest.fixture +def customer_user() -> User: + return subscriber_user.make() + + +@pytest.fixture +def counter_bar() -> Counter: + return baker.make(Counter, type="BAR") + + +@pytest.fixture +def barmen(counter_bar: Counter) -> User: + user = subscriber_user.make(password=make_password("plop")) + counter_bar.sellers.add(user) + return user + + +@pytest.fixture +def board_member() -> User: + return board_user.make() + + +@pytest.fixture +def root_user() -> User: + return baker.make(User, is_superuser=True) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("connected_user"), + [ + None, # Anonymous user + "barmen", + "customer_user", + "board_member", + "root_user", + ], +) +def test_get_customer_fail( + client: Client, + customer_user: User, + request: pytest.FixtureRequest, + connected_user: str | None, +): + if connected_user is not None: + client.force_login(request.getfixturevalue(connected_user)) + assert ( + client.get( + reverse("api:get_customer", kwargs={"customer_id": customer_user.id}) + ).status_code + == 403 + ) + + +@pytest.mark.django_db +def test_get_customer_from_bar_fail_wrong_referrer( + client: Client, customer_user: User, barmen: User, counter_bar: Counter +): + client.post( + reverse("counter:login", args=[counter_bar.pk]), + {"username": barmen.username, "password": "plop"}, + ) + + assert ( + client.get( + reverse("api:get_customer", kwargs={"customer_id": customer_user.id}) + ).status_code + == 403 + ) + + +@pytest.mark.django_db +def test_get_customer_from_bar_success( + client: Client, customer_user: User, barmen: User, counter_bar: Counter +): + client.post( + reverse("counter:login", args=[counter_bar.pk]), + {"username": barmen.username, "password": "plop"}, + ) + + response = client.get( + reverse("api:get_customer", kwargs={"customer_id": customer_user.id}), + HTTP_REFERER=reverse( + "counter:click", + kwargs={"counter_id": counter_bar.id, "user_id": customer_user.id}, + ), + ) + assert response.status_code == 200 + assert response.json() == { + "user": customer_user.id, + "account_id": customer_user.customer.account_id, + "amount": f"{customer_user.customer.amount:.2f}", + "recorded_products": customer_user.customer.recorded_products, + } diff --git a/counter/views/click.py b/counter/views/click.py index 1d87ac8b..6a379564 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -137,7 +137,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): request.session["no_age"] = False if self.object.type != "BAR": self.operator = request.user - elif self.customer_is_barman(): + elif self.object.customer_is_barman(self.customer): self.operator = self.customer.user else: self.operator = self.object.get_random_barman() @@ -157,16 +157,12 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): context = self.get_context_data(object=self.object) return self.render_to_response(context) - def customer_is_barman(self) -> bool: - barmen = self.object.barmen_list - return self.object.type == "BAR" and self.customer.user in barmen - def get_product(self, pid): return Product.objects.filter(pk=int(pid)).first() def get_price(self, pid): p = self.get_product(pid) - if self.customer_is_barman(): + if self.object.customer_is_barman(self.customer): price = p.special_selling_price else: price = p.selling_price @@ -331,7 +327,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): for pid, infos in request.session["basket"].items(): # This duplicates code for DB optimization (prevent to load many times the same object) p = Product.objects.filter(pk=pid).first() - if self.customer_is_barman(): + if self.object.customer_is_barman(self.customer): uprice = p.special_selling_price else: uprice = p.selling_price @@ -385,7 +381,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): """Add customer to the context.""" kwargs = super().get_context_data(**kwargs) products = self.object.products.select_related("product_type") - if self.customer_is_barman(): + if self.object.customer_is_barman(self.customer): products = products.annotate(price=F("special_selling_price")) else: products = products.annotate(price=F("selling_price")) @@ -444,17 +440,13 @@ class RefillingCreateView(FormView): if not self.counter.can_refill(): raise PermissionDenied - if self.customer_is_barman(): + if self.counter.customer_is_barman(self.customer): 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() From f63fb59cbfd7ed927efb778700fc24f03d7aa206 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 16 Dec 2024 00:15:21 +0100 Subject: [PATCH 46/53] Allow filtering of refilling options * Move settings.SITH_COUNTER_PAYMENT_METHOD to counter.apps.PAYMENT_METHOD * Move student cards to an accordion on counter click * Make cash default refilling option * Disable bank selection option in refilling if CHECK are not allowed * Disable refilling with CHECK from the frontend --- counter/apps.py | 6 + counter/forms.py | 17 + .../0027_alter_refilling_payment_method.py | 22 + counter/models.py | 5 +- counter/templates/counter/counter_click.jinja | 11 +- counter/views/click.py | 8 +- locale/fr/LC_MESSAGES/django.po | 397 +++++++++--------- sith/settings.py | 6 - subscription/views.py | 3 +- 9 files changed, 260 insertions(+), 215 deletions(-) create mode 100644 counter/migrations/0027_alter_refilling_payment_method.py diff --git a/counter/apps.py b/counter/apps.py index 54e7ad4c..9348cd1c 100644 --- a/counter/apps.py +++ b/counter/apps.py @@ -24,6 +24,12 @@ from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ +PAYMENT_METHOD = [ + ("CHECK", _("Check")), + ("CASH", _("Cash")), + ("CARD", _("Credit card")), +] + class CounterConfig(AppConfig): name = "counter" diff --git a/counter/forms.py b/counter/forms.py index 0a8bb3be..80a0e0ad 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -111,6 +111,8 @@ class GetUserForm(forms.Form): class RefillForm(forms.ModelForm): + allowed_refilling_methods = ["CASH", "CARD"] + error_css_class = "error" required_css_class = "required" amount = forms.FloatField( @@ -120,6 +122,21 @@ class RefillForm(forms.ModelForm): class Meta: model = Refilling fields = ["amount", "payment_method", "bank"] + widgets = {"payment_method": forms.RadioSelect} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields["payment_method"].choices = ( + method + for method in self.fields["payment_method"].choices + if method[0] in self.allowed_refilling_methods + ) + if self.fields["payment_method"].initial not in self.allowed_refilling_methods: + self.fields["payment_method"].initial = self.allowed_refilling_methods[0] + + if "CHECK" not in self.allowed_refilling_methods: + del self.fields["bank"] class CounterEditForm(forms.ModelForm): diff --git a/counter/migrations/0027_alter_refilling_payment_method.py b/counter/migrations/0027_alter_refilling_payment_method.py new file mode 100644 index 00000000..af9dd54a --- /dev/null +++ b/counter/migrations/0027_alter_refilling_payment_method.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.17 on 2024-12-15 22:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("counter", "0026_alter_studentcard_customer"), + ] + + operations = [ + migrations.AlterField( + model_name="refilling", + name="payment_method", + field=models.CharField( + choices=[("CHECK", "Check"), ("CASH", "Cash"), ("CARD", "Credit card")], + default="CARD", + max_length=255, + verbose_name="payment method", + ), + ), + ] diff --git a/counter/models.py b/counter/models.py index da2f33f4..3640c3f5 100644 --- a/counter/models.py +++ b/counter/models.py @@ -42,6 +42,7 @@ from club.models import Club from core.fields import ResizedImageField from core.models import Group, Notification, User from core.utils import get_start_of_semester +from counter.apps import PAYMENT_METHOD from sith.settings import SITH_COUNTER_OFFICES, SITH_MAIN_CLUB from subscription.models import Subscription @@ -697,8 +698,8 @@ class Refilling(models.Model): payment_method = models.CharField( _("payment method"), max_length=255, - choices=settings.SITH_COUNTER_PAYMENT_METHOD, - default="CASH", + choices=PAYMENT_METHOD, + default="CARD", ) bank = models.CharField( _("bank"), max_length=255, choices=settings.SITH_COUNTER_BANK, default="OTHER" diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 65c7d9ca..bc5c4e58 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -29,11 +29,6 @@ {{ user_mini_profile(customer.user) }} {{ user_subscription(customer.user) }}

    {% trans %}Amount: {% endtrans %}

    - - {% if counter.type == 'BAR' %} -
    {% trans %}Student card{% endtrans %}
    - {{ student_card_fragment }} - {% endif %}
    @@ -113,6 +108,12 @@ {{ refilling_fragment }}
    {% endif %} + {% if student_card_fragment %} +
    {% trans %}Student card{% endtrans %}
    +
    + {{ student_card_fragment }} +
    + {% endif %}
    diff --git a/counter/views/click.py b/counter/views/click.py index 6a379564..c6bafa57 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -394,9 +394,11 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): ) kwargs["customer"] = self.customer kwargs["basket_total"] = self.sum_basket(self.request) - kwargs["student_card_fragment"] = StudentCardFormView.get_template_data( - self.customer - ).render(self.request) + + if self.object.type == "BAR": + 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( diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 482befd3..dd9aef28 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-11 09:34+0100\n" +"POT-Creation-Date: 2024-12-16 00:11+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -18,8 +18,8 @@ msgstr "" #: accounting/models.py:62 accounting/models.py:101 accounting/models.py:132 #: accounting/models.py:190 club/models.py:55 com/models.py:274 -#: com/models.py:293 counter/models.py:297 counter/models.py:328 -#: counter/models.py:479 forum/models.py:60 launderette/models.py:29 +#: com/models.py:293 counter/models.py:298 counter/models.py:329 +#: counter/models.py:480 forum/models.py:60 launderette/models.py:29 #: launderette/models.py:80 launderette/models.py:116 msgid "name" msgstr "nom" @@ -65,8 +65,8 @@ msgid "account number" msgstr "numéro de compte" #: accounting/models.py:107 accounting/models.py:136 club/models.py:345 -#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:357 -#: counter/models.py:481 trombi/models.py:209 +#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:358 +#: counter/models.py:482 trombi/models.py:209 msgid "club" msgstr "club" @@ -87,12 +87,12 @@ msgstr "Compte club" msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" -#: accounting/models.py:188 club/models.py:351 counter/models.py:959 +#: accounting/models.py:188 club/models.py:351 counter/models.py:969 #: election/models.py:16 launderette/models.py:165 msgid "start date" msgstr "date de début" -#: accounting/models.py:189 club/models.py:352 counter/models.py:960 +#: accounting/models.py:189 club/models.py:352 counter/models.py:970 #: election/models.py:17 msgid "end date" msgstr "date de fin" @@ -105,8 +105,8 @@ msgstr "est fermé" msgid "club account" msgstr "compte club" -#: accounting/models.py:199 accounting/models.py:255 counter/models.py:91 -#: counter/models.py:677 +#: accounting/models.py:199 accounting/models.py:255 counter/models.py:92 +#: counter/models.py:687 msgid "amount" msgstr "montant" @@ -128,18 +128,18 @@ msgstr "classeur" #: accounting/models.py:256 core/models.py:956 core/models.py:1467 #: core/models.py:1512 core/models.py:1541 core/models.py:1565 -#: counter/models.py:687 counter/models.py:791 counter/models.py:995 +#: counter/models.py:697 counter/models.py:801 counter/models.py:1005 #: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312 #: forum/models.py:413 msgid "date" msgstr "date" -#: accounting/models.py:257 counter/models.py:299 counter/models.py:996 +#: accounting/models.py:257 counter/models.py:300 counter/models.py:1006 #: pedagogy/models.py:208 msgid "comment" msgstr "commentaire" -#: accounting/models.py:259 counter/models.py:689 counter/models.py:793 +#: accounting/models.py:259 counter/models.py:699 counter/models.py:803 #: subscription/models.py:56 msgid "payment method" msgstr "méthode de paiement" @@ -166,7 +166,7 @@ msgstr "type comptable" #: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460 #: accounting/models.py:492 core/models.py:1540 core/models.py:1566 -#: counter/models.py:757 +#: counter/models.py:767 msgid "label" msgstr "étiquette" @@ -264,7 +264,7 @@ msgstr "" "Vous devez fournir soit un type comptable simplifié ou un type comptable " "standard" -#: accounting/models.py:421 counter/models.py:338 pedagogy/models.py:41 +#: accounting/models.py:421 counter/models.py:339 pedagogy/models.py:41 msgid "code" msgstr "code" @@ -369,7 +369,7 @@ msgstr "Compte en banque : " #: core/templates/core/user_clubs.jinja:34 #: core/templates/core/user_clubs.jinja:63 #: core/templates/core/user_edit.jinja:62 -#: counter/templates/counter/fragments/create_student_card.jinja:22 +#: counter/templates/counter/fragments/create_student_card.jinja:25 #: counter/templates/counter/last_ops.jinja:35 #: counter/templates/counter/last_ops.jinja:65 #: election/templates/election/election_detail.jinja:191 @@ -517,7 +517,7 @@ msgid "Effective amount" msgstr "Montant effectif" #: accounting/templates/accounting/club_account_details.jinja:36 -#: sith/settings.py:469 +#: sith/settings.py:463 msgid "Closed" msgstr "Fermé" @@ -950,11 +950,11 @@ msgstr "Une action est requise" msgid "You must specify at least an user or an email address" msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" -#: club/forms.py:149 counter/forms.py:189 +#: club/forms.py:149 counter/forms.py:206 msgid "Begin date" msgstr "Date de début" -#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:192 +#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:209 #: election/views.py:170 subscription/forms.py:21 msgid "End date" msgstr "Date de fin" @@ -1041,7 +1041,7 @@ msgstr "Vous ne pouvez pas faire de boucles dans les clubs" msgid "A club with that unix_name already exists" msgstr "Un club avec ce nom UNIX existe déjà." -#: club/models.py:337 counter/models.py:950 counter/models.py:986 +#: club/models.py:337 counter/models.py:960 counter/models.py:996 #: eboutic/models.py:53 eboutic/models.py:189 election/models.py:183 #: launderette/models.py:130 launderette/models.py:184 sas/models.py:273 #: trombi/models.py:205 @@ -1053,8 +1053,8 @@ msgstr "nom d'utilisateur" msgid "role" msgstr "rôle" -#: club/models.py:359 core/models.py:90 counter/models.py:298 -#: counter/models.py:329 election/models.py:13 election/models.py:115 +#: club/models.py:359 core/models.py:90 counter/models.py:299 +#: counter/models.py:330 election/models.py:13 election/models.py:115 #: election/models.py:188 forum/models.py:61 forum/models.py:245 msgid "description" msgstr "description" @@ -2501,7 +2501,7 @@ msgstr "Forum" msgid "Gallery" msgstr "Photos" -#: core/templates/core/base/navbar.jinja:22 counter/models.py:489 +#: core/templates/core/base/navbar.jinja:22 counter/models.py:490 #: counter/templates/counter/counter_list.jinja:11 #: eboutic/templates/eboutic/eboutic_main.jinja:4 #: eboutic/templates/eboutic/eboutic_main.jinja:22 @@ -2587,7 +2587,7 @@ msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/file_delete_confirm.jinja:46 -#: counter/templates/counter/counter_click.jinja:104 +#: counter/templates/counter/counter_click.jinja:100 #: counter/templates/counter/fragments/delete_student_card.jinja:12 #: sas/templates/sas/ask_picture_removal.jinja:20 msgid "Cancel" @@ -3300,7 +3300,12 @@ msgstr "Vous avez déjà choisi ce Trombi: %(trombi)s." msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" -#: core/templates/core/user_preferences.jinja:41 +#: core/templates/core/user_preferences.jinja:39 +#: counter/templates/counter/counter_click.jinja:112 +msgid "Student card" +msgstr "Carte étudiante" + +#: core/templates/core/user_preferences.jinja:42 msgid "" "You can add a card by asking at a counter or add it yourself here. If you " "want to manually\n" @@ -3374,7 +3379,7 @@ msgstr "Cotisations" msgid "Subscription stats" msgstr "Statistiques de cotisation" -#: core/templates/core/user_tools.jinja:48 counter/forms.py:162 +#: core/templates/core/user_tools.jinja:48 counter/forms.py:179 #: counter/views/mixins.py:89 msgid "Counters" msgstr "Comptoirs" @@ -3594,8 +3599,21 @@ msgstr "Photos" msgid "Galaxy" msgstr "Galaxie" -#: counter/apps.py:30 counter/models.py:505 counter/models.py:956 -#: counter/models.py:992 launderette/models.py:32 +#: counter/apps.py:28 sith/settings.py:412 sith/settings.py:419 +msgid "Check" +msgstr "Chèque" + +#: counter/apps.py:29 sith/settings.py:413 sith/settings.py:421 +msgid "Cash" +msgstr "Espèces" + +#: counter/apps.py:30 counter/models.py:805 sith/settings.py:415 +#: sith/settings.py:420 +msgid "Credit card" +msgstr "Carte bancaire" + +#: counter/apps.py:36 counter/models.py:506 counter/models.py:966 +#: counter/models.py:1002 launderette/models.py:32 msgid "counter" msgstr "comptoir" @@ -3619,185 +3637,180 @@ msgstr "Vidange de votre compte AE" msgid "Ecocup regularization" msgstr "Régularization des ecocups" -#: counter/models.py:90 +#: counter/models.py:91 msgid "account id" msgstr "numéro de compte" -#: counter/models.py:92 +#: counter/models.py:93 msgid "recorded product" msgstr "produits consignés" -#: counter/models.py:97 +#: counter/models.py:98 msgid "customer" msgstr "client" -#: counter/models.py:98 +#: counter/models.py:99 msgid "customers" msgstr "clients" -#: counter/models.py:110 counter/views/click.py:66 +#: counter/models.py:111 counter/views/click.py:68 msgid "Not enough money" msgstr "Solde insuffisant" -#: counter/models.py:196 +#: counter/models.py:197 msgid "First name" msgstr "Prénom" -#: counter/models.py:197 +#: counter/models.py:198 msgid "Last name" msgstr "Nom de famille" -#: counter/models.py:198 +#: counter/models.py:199 msgid "Address 1" msgstr "Adresse 1" -#: counter/models.py:199 +#: counter/models.py:200 msgid "Address 2" msgstr "Adresse 2" -#: counter/models.py:200 +#: counter/models.py:201 msgid "Zip code" msgstr "Code postal" -#: counter/models.py:201 +#: counter/models.py:202 msgid "City" msgstr "Ville" -#: counter/models.py:202 +#: counter/models.py:203 msgid "Country" msgstr "Pays" -#: counter/models.py:210 +#: counter/models.py:211 msgid "Phone number" msgstr "Numéro de téléphone" -#: counter/models.py:252 +#: counter/models.py:253 msgid "When the mail warning that the account was about to be dumped was sent." msgstr "Quand le mail d'avertissement de la vidange du compte a été envoyé." -#: counter/models.py:257 +#: counter/models.py:258 msgid "Set this to True if the warning mail received an error" msgstr "Mettre à True si le mail a reçu une erreur" -#: counter/models.py:264 +#: counter/models.py:265 msgid "The operation that emptied the account." msgstr "L'opération qui a vidé le compte." -#: counter/models.py:309 counter/models.py:333 +#: counter/models.py:310 counter/models.py:334 msgid "product type" msgstr "type du produit" -#: counter/models.py:340 +#: counter/models.py:341 msgid "purchase price" msgstr "prix d'achat" -#: counter/models.py:341 +#: counter/models.py:342 msgid "Initial cost of purchasing the product" msgstr "Coût initial d'achat du produit" -#: counter/models.py:343 +#: counter/models.py:344 msgid "selling price" msgstr "prix de vente" -#: counter/models.py:345 +#: counter/models.py:346 msgid "special selling price" msgstr "prix de vente spécial" -#: counter/models.py:346 +#: counter/models.py:347 msgid "Price for barmen during their permanence" msgstr "Prix pour les barmen durant leur permanence" -#: counter/models.py:354 +#: counter/models.py:355 msgid "icon" msgstr "icône" -#: counter/models.py:359 +#: counter/models.py:360 msgid "limit age" msgstr "âge limite" -#: counter/models.py:360 +#: counter/models.py:361 msgid "tray price" msgstr "prix plateau" -#: counter/models.py:362 +#: counter/models.py:363 msgid "buying groups" msgstr "groupe d'achat" -#: counter/models.py:364 election/models.py:50 +#: counter/models.py:365 election/models.py:50 msgid "archived" msgstr "archivé" -#: counter/models.py:367 counter/models.py:1090 +#: counter/models.py:368 counter/models.py:1100 msgid "product" msgstr "produit" -#: counter/models.py:484 +#: counter/models.py:485 msgid "products" msgstr "produits" -#: counter/models.py:487 +#: counter/models.py:488 msgid "counter type" msgstr "type de comptoir" -#: counter/models.py:489 +#: counter/models.py:490 msgid "Bar" msgstr "Bar" -#: counter/models.py:489 +#: counter/models.py:490 msgid "Office" msgstr "Bureau" -#: counter/models.py:492 +#: counter/models.py:493 msgid "sellers" msgstr "vendeurs" -#: counter/models.py:500 launderette/models.py:178 +#: counter/models.py:501 launderette/models.py:178 msgid "token" msgstr "jeton" -#: counter/models.py:695 +#: counter/models.py:705 msgid "bank" msgstr "banque" -#: counter/models.py:697 counter/models.py:798 +#: counter/models.py:707 counter/models.py:808 msgid "is validated" msgstr "est validé" -#: counter/models.py:702 +#: counter/models.py:712 msgid "refilling" msgstr "rechargement" -#: counter/models.py:775 eboutic/models.py:249 +#: counter/models.py:785 eboutic/models.py:249 msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:776 counter/models.py:1070 eboutic/models.py:250 +#: counter/models.py:786 counter/models.py:1080 eboutic/models.py:250 msgid "quantity" msgstr "quantité" -#: counter/models.py:795 +#: counter/models.py:805 msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:795 sith/settings.py:415 sith/settings.py:420 -#: sith/settings.py:440 -msgid "Credit card" -msgstr "Carte bancaire" - -#: counter/models.py:803 +#: counter/models.py:813 msgid "selling" msgstr "vente" -#: counter/models.py:907 +#: counter/models.py:917 msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:908 +#: counter/models.py:918 #, python-format msgid "Eticket bought for the event %(event)s" msgstr "Eticket acheté pour l'événement %(event)s" -#: counter/models.py:910 counter/models.py:923 +#: counter/models.py:920 counter/models.py:933 #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -3809,67 +3822,67 @@ msgstr "" "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "%(url)s." -#: counter/models.py:961 +#: counter/models.py:971 msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:964 +#: counter/models.py:974 msgid "permanency" msgstr "permanence" -#: counter/models.py:997 +#: counter/models.py:1007 msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:1000 +#: counter/models.py:1010 msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:1066 +#: counter/models.py:1076 msgid "cash summary" msgstr "relevé" -#: counter/models.py:1069 +#: counter/models.py:1079 msgid "value" msgstr "valeur" -#: counter/models.py:1072 +#: counter/models.py:1082 msgid "check" msgstr "chèque" -#: counter/models.py:1074 +#: counter/models.py:1084 msgid "True if this is a bank check, else False" msgstr "Vrai si c'est un chèque, sinon Faux." -#: counter/models.py:1078 +#: counter/models.py:1088 msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:1094 +#: counter/models.py:1104 msgid "banner" msgstr "bannière" -#: counter/models.py:1096 +#: counter/models.py:1106 msgid "event date" msgstr "date de l'événement" -#: counter/models.py:1098 +#: counter/models.py:1108 msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:1100 +#: counter/models.py:1110 msgid "secret" msgstr "secret" -#: counter/models.py:1139 +#: counter/models.py:1149 msgid "uid" msgstr "uid" -#: counter/models.py:1144 counter/models.py:1149 +#: counter/models.py:1154 counter/models.py:1159 msgid "student card" msgstr "carte étudiante" -#: counter/models.py:1150 +#: counter/models.py:1160 msgid "student cards" msgstr "cartes étudiantes" @@ -3929,14 +3942,14 @@ msgstr "oui" msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." -#: counter/templates/counter/counter_click.jinja:39 +#: counter/templates/counter/counter_click.jinja:35 #: launderette/templates/launderette/launderette_admin.jinja:8 msgid "Selling" msgstr "Vente" -#: counter/templates/counter/counter_click.jinja:50 -#: counter/templates/counter/counter_click.jinja:115 -#: counter/templates/counter/fragments/create_student_card.jinja:11 +#: counter/templates/counter/counter_click.jinja:46 +#: counter/templates/counter/fragments/create_refill.jinja:8 +#: counter/templates/counter/fragments/create_student_card.jinja:10 #: counter/templates/counter/invoices_call.jinja:16 #: launderette/templates/launderette/launderette_admin.jinja:35 #: launderette/templates/launderette/launderette_click.jinja:13 @@ -3945,16 +3958,16 @@ msgstr "Vente" msgid "Go" msgstr "Valider" -#: counter/templates/counter/counter_click.jinja:57 +#: counter/templates/counter/counter_click.jinja:53 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:19 msgid "Basket: " msgstr "Panier : " -#: counter/templates/counter/counter_click.jinja:98 +#: counter/templates/counter/counter_click.jinja:94 msgid "Finish" msgstr "Terminer" -#: counter/templates/counter/counter_click.jinja:108 +#: counter/templates/counter/counter_click.jinja:104 #: counter/templates/counter/refilling_list.jinja:9 msgid "Refilling" msgstr "Rechargement" @@ -4038,23 +4051,19 @@ msgstr "Nouveau eticket" msgid "There is no eticket in this website." msgstr "Il n'y a pas de eticket sur ce site web." -#: counter/templates/counter/fragments/create_student_card.jinja:2 -msgid "Student card" -msgstr "Carte étudiante" - -#: counter/templates/counter/fragments/create_student_card.jinja:13 +#: counter/templates/counter/fragments/create_student_card.jinja:12 msgid "No student card registered." msgstr "Aucune carte étudiante enregistrée." +#: counter/templates/counter/fragments/create_student_card.jinja:15 +#, python-format +msgid "uid: %(uid)s " +msgstr "uid: %(uid)s" + #: counter/templates/counter/fragments/create_student_card.jinja:16 msgid "Card registered" msgstr "Carte enregistrée" -#: counter/templates/counter/fragments/create_student_card.jinja:17 -#, python-format -msgid "uid: %(uid)s " -msgstr "uid: %(uid)s" - #: counter/templates/counter/invoices_call.jinja:8 #, python-format msgid "Invoices call for %(date)s" @@ -4277,19 +4286,19 @@ msgstr "Montant du chèque" msgid "Check quantity" msgstr "Nombre de chèque" -#: counter/views/click.py:57 +#: counter/views/click.py:59 msgid "Too young for that product" msgstr "Trop jeune pour ce produit" -#: counter/views/click.py:60 +#: counter/views/click.py:62 msgid "Not allowed for that product" msgstr "Non autorisé pour ce produit" -#: counter/views/click.py:63 +#: counter/views/click.py:65 msgid "No date of birth provided" msgstr "Pas de date de naissance renseignée" -#: counter/views/click.py:331 +#: counter/views/click.py:325 msgid "You have not enough money to buy all the basket" msgstr "Vous n'avez pas assez d'argent pour acheter le panier" @@ -4907,12 +4916,12 @@ msgid "Washing and drying" msgstr "Lavage et séchage" #: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:658 +#: sith/settings.py:652 msgid "Washing" msgstr "Lavage" #: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:658 +#: sith/settings.py:652 msgid "Drying" msgstr "Séchage" @@ -5427,11 +5436,11 @@ msgstr "Personne(s)" msgid "Identify users on pictures" msgstr "Identifiez les utilisateurs sur les photos" -#: sith/settings.py:253 sith/settings.py:477 +#: sith/settings.py:253 sith/settings.py:471 msgid "English" msgstr "Anglais" -#: sith/settings.py:253 sith/settings.py:476 +#: sith/settings.py:253 sith/settings.py:470 msgid "French" msgstr "Français" @@ -5455,7 +5464,7 @@ msgstr "INFO" msgid "GI" msgstr "GI" -#: sith/settings.py:401 sith/settings.py:487 +#: sith/settings.py:401 sith/settings.py:481 msgid "E" msgstr "E" @@ -5487,14 +5496,6 @@ msgstr "Humanités" msgid "N/A" msgstr "N/A" -#: sith/settings.py:412 sith/settings.py:419 sith/settings.py:438 -msgid "Check" -msgstr "Chèque" - -#: sith/settings.py:413 sith/settings.py:421 sith/settings.py:439 -msgid "Cash" -msgstr "Espèces" - #: sith/settings.py:414 msgid "Transfert" msgstr "Virement" @@ -5511,296 +5512,296 @@ msgstr "Sevenans" msgid "Montbéliard" msgstr "Montbéliard" -#: sith/settings.py:457 +#: sith/settings.py:451 msgid "Free" msgstr "Libre" -#: sith/settings.py:458 +#: sith/settings.py:452 msgid "CS" msgstr "CS" -#: sith/settings.py:459 +#: sith/settings.py:453 msgid "TM" msgstr "TM" -#: sith/settings.py:460 +#: sith/settings.py:454 msgid "OM" msgstr "OM" -#: sith/settings.py:461 +#: sith/settings.py:455 msgid "QC" msgstr "QC" -#: sith/settings.py:462 +#: sith/settings.py:456 msgid "EC" msgstr "EC" -#: sith/settings.py:463 +#: sith/settings.py:457 msgid "RN" msgstr "RN" -#: sith/settings.py:464 +#: sith/settings.py:458 msgid "ST" msgstr "ST" -#: sith/settings.py:465 +#: sith/settings.py:459 msgid "EXT" msgstr "EXT" -#: sith/settings.py:470 +#: sith/settings.py:464 msgid "Autumn" msgstr "Automne" -#: sith/settings.py:471 +#: sith/settings.py:465 msgid "Spring" msgstr "Printemps" -#: sith/settings.py:472 +#: sith/settings.py:466 msgid "Autumn and spring" msgstr "Automne et printemps" -#: sith/settings.py:478 +#: sith/settings.py:472 msgid "German" msgstr "Allemand" -#: sith/settings.py:479 +#: sith/settings.py:473 msgid "Spanish" msgstr "Espagnol" -#: sith/settings.py:483 +#: sith/settings.py:477 msgid "A" msgstr "A" -#: sith/settings.py:484 +#: sith/settings.py:478 msgid "B" msgstr "B" -#: sith/settings.py:485 +#: sith/settings.py:479 msgid "C" msgstr "C" -#: sith/settings.py:486 +#: sith/settings.py:480 msgid "D" msgstr "D" -#: sith/settings.py:488 +#: sith/settings.py:482 msgid "FX" msgstr "FX" -#: sith/settings.py:489 +#: sith/settings.py:483 msgid "F" msgstr "F" -#: sith/settings.py:490 +#: sith/settings.py:484 msgid "Abs" msgstr "Abs" -#: sith/settings.py:494 +#: sith/settings.py:488 msgid "Selling deletion" msgstr "Suppression de vente" -#: sith/settings.py:495 +#: sith/settings.py:489 msgid "Refilling deletion" msgstr "Suppression de rechargement" -#: sith/settings.py:539 +#: sith/settings.py:533 msgid "One semester" msgstr "Un semestre, 20 €" -#: sith/settings.py:540 +#: sith/settings.py:534 msgid "Two semesters" msgstr "Deux semestres, 35 €" -#: sith/settings.py:542 +#: sith/settings.py:536 msgid "Common core cursus" msgstr "Cursus tronc commun, 60 €" -#: sith/settings.py:546 +#: sith/settings.py:540 msgid "Branch cursus" msgstr "Cursus branche, 60 €" -#: sith/settings.py:547 +#: sith/settings.py:541 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:548 +#: sith/settings.py:542 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:549 +#: sith/settings.py:543 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:550 +#: sith/settings.py:544 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:551 +#: sith/settings.py:545 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:552 +#: sith/settings.py:546 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:553 +#: sith/settings.py:547 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 20 €" -#: sith/settings.py:555 +#: sith/settings.py:549 msgid "One semester Welcome Week" msgstr "Un semestre Welcome Week" -#: sith/settings.py:559 +#: sith/settings.py:553 msgid "One month for free" msgstr "Un mois gratuit" -#: sith/settings.py:560 +#: sith/settings.py:554 msgid "Two months for free" msgstr "Deux mois gratuits" -#: sith/settings.py:561 +#: sith/settings.py:555 msgid "Eurok's volunteer" msgstr "Bénévole Eurockéennes" -#: sith/settings.py:563 +#: sith/settings.py:557 msgid "Six weeks for free" msgstr "6 semaines gratuites" -#: sith/settings.py:567 +#: sith/settings.py:561 msgid "One day" msgstr "Un jour" -#: sith/settings.py:568 +#: sith/settings.py:562 msgid "GA staff member" msgstr "Membre staff GA (2 semaines), 1 €" -#: sith/settings.py:571 +#: sith/settings.py:565 msgid "One semester (-20%)" msgstr "Un semestre (-20%), 12 €" -#: sith/settings.py:576 +#: sith/settings.py:570 msgid "Two semesters (-20%)" msgstr "Deux semestres (-20%), 22 €" -#: sith/settings.py:581 +#: sith/settings.py:575 msgid "Common core cursus (-20%)" msgstr "Cursus tronc commun (-20%), 36 €" -#: sith/settings.py:586 +#: sith/settings.py:580 msgid "Branch cursus (-20%)" msgstr "Cursus branche (-20%), 36 €" -#: sith/settings.py:591 +#: sith/settings.py:585 msgid "Alternating cursus (-20%)" msgstr "Cursus alternant (-20%), 24 €" -#: sith/settings.py:597 +#: sith/settings.py:591 msgid "One year for free(CA offer)" msgstr "Une année offerte (Offre CA)" -#: sith/settings.py:617 +#: sith/settings.py:611 msgid "President" msgstr "Président⸱e" -#: sith/settings.py:618 +#: sith/settings.py:612 msgid "Vice-President" msgstr "Vice-Président⸱e" -#: sith/settings.py:619 +#: sith/settings.py:613 msgid "Treasurer" msgstr "Trésorier⸱e" -#: sith/settings.py:620 +#: sith/settings.py:614 msgid "Communication supervisor" msgstr "Responsable communication" -#: sith/settings.py:621 +#: sith/settings.py:615 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:622 +#: sith/settings.py:616 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:623 +#: sith/settings.py:617 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:624 +#: sith/settings.py:618 msgid "Active member" msgstr "Membre actif⸱ve" -#: sith/settings.py:625 +#: sith/settings.py:619 msgid "Curious" msgstr "Curieux⸱euse" -#: sith/settings.py:662 +#: sith/settings.py:656 msgid "A new poster needs to be moderated" msgstr "Une nouvelle affiche a besoin d'être modérée" -#: sith/settings.py:663 +#: sith/settings.py:657 msgid "A new mailing list needs to be moderated" msgstr "Une nouvelle mailing list a besoin d'être modérée" -#: sith/settings.py:666 +#: sith/settings.py:660 msgid "A new pedagogy comment has been signaled for moderation" msgstr "" "Un nouveau commentaire de la pédagogie a été signalé pour la modération" -#: sith/settings.py:668 +#: sith/settings.py:662 #, python-format msgid "There are %s fresh news to be moderated" msgstr "Il y a %s nouvelles toutes fraîches à modérer" -#: sith/settings.py:669 +#: sith/settings.py:663 msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:670 +#: sith/settings.py:664 #, python-format msgid "There are %s pictures to be moderated in the SAS" msgstr "Il y a %s photos à modérer dans le SAS" -#: sith/settings.py:671 +#: sith/settings.py:665 msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:672 +#: sith/settings.py:666 #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s€" -#: sith/settings.py:673 +#: sith/settings.py:667 #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:674 +#: sith/settings.py:668 msgid "You have a notification" msgstr "Vous avez une notification" -#: sith/settings.py:686 +#: sith/settings.py:680 msgid "Success!" msgstr "Succès !" -#: sith/settings.py:687 +#: sith/settings.py:681 msgid "Fail!" msgstr "Échec !" -#: sith/settings.py:688 +#: sith/settings.py:682 msgid "You successfully posted an article in the Weekmail" msgstr "Article posté avec succès dans le Weekmail" -#: sith/settings.py:689 +#: sith/settings.py:683 msgid "You successfully edited an article in the Weekmail" msgstr "Article édité avec succès dans le Weekmail" -#: sith/settings.py:690 +#: sith/settings.py:684 msgid "You successfully sent the Weekmail" msgstr "Weekmail envoyé avec succès" -#: sith/settings.py:698 +#: sith/settings.py:692 msgid "AE tee-shirt" msgstr "Tee-shirt AE" diff --git a/sith/settings.py b/sith/settings.py index ba0a35da..56c6c0bc 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -434,12 +434,6 @@ SITH_COUNTER_BARS = [(1, "MDE"), (2, "Foyer"), (35, "La Gommette")] SITH_COUNTER_OFFICES = {2: "PdF", 1: "AE"} -SITH_COUNTER_PAYMENT_METHOD = [ - ("CHECK", _("Check")), - ("CASH", _("Cash")), - ("CARD", _("Credit card")), -] - SITH_COUNTER_BANK = [ ("OTHER", "Autre"), ("SOCIETE-GENERALE", "Société générale"), diff --git a/subscription/views.py b/subscription/views.py index 2948391b..b285a137 100644 --- a/subscription/views.py +++ b/subscription/views.py @@ -21,6 +21,7 @@ from django.utils.timezone import localdate from django.views.generic import CreateView, DetailView, TemplateView from django.views.generic.edit import FormView +from counter.apps import PAYMENT_METHOD from subscription.forms import ( SelectionDateForm, SubscriptionExistingUserForm, @@ -108,6 +109,6 @@ class SubscriptionsStatsView(FormView): subscription_end__gte=self.end_date, subscription_start__lte=self.start_date ) kwargs["subscriptions_types"] = settings.SITH_SUBSCRIPTIONS - kwargs["payment_types"] = settings.SITH_COUNTER_PAYMENT_METHOD + kwargs["payment_types"] = PAYMENT_METHOD kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS return kwargs From 379527cd58dc694d5396a02dee7f8130fa8934ff Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 16 Dec 2024 00:58:23 +0100 Subject: [PATCH 47/53] Add a nice animation on successful refilling --- counter/static/bundled/counter/counter-click-index.ts | 5 +++++ counter/templates/counter/counter_click.jinja | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/counter/static/bundled/counter/counter-click-index.ts b/counter/static/bundled/counter/counter-click-index.ts index 01d7e8f9..044d14de 100644 --- a/counter/static/bundled/counter/counter-click-index.ts +++ b/counter/static/bundled/counter/counter-click-index.ts @@ -44,6 +44,11 @@ exportToHtml("loadCounter", (config: CounterConfig) => { ).data.amount; }, + async onRefillingSuccess() { + await this.updateBalance(); + document.getElementById("selling-accordion").click(); + }, + async handleCode(event: SubmitEvent) { const code = ( $(event.target).find("#code_field").val() as string diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index bc5c4e58..f9821bf1 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -32,7 +32,7 @@
    -
    {% trans %}Selling{% endtrans %}
    +
    {% trans %}Selling{% endtrans %}
    {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} @@ -103,7 +103,7 @@ {% if refilling_fragment %}
    {% trans %}Refilling{% endtrans %}
    {{ refilling_fragment }}
    From 4c65939bbe40a43dfc04941863f7f6b84ae59b96 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 16 Dec 2024 09:31:43 +0100 Subject: [PATCH 48/53] Fix crash when admin gets to preferences of an user subscribed to a trombinoscope --- core/templates/core/user_preferences.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index 13bee656..d20f708f 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -28,7 +28,7 @@ {% else %} -

    {% trans trombi=user.trombi_user.trombi %}You already choose to be in that Trombi: {{ trombi }}.{% endtrans %} +

    {% trans trombi=profile.trombi_user.trombi %}You already choose to be in that Trombi: {{ trombi }}.{% endtrans %}
    {% trans %}Go to my Trombi tools{% endtrans %}

    From 66e5ef64fd5c5dab3503fb7bd6edae6a834af5a7 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 17 Dec 2024 00:45:51 +0100 Subject: [PATCH 49/53] Don't use API to update amount after a refilling query --- .../bundled/counter/counter-click-index.ts | 21 +++++++------------ counter/templates/counter/counter_click.jinja | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/counter/static/bundled/counter/counter-click-index.ts b/counter/static/bundled/counter/counter-click-index.ts index 044d14de..b8de64be 100644 --- a/counter/static/bundled/counter/counter-click-index.ts +++ b/counter/static/bundled/counter/counter-click-index.ts @@ -1,5 +1,4 @@ import { exportToHtml } from "#core:utils/globals"; -import { customerGetCustomer } from "#openapi"; interface CounterConfig { csrfToken: string; @@ -33,19 +32,13 @@ exportToHtml("loadCounter", (config: CounterConfig) => { return total / 100; }, - async updateBalance() { - this.customerBalance = ( - await customerGetCustomer({ - path: { - // biome-ignore lint/style/useNamingConvention: api is in snake_case - customer_id: config.customerId, - }, - }) - ).data.amount; - }, - - async onRefillingSuccess() { - await this.updateBalance(); + onRefillingSuccess(event: CustomEvent) { + if (event.type !== "htmx:after-request" || event.detail.failed) { + return; + } + this.customerBalance += Number.parseFloat( + (event.detail.target.querySelector("#id_amount") as HTMLInputElement).value, + ); document.getElementById("selling-accordion").click(); }, diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index f9821bf1..cb62be31 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -103,7 +103,7 @@ {% if refilling_fragment %}
    {% trans %}Refilling{% endtrans %}
    {{ refilling_fragment }}
    From a0eb53a607d8e8baed40d3b5d480d2943b9fe183 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 17 Dec 2024 01:41:45 +0100 Subject: [PATCH 50/53] Apply review comments --- counter/models.py | 11 +++++------ counter/views/click.py | 2 +- counter/views/student_card.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/counter/models.py b/counter/models.py index 3640c3f5..18215856 100644 --- a/counter/models.py +++ b/counter/models.py @@ -652,13 +652,12 @@ class Counter(models.Model): )["total"] def customer_is_barman(self, customer: Customer | User) -> bool: - """Check if current counter is a `bar` and that the customer is on the barmen_list + """Check if this counter is a `bar` and if the customer is currently logged in. + This is useful to compute special prices.""" - This is useful to compute special prices""" - if isinstance(customer, Customer): - customer: User = customer.user - - return self.type == "BAR" and customer in self.barmen_list + # Customer and User are two different tables, + # but they share the same primary key + return self.type == "BAR" and any(b.pk == customer.pk for b in self.barmen_list) class RefillingQuerySet(models.QuerySet): diff --git a/counter/views/click.py b/counter/views/click.py index c6bafa57..9542b467 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -417,7 +417,7 @@ class RefillingCreateView(FormView): def get_template_data( cls, customer: Customer, *, form_instance: form_class | None = None ) -> FormFragmentTemplateData[form_class]: - return FormFragmentTemplateData[cls.form_class]( + return FormFragmentTemplateData( form=form_instance if form_instance else cls.form_class(), template=cls.template_name, context={ diff --git a/counter/views/student_card.py b/counter/views/student_card.py index 952f65eb..f916260b 100644 --- a/counter/views/student_card.py +++ b/counter/views/student_card.py @@ -73,7 +73,7 @@ class StudentCardFormView(FormView): cls, customer: Customer, *, form_instance: form_class | None = None ) -> FormFragmentTemplateData[form_class]: """Get necessary data to pre-render the fragment""" - return FormFragmentTemplateData[cls.form_class]( + return FormFragmentTemplateData( form=form_instance if form_instance else cls.form_class(), template=cls.template_name, context={ From fc0ef29738086e843fda5fb3a40139377a7a8c7b Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 17 Dec 2024 01:42:10 +0100 Subject: [PATCH 51/53] Remove GetCustomer API endpoint --- counter/api.py | 17 +----- counter/schemas.py | 8 +-- counter/tests/test_api.py | 105 ---------------------------------- counter/tests/test_counter.py | 4 +- 4 files changed, 4 insertions(+), 130 deletions(-) delete mode 100644 counter/tests/test_api.py diff --git a/counter/api.py b/counter/api.py index e51aea26..f3f0f101 100644 --- a/counter/api.py +++ b/counter/api.py @@ -21,12 +21,11 @@ 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, IsLoggedInCounter, IsRoot -from counter.models import Counter, Customer, Product +from core.api_permissions import CanAccessLookup, CanView, IsRoot +from counter.models import Counter, Product from counter.schemas import ( CounterFilterSchema, CounterSchema, - CustomerSchema, ProductSchema, SimplifiedCounterSchema, ) @@ -61,18 +60,6 @@ class CounterController(ControllerBase): return filters.filter(Counter.objects.all()) -@api_controller("/customer") -class CustomerController(ControllerBase): - @route.get( - "{customer_id}", - response=CustomerSchema, - permissions=[IsLoggedInCounter], - url_name="get_customer", - ) - def get_customer(self, customer_id: int): - return self.get_object_or_exception(Customer, pk=customer_id) - - @api_controller("/product") class ProductController(ControllerBase): @route.get( diff --git a/counter/schemas.py b/counter/schemas.py index 7fbe1a71..ec1a842d 100644 --- a/counter/schemas.py +++ b/counter/schemas.py @@ -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, Customer, Product +from counter.models import Counter, Product class CounterSchema(ModelSchema): @@ -16,12 +16,6 @@ class CounterSchema(ModelSchema): fields = ["id", "name", "type", "club", "products"] -class CustomerSchema(ModelSchema): - class Meta: - model = Customer - fields = ["user", "account_id", "amount", "recorded_products"] - - class CounterFilterSchema(FilterSchema): search: Annotated[str, MinLen(1)] = Field(None, q="name__icontains") diff --git a/counter/tests/test_api.py b/counter/tests/test_api.py deleted file mode 100644 index 8a5efdc1..00000000 --- a/counter/tests/test_api.py +++ /dev/null @@ -1,105 +0,0 @@ -import pytest -from django.contrib.auth.models import make_password -from django.test.client import Client -from django.urls import reverse -from model_bakery import baker - -from core.baker_recipes import board_user, subscriber_user -from core.models import User -from counter.models import Counter - - -@pytest.fixture -def customer_user() -> User: - return subscriber_user.make() - - -@pytest.fixture -def counter_bar() -> Counter: - return baker.make(Counter, type="BAR") - - -@pytest.fixture -def barmen(counter_bar: Counter) -> User: - user = subscriber_user.make(password=make_password("plop")) - counter_bar.sellers.add(user) - return user - - -@pytest.fixture -def board_member() -> User: - return board_user.make() - - -@pytest.fixture -def root_user() -> User: - return baker.make(User, is_superuser=True) - - -@pytest.mark.django_db -@pytest.mark.parametrize( - ("connected_user"), - [ - None, # Anonymous user - "barmen", - "customer_user", - "board_member", - "root_user", - ], -) -def test_get_customer_fail( - client: Client, - customer_user: User, - request: pytest.FixtureRequest, - connected_user: str | None, -): - if connected_user is not None: - client.force_login(request.getfixturevalue(connected_user)) - assert ( - client.get( - reverse("api:get_customer", kwargs={"customer_id": customer_user.id}) - ).status_code - == 403 - ) - - -@pytest.mark.django_db -def test_get_customer_from_bar_fail_wrong_referrer( - client: Client, customer_user: User, barmen: User, counter_bar: Counter -): - client.post( - reverse("counter:login", args=[counter_bar.pk]), - {"username": barmen.username, "password": "plop"}, - ) - - assert ( - client.get( - reverse("api:get_customer", kwargs={"customer_id": customer_user.id}) - ).status_code - == 403 - ) - - -@pytest.mark.django_db -def test_get_customer_from_bar_success( - client: Client, customer_user: User, barmen: User, counter_bar: Counter -): - client.post( - reverse("counter:login", args=[counter_bar.pk]), - {"username": barmen.username, "password": "plop"}, - ) - - response = client.get( - reverse("api:get_customer", kwargs={"customer_id": customer_user.id}), - HTTP_REFERER=reverse( - "counter:click", - kwargs={"counter_id": counter_bar.id, "user_id": customer_user.id}, - ), - ) - assert response.status_code == 200 - assert response.json() == { - "user": customer_user.id, - "account_id": customer_user.customer.account_id, - "amount": f"{customer_user.customer.amount:.2f}", - "recorded_products": customer_user.customer.recorded_products, - } diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index 2574865f..9c6d3b7a 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -72,9 +72,7 @@ class TestCounter(TestCase): kwargs={"customer_id": self.richard.customer.pk}, ) - response = self.client.get( - response.get("location"), - ) + response = self.client.get(counter_url) assert ">Richard Batsbak Date: Tue, 17 Dec 2024 02:42:07 +0100 Subject: [PATCH 52/53] Fix refill permissions * Remove ability to refill from counters * Fix bug where you could refill without any board member on a BAR * Add a warning message explaining why refilling are disabled --- counter/models.py | 5 +- counter/templates/counter/counter_click.jinja | 35 ++- counter/tests/test_counter.py | 2 +- locale/fr/LC_MESSAGES/django.po | 246 +++++++++--------- sith/settings.py | 2 - 5 files changed, 152 insertions(+), 138 deletions(-) diff --git a/counter/models.py b/counter/models.py index 18215856..087baffc 100644 --- a/counter/models.py +++ b/counter/models.py @@ -43,7 +43,7 @@ from core.fields import ResizedImageField from core.models import Group, Notification, User from core.utils import get_start_of_semester from counter.apps import PAYMENT_METHOD -from sith.settings import SITH_COUNTER_OFFICES, SITH_MAIN_CLUB +from sith.settings import SITH_MAIN_CLUB from subscription.models import Subscription @@ -559,9 +559,6 @@ class Counter(models.Model): """Show if the counter authorize the refilling with physic money.""" if self.type != "BAR": return False - if self.id in SITH_COUNTER_OFFICES: - # If the counter is either 'AE' or 'BdF', refills are authorized - return True # at least one of the barmen is in the AE board ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"]) return any(ae.get_membership_for(barman) for barman in self.barmen_list) diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index cb62be31..96a72435 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -31,7 +31,7 @@

    {% trans %}Amount: {% endtrans %}

    -
    +
    {% trans %}Selling{% endtrans %}
    {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %} @@ -100,19 +100,28 @@
    - {% if refilling_fragment %} + {% if object.type == "BAR" %}
    {% trans %}Refilling{% endtrans %}
    -
    - {{ refilling_fragment }} -
    - {% endif %} - {% if student_card_fragment %} -
    {% trans %}Student card{% endtrans %}
    -
    - {{ student_card_fragment }} -
    + {% if refilling_fragment %} +
    + {{ refilling_fragment }} +
    + {% else %} +
    +

    + {% trans trimmed %}As a barman, you are not able to refill any account on your own. An admin should be connected on this counter for that. The customer can refill by using the eboutic.{% endtrans %} +

    +
    + {% endif %} + {% if student_card_fragment %} +
    {% trans %}Student card{% endtrans %}
    +
    + {{ student_card_fragment }} +
    + {% endif %} + {% endif %}
    diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index 9c6d3b7a..154578c6 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -159,7 +159,7 @@ class TestCounter(TestCase): }, HTTP_REFERER=counter_url, ) - assert response.status_code == 302 + assert response.status_code == 403 # Krophil is not board admin def test_annotate_has_barman_queryset(self): """Test if the custom queryset method `annotate_has_barman` works as intended.""" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index dd9aef28..6fb47e5b 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-16 00:11+0100\n" +"POT-Creation-Date: 2024-12-17 02:39+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -87,12 +87,12 @@ msgstr "Compte club" msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" -#: accounting/models.py:188 club/models.py:351 counter/models.py:969 +#: accounting/models.py:188 club/models.py:351 counter/models.py:965 #: election/models.py:16 launderette/models.py:165 msgid "start date" msgstr "date de début" -#: accounting/models.py:189 club/models.py:352 counter/models.py:970 +#: accounting/models.py:189 club/models.py:352 counter/models.py:966 #: election/models.py:17 msgid "end date" msgstr "date de fin" @@ -106,7 +106,7 @@ msgid "club account" msgstr "compte club" #: accounting/models.py:199 accounting/models.py:255 counter/models.py:92 -#: counter/models.py:687 +#: counter/models.py:683 msgid "amount" msgstr "montant" @@ -128,18 +128,18 @@ msgstr "classeur" #: accounting/models.py:256 core/models.py:956 core/models.py:1467 #: core/models.py:1512 core/models.py:1541 core/models.py:1565 -#: counter/models.py:697 counter/models.py:801 counter/models.py:1005 +#: counter/models.py:693 counter/models.py:797 counter/models.py:1001 #: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312 #: forum/models.py:413 msgid "date" msgstr "date" -#: accounting/models.py:257 counter/models.py:300 counter/models.py:1006 +#: accounting/models.py:257 counter/models.py:300 counter/models.py:1002 #: pedagogy/models.py:208 msgid "comment" msgstr "commentaire" -#: accounting/models.py:259 counter/models.py:699 counter/models.py:803 +#: accounting/models.py:259 counter/models.py:695 counter/models.py:799 #: subscription/models.py:56 msgid "payment method" msgstr "méthode de paiement" @@ -166,7 +166,7 @@ msgstr "type comptable" #: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460 #: accounting/models.py:492 core/models.py:1540 core/models.py:1566 -#: counter/models.py:767 +#: counter/models.py:763 msgid "label" msgstr "étiquette" @@ -517,7 +517,7 @@ msgid "Effective amount" msgstr "Montant effectif" #: accounting/templates/accounting/club_account_details.jinja:36 -#: sith/settings.py:463 +#: sith/settings.py:461 msgid "Closed" msgstr "Fermé" @@ -1041,7 +1041,7 @@ msgstr "Vous ne pouvez pas faire de boucles dans les clubs" msgid "A club with that unix_name already exists" msgstr "Un club avec ce nom UNIX existe déjà." -#: club/models.py:337 counter/models.py:960 counter/models.py:996 +#: club/models.py:337 counter/models.py:956 counter/models.py:992 #: eboutic/models.py:53 eboutic/models.py:189 election/models.py:183 #: launderette/models.py:130 launderette/models.py:184 sas/models.py:273 #: trombi/models.py:205 @@ -3301,7 +3301,7 @@ msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" #: core/templates/core/user_preferences.jinja:39 -#: counter/templates/counter/counter_click.jinja:112 +#: counter/templates/counter/counter_click.jinja:119 msgid "Student card" msgstr "Carte étudiante" @@ -3607,13 +3607,13 @@ msgstr "Chèque" msgid "Cash" msgstr "Espèces" -#: counter/apps.py:30 counter/models.py:805 sith/settings.py:415 +#: counter/apps.py:30 counter/models.py:801 sith/settings.py:415 #: sith/settings.py:420 msgid "Credit card" msgstr "Carte bancaire" -#: counter/apps.py:36 counter/models.py:506 counter/models.py:966 -#: counter/models.py:1002 launderette/models.py:32 +#: counter/apps.py:36 counter/models.py:506 counter/models.py:962 +#: counter/models.py:998 launderette/models.py:32 msgid "counter" msgstr "comptoir" @@ -3745,7 +3745,7 @@ msgstr "groupe d'achat" msgid "archived" msgstr "archivé" -#: counter/models.py:368 counter/models.py:1100 +#: counter/models.py:368 counter/models.py:1096 msgid "product" msgstr "produit" @@ -3773,44 +3773,44 @@ msgstr "vendeurs" msgid "token" msgstr "jeton" -#: counter/models.py:705 +#: counter/models.py:701 msgid "bank" msgstr "banque" -#: counter/models.py:707 counter/models.py:808 +#: counter/models.py:703 counter/models.py:804 msgid "is validated" msgstr "est validé" -#: counter/models.py:712 +#: counter/models.py:708 msgid "refilling" msgstr "rechargement" -#: counter/models.py:785 eboutic/models.py:249 +#: counter/models.py:781 eboutic/models.py:249 msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:786 counter/models.py:1080 eboutic/models.py:250 +#: counter/models.py:782 counter/models.py:1076 eboutic/models.py:250 msgid "quantity" msgstr "quantité" -#: counter/models.py:805 +#: counter/models.py:801 msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:813 +#: counter/models.py:809 msgid "selling" msgstr "vente" -#: counter/models.py:917 +#: counter/models.py:913 msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:918 +#: counter/models.py:914 #, python-format msgid "Eticket bought for the event %(event)s" msgstr "Eticket acheté pour l'événement %(event)s" -#: counter/models.py:920 counter/models.py:933 +#: counter/models.py:916 counter/models.py:929 #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -3822,67 +3822,67 @@ msgstr "" "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "%(url)s." -#: counter/models.py:971 +#: counter/models.py:967 msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:974 +#: counter/models.py:970 msgid "permanency" msgstr "permanence" -#: counter/models.py:1007 +#: counter/models.py:1003 msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:1010 +#: counter/models.py:1006 msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:1076 +#: counter/models.py:1072 msgid "cash summary" msgstr "relevé" -#: counter/models.py:1079 +#: counter/models.py:1075 msgid "value" msgstr "valeur" -#: counter/models.py:1082 +#: counter/models.py:1078 msgid "check" msgstr "chèque" -#: counter/models.py:1084 +#: counter/models.py:1080 msgid "True if this is a bank check, else False" msgstr "Vrai si c'est un chèque, sinon Faux." -#: counter/models.py:1088 +#: counter/models.py:1084 msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:1104 +#: counter/models.py:1100 msgid "banner" msgstr "bannière" -#: counter/models.py:1106 +#: counter/models.py:1102 msgid "event date" msgstr "date de l'événement" -#: counter/models.py:1108 +#: counter/models.py:1104 msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:1110 +#: counter/models.py:1106 msgid "secret" msgstr "secret" -#: counter/models.py:1149 +#: counter/models.py:1145 msgid "uid" msgstr "uid" -#: counter/models.py:1154 counter/models.py:1159 +#: counter/models.py:1150 counter/models.py:1155 msgid "student card" msgstr "carte étudiante" -#: counter/models.py:1160 +#: counter/models.py:1156 msgid "student cards" msgstr "cartes étudiantes" @@ -3972,6 +3972,16 @@ msgstr "Terminer" msgid "Refilling" msgstr "Rechargement" +#: counter/templates/counter/counter_click.jinja:114 +msgid "" +"As a barman, you are not able to refill any account on your own. An admin " +"should be connected on this counter for that. The customer can refill by " +"using the eboutic." +msgstr "" +"En tant que barman, vous n'êtes pas en mesure de recharger un compte par vous même. " +"Un admin doit être connecté sur ce comptoir pour cela. Le client peut recharger son compte " +"en utilisant l'eboutic" + #: counter/templates/counter/counter_list.jinja:4 #: counter/templates/counter/counter_list.jinja:10 msgid "Counter admin list" @@ -4916,12 +4926,12 @@ msgid "Washing and drying" msgstr "Lavage et séchage" #: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:652 +#: sith/settings.py:650 msgid "Washing" msgstr "Lavage" #: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:652 +#: sith/settings.py:650 msgid "Drying" msgstr "Séchage" @@ -5436,11 +5446,11 @@ msgstr "Personne(s)" msgid "Identify users on pictures" msgstr "Identifiez les utilisateurs sur les photos" -#: sith/settings.py:253 sith/settings.py:471 +#: sith/settings.py:253 sith/settings.py:469 msgid "English" msgstr "Anglais" -#: sith/settings.py:253 sith/settings.py:470 +#: sith/settings.py:253 sith/settings.py:468 msgid "French" msgstr "Français" @@ -5464,7 +5474,7 @@ msgstr "INFO" msgid "GI" msgstr "GI" -#: sith/settings.py:401 sith/settings.py:481 +#: sith/settings.py:401 sith/settings.py:479 msgid "E" msgstr "E" @@ -5512,296 +5522,296 @@ msgstr "Sevenans" msgid "Montbéliard" msgstr "Montbéliard" -#: sith/settings.py:451 +#: sith/settings.py:449 msgid "Free" msgstr "Libre" -#: sith/settings.py:452 +#: sith/settings.py:450 msgid "CS" msgstr "CS" -#: sith/settings.py:453 +#: sith/settings.py:451 msgid "TM" msgstr "TM" -#: sith/settings.py:454 +#: sith/settings.py:452 msgid "OM" msgstr "OM" -#: sith/settings.py:455 +#: sith/settings.py:453 msgid "QC" msgstr "QC" -#: sith/settings.py:456 +#: sith/settings.py:454 msgid "EC" msgstr "EC" -#: sith/settings.py:457 +#: sith/settings.py:455 msgid "RN" msgstr "RN" -#: sith/settings.py:458 +#: sith/settings.py:456 msgid "ST" msgstr "ST" -#: sith/settings.py:459 +#: sith/settings.py:457 msgid "EXT" msgstr "EXT" -#: sith/settings.py:464 +#: sith/settings.py:462 msgid "Autumn" msgstr "Automne" -#: sith/settings.py:465 +#: sith/settings.py:463 msgid "Spring" msgstr "Printemps" -#: sith/settings.py:466 +#: sith/settings.py:464 msgid "Autumn and spring" msgstr "Automne et printemps" -#: sith/settings.py:472 +#: sith/settings.py:470 msgid "German" msgstr "Allemand" -#: sith/settings.py:473 +#: sith/settings.py:471 msgid "Spanish" msgstr "Espagnol" -#: sith/settings.py:477 +#: sith/settings.py:475 msgid "A" msgstr "A" -#: sith/settings.py:478 +#: sith/settings.py:476 msgid "B" msgstr "B" -#: sith/settings.py:479 +#: sith/settings.py:477 msgid "C" msgstr "C" -#: sith/settings.py:480 +#: sith/settings.py:478 msgid "D" msgstr "D" -#: sith/settings.py:482 +#: sith/settings.py:480 msgid "FX" msgstr "FX" -#: sith/settings.py:483 +#: sith/settings.py:481 msgid "F" msgstr "F" -#: sith/settings.py:484 +#: sith/settings.py:482 msgid "Abs" msgstr "Abs" -#: sith/settings.py:488 +#: sith/settings.py:486 msgid "Selling deletion" msgstr "Suppression de vente" -#: sith/settings.py:489 +#: sith/settings.py:487 msgid "Refilling deletion" msgstr "Suppression de rechargement" -#: sith/settings.py:533 +#: sith/settings.py:531 msgid "One semester" msgstr "Un semestre, 20 €" -#: sith/settings.py:534 +#: sith/settings.py:532 msgid "Two semesters" msgstr "Deux semestres, 35 €" -#: sith/settings.py:536 +#: sith/settings.py:534 msgid "Common core cursus" msgstr "Cursus tronc commun, 60 €" -#: sith/settings.py:540 +#: sith/settings.py:538 msgid "Branch cursus" msgstr "Cursus branche, 60 €" -#: sith/settings.py:541 +#: sith/settings.py:539 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:542 +#: sith/settings.py:540 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:543 +#: sith/settings.py:541 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:544 +#: sith/settings.py:542 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:545 +#: sith/settings.py:543 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:546 +#: sith/settings.py:544 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:547 +#: sith/settings.py:545 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 20 €" -#: sith/settings.py:549 +#: sith/settings.py:547 msgid "One semester Welcome Week" msgstr "Un semestre Welcome Week" -#: sith/settings.py:553 +#: sith/settings.py:551 msgid "One month for free" msgstr "Un mois gratuit" -#: sith/settings.py:554 +#: sith/settings.py:552 msgid "Two months for free" msgstr "Deux mois gratuits" -#: sith/settings.py:555 +#: sith/settings.py:553 msgid "Eurok's volunteer" msgstr "Bénévole Eurockéennes" -#: sith/settings.py:557 +#: sith/settings.py:555 msgid "Six weeks for free" msgstr "6 semaines gratuites" -#: sith/settings.py:561 +#: sith/settings.py:559 msgid "One day" msgstr "Un jour" -#: sith/settings.py:562 +#: sith/settings.py:560 msgid "GA staff member" msgstr "Membre staff GA (2 semaines), 1 €" -#: sith/settings.py:565 +#: sith/settings.py:563 msgid "One semester (-20%)" msgstr "Un semestre (-20%), 12 €" -#: sith/settings.py:570 +#: sith/settings.py:568 msgid "Two semesters (-20%)" msgstr "Deux semestres (-20%), 22 €" -#: sith/settings.py:575 +#: sith/settings.py:573 msgid "Common core cursus (-20%)" msgstr "Cursus tronc commun (-20%), 36 €" -#: sith/settings.py:580 +#: sith/settings.py:578 msgid "Branch cursus (-20%)" msgstr "Cursus branche (-20%), 36 €" -#: sith/settings.py:585 +#: sith/settings.py:583 msgid "Alternating cursus (-20%)" msgstr "Cursus alternant (-20%), 24 €" -#: sith/settings.py:591 +#: sith/settings.py:589 msgid "One year for free(CA offer)" msgstr "Une année offerte (Offre CA)" -#: sith/settings.py:611 +#: sith/settings.py:609 msgid "President" msgstr "Président⸱e" -#: sith/settings.py:612 +#: sith/settings.py:610 msgid "Vice-President" msgstr "Vice-Président⸱e" -#: sith/settings.py:613 +#: sith/settings.py:611 msgid "Treasurer" msgstr "Trésorier⸱e" -#: sith/settings.py:614 +#: sith/settings.py:612 msgid "Communication supervisor" msgstr "Responsable communication" -#: sith/settings.py:615 +#: sith/settings.py:613 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:616 +#: sith/settings.py:614 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:617 +#: sith/settings.py:615 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:618 +#: sith/settings.py:616 msgid "Active member" msgstr "Membre actif⸱ve" -#: sith/settings.py:619 +#: sith/settings.py:617 msgid "Curious" msgstr "Curieux⸱euse" -#: sith/settings.py:656 +#: sith/settings.py:654 msgid "A new poster needs to be moderated" msgstr "Une nouvelle affiche a besoin d'être modérée" -#: sith/settings.py:657 +#: sith/settings.py:655 msgid "A new mailing list needs to be moderated" msgstr "Une nouvelle mailing list a besoin d'être modérée" -#: sith/settings.py:660 +#: sith/settings.py:658 msgid "A new pedagogy comment has been signaled for moderation" msgstr "" "Un nouveau commentaire de la pédagogie a été signalé pour la modération" -#: sith/settings.py:662 +#: sith/settings.py:660 #, python-format msgid "There are %s fresh news to be moderated" msgstr "Il y a %s nouvelles toutes fraîches à modérer" -#: sith/settings.py:663 +#: sith/settings.py:661 msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:664 +#: sith/settings.py:662 #, python-format msgid "There are %s pictures to be moderated in the SAS" msgstr "Il y a %s photos à modérer dans le SAS" -#: sith/settings.py:665 +#: sith/settings.py:663 msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:666 +#: sith/settings.py:664 #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s€" -#: sith/settings.py:667 +#: sith/settings.py:665 #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:668 +#: sith/settings.py:666 msgid "You have a notification" msgstr "Vous avez une notification" -#: sith/settings.py:680 +#: sith/settings.py:678 msgid "Success!" msgstr "Succès !" -#: sith/settings.py:681 +#: sith/settings.py:679 msgid "Fail!" msgstr "Échec !" -#: sith/settings.py:682 +#: sith/settings.py:680 msgid "You successfully posted an article in the Weekmail" msgstr "Article posté avec succès dans le Weekmail" -#: sith/settings.py:683 +#: sith/settings.py:681 msgid "You successfully edited an article in the Weekmail" msgstr "Article édité avec succès dans le Weekmail" -#: sith/settings.py:684 +#: sith/settings.py:682 msgid "You successfully sent the Weekmail" msgstr "Weekmail envoyé avec succès" -#: sith/settings.py:692 +#: sith/settings.py:690 msgid "AE tee-shirt" msgstr "Tee-shirt AE" diff --git a/sith/settings.py b/sith/settings.py index 56c6c0bc..054787e7 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -432,8 +432,6 @@ SITH_SUBSCRIPTION_LOCATIONS = [ SITH_COUNTER_BARS = [(1, "MDE"), (2, "Foyer"), (35, "La Gommette")] -SITH_COUNTER_OFFICES = {2: "PdF", 1: "AE"} - SITH_COUNTER_BANK = [ ("OTHER", "Autre"), ("SOCIETE-GENERALE", "Société générale"), From ad44fd52a4e8d146baa57ff5e0bb97feb287f64f Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 17 Dec 2024 10:54:41 +0100 Subject: [PATCH 53/53] Apply review comments --- counter/templates/counter/counter_click.jinja | 6 +++++- locale/fr/LC_MESSAGES/django.po | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 96a72435..6de57147 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -111,7 +111,11 @@ {% else %}

    - {% trans trimmed %}As a barman, you are not able to refill any account on your own. An admin should be connected on this counter for that. The customer can refill by using the eboutic.{% endtrans %} + {% trans trimmed %} + As a barman, you are not able to refill any account on your own. + An admin should be connected on this counter for that. + The customer can refill by using the eboutic. + {% endtrans %}

    {% endif %} diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 6fb47e5b..1f74ddaa 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-17 02:39+0100\n" +"POT-Creation-Date: 2024-12-17 10:53+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -3301,7 +3301,7 @@ msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" #: core/templates/core/user_preferences.jinja:39 -#: counter/templates/counter/counter_click.jinja:119 +#: counter/templates/counter/counter_click.jinja:123 msgid "Student card" msgstr "Carte étudiante" @@ -3978,9 +3978,9 @@ msgid "" "should be connected on this counter for that. The customer can refill by " "using the eboutic." msgstr "" -"En tant que barman, vous n'êtes pas en mesure de recharger un compte par vous même. " -"Un admin doit être connecté sur ce comptoir pour cela. Le client peut recharger son compte " -"en utilisant l'eboutic" +"En tant que barman, vous n'êtes pas en mesure de recharger un compte par " +"vous même. Un admin doit être connecté sur ce comptoir pour cela. Le client " +"peut recharger son compte en utilisant l'eboutic" #: counter/templates/counter/counter_list.jinja:4 #: counter/templates/counter/counter_list.jinja:10