diff --git a/la_chariotte/order/models.py b/la_chariotte/order/models.py index 31748a3..70bc93b 100644 --- a/la_chariotte/order/models.py +++ b/la_chariotte/order/models.py @@ -31,6 +31,11 @@ class GroupedOrder(models.Model): self.total_price = price self.save() + def compute_items_ordered_nb(self): + """Updates the ordered_nb of all items fo the grouped_order""" + for item in self.item_set.all(): + item.compute_ordered_nb() + def is_ongoing(self): """Returns True if the grouped order is open for new Orders - False if it's too late""" return self.deadline >= timezone.now() @@ -130,6 +135,13 @@ class Item(models.Model): """Returns the price of all orders on this item""" return self.price * self.ordered_nb + def get_remaining_nb(self): + """Returns the number of remaining articles for this item in this grouped order""" + if self.max_limit: + return self.max_limit - self.ordered_nb + else: + return None + def get_absolute_url(self): return reverse("order:manage_items", kwargs={"pk": self.grouped_order.pk}) diff --git a/la_chariotte/order/templates/order/grouped_order_add_items.html b/la_chariotte/order/templates/order/grouped_order_add_items.html index 41739cd..654e8c4 100644 --- a/la_chariotte/order/templates/order/grouped_order_add_items.html +++ b/la_chariotte/order/templates/order/grouped_order_add_items.html @@ -26,7 +26,7 @@
{% csrf_token %} - + diff --git a/la_chariotte/order/templates/order/grouped_order_detail.html b/la_chariotte/order/templates/order/grouped_order_detail.html index 788e6f4..f2366d2 100644 --- a/la_chariotte/order/templates/order/grouped_order_detail.html +++ b/la_chariotte/order/templates/order/grouped_order_detail.html @@ -64,7 +64,8 @@ {% csrf_token %} {{ item.name }} {{ item.price }} € - + + {% if item.get_remaining_nb %} {{ item.get_remaining_nb }} disponibles{% endif %} {% endfor %} diff --git a/la_chariotte/order/tests/test_views.py b/la_chariotte/order/tests/test_views.py index 869bae0..a753116 100644 --- a/la_chariotte/order/tests/test_views.py +++ b/la_chariotte/order/tests/test_views.py @@ -232,7 +232,7 @@ class TestGroupedOrderDetailView: orga_user=other_user, ) item = models.Item.objects.create( - name="test item 1", grouped_order=grouped_order, price=1 + name="test item 1", grouped_order=grouped_order, price=1, max_limit=20 ) item2 = models.Item.objects.create( name="test item 2", grouped_order=grouped_order, price=5 @@ -247,6 +247,7 @@ class TestGroupedOrderDetailView: assert response.status_code == 200 assert "test item" in response.content.decode() assert "gr order test" in response.content.decode() + assert "20 disponibles" in response.content.decode() assert item.ordered_nb == 0 assert item2.ordered_nb == 0 order_url = reverse( @@ -286,11 +287,13 @@ class TestGroupedOrderDetailView: assert order.ordered_items.count() == 2 assert order.articles_nb == 5 assert order.price == 9 + assert item.get_remaining_nb() == 16 def test_order_item__no_articles_ordered(self, client, other_user): """ From the OrderDetailView, we order without having changed any item quantity. An error is raised. + The order is deleted """ grouped_order = create_grouped_order( days_before_delivery_date=5, @@ -328,13 +331,13 @@ class TestGroupedOrderDetailView: "email": "test@mail.fr", }, ) - order = models.Order.objects.first() - order.articles_nb == 0 assert response.status_code == 200 assert ( response.context["error_message"] == "Veuillez commander au moins un produit" ) + assert not models.Order.objects.first() + assert not models.OrderAuthor.objects.first() def test_deadline_passed(self, client, other_user): """ @@ -577,6 +580,122 @@ class TestOrder: ) assert response.status_code == 403 + def test_order_too_many_items_ordered(self, client, other_user): + """If a user orderd more articles than what is available, + the form is displayed again with an error. + The OrderedItems, OrderAuthor and Order are deleted.""" + grouped_order = create_grouped_order( + days_before_delivery_date=5, + days_before_deadline=2, + name="gr order test", + orga_user=other_user, + ) + item = models.Item.objects.create( + name="test item 1", grouped_order=grouped_order, price=1 + ) + item2 = models.Item.objects.create( + name="test item 2", grouped_order=grouped_order, price=5, max_limit=20 + ) + detail_url = reverse( + "order:grouped_order_detail", + kwargs={ + "pk": grouped_order.pk, + }, + ) + response = client.get(detail_url) + assert response.status_code == 200 + assert "test item" in response.content.decode() + assert "gr order test" in response.content.decode() + assert item.ordered_nb == 0 + assert item2.ordered_nb == 0 + order_url = reverse( + "order:order", + kwargs={ + "grouped_order_id": grouped_order.pk, + }, + ) + response = client.post( + order_url, + { + f"quantity_{item.pk}": 4, + f"quantity_{item2.pk}": 25, + "first_name": "Prénom", + "last_name": "Nom", + "phone": "0645632569", + "email": "test@mail.fr", + }, + ) + assert response.status_code == 200 + assert ( + response.context["error_message"] + == "Trop de test item 2 commandés pour la quantité disponible" + ) + item.refresh_from_db() + item2.refresh_from_db() + assert item.ordered_nb == 0 + assert item2.ordered_nb == 0 + assert not models.OrderAuthor.objects.first() + assert not models.Order.objects.first() + assert not models.OrderedItem.objects.first() + + def test_negative_nb_ordered(self, client, other_user): + """If a user orders a negative nb of articles for an item, + the form is displayed again with an error. + The OrderedItems, OrderAuthor and Order are deleted.""" + grouped_order = create_grouped_order( + days_before_delivery_date=5, + days_before_deadline=2, + name="gr order test", + orga_user=other_user, + ) + item = models.Item.objects.create( + name="test item 1", grouped_order=grouped_order, price=1 + ) + item2 = models.Item.objects.create( + name="test item 2", grouped_order=grouped_order, price=5, max_limit=20 + ) + detail_url = reverse( + "order:grouped_order_detail", + kwargs={ + "pk": grouped_order.pk, + }, + ) + response = client.get(detail_url) + assert response.status_code == 200 + assert "test item" in response.content.decode() + assert "gr order test" in response.content.decode() + assert item.ordered_nb == 0 + assert item2.ordered_nb == 0 + order_url = reverse( + "order:order", + kwargs={ + "grouped_order_id": grouped_order.pk, + }, + ) + response = client.post( + order_url, + { + f"quantity_{item.pk}": 4, + f"quantity_{item2.pk}": -2, + "first_name": "Prénom", + "last_name": "Nom", + "phone": "0645632569", + "email": "test@mail.fr", + }, + ) + assert response.status_code == 200 + assert ( + response.context["error_message"] + == "Veuillez commander un nombre positif de test item 2" + ) + item.refresh_from_db() + item2.refresh_from_db() + assert item.ordered_nb == 0 + assert item2.ordered_nb == 0 + assert not models.OrderAuthor.objects.first() + assert not models.Order.objects.first() + assert not models.OrderedItem.objects.first() + class TestGroupedOrderCreateView: def test_create_grouped_order(self, client_log): diff --git a/la_chariotte/order/views.py b/la_chariotte/order/views.py index 59de77f..f65881d 100644 --- a/la_chariotte/order/views.py +++ b/la_chariotte/order/views.py @@ -2,6 +2,7 @@ from io import BytesIO from django import http from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.core.exceptions import ValidationError from django.shortcuts import get_object_or_404, render from django.template.loader import get_template from django.urls import reverse, reverse_lazy @@ -62,6 +63,14 @@ class GroupedOrderDetailView(generic.DetailView): template_name = "order/grouped_order_detail.html" context_object_name = "grouped_order" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + qty_dict = {} + for item in self.get_object().item_set.all(): + qty_dict[item.id] = item.get_remaining_nb() + context["remaining_qty"] = qty_dict + return context + class GroupedOrderOverview(UserPassesTestMixin, generic.DetailView): """Overview of a grouped order, for the organizer""" @@ -144,12 +153,17 @@ class ItemCreateView(UserPassesTestMixin, generic.CreateView): def order(request, grouped_order_id): """Creates an AnonymousUser, and an Order for this GroupedOrder, with related OrderedItems""" grouped_order = get_object_or_404(GroupedOrder, pk=grouped_order_id) + + # check if the grouped order is ongoing if not grouped_order.is_ongoing(): return http.HttpResponseForbidden() + # get a dict with (quantity_{{item_id}}:{{quantity}}) orders_dict = { key: value for key, value in request.POST.items() if key.startswith("quantity") } + + # create an order first_name = request.POST["first_name"] last_name = request.POST["last_name"] phone = request.POST["phone"] @@ -158,33 +172,76 @@ def order(request, grouped_order_id): first_name=first_name, last_name=last_name, email=email, phone=phone ) order = Order.objects.create(author=author, grouped_order=grouped_order) + + # add items to the order + error_message = None for key, quantity in orders_dict.items(): - if quantity == "": - quantity = 0 - if int(quantity) > 0: - item = grouped_order.item_set.get(pk=key.split("_")[1]) - OrderedItem.objects.create(nb=quantity, order=order, item=item) - item.compute_ordered_nb() - order.compute_order_articles_nb() - if order.articles_nb == 0: - # Redisplay the order form for this grouped order. + quantity = int(quantity) + item = grouped_order.item_set.get(pk=key.split("_")[1]) + # check if too many items are ordered + error_message = validate_item_ordered_nb(item, quantity) + if error_message: + break # stop creating items if there is an error + OrderedItem.objects.create(nb=quantity, order=order, item=item) + + # Redisplay the form with error messages if there is an error + if error_message: + order.delete() + author.delete() + grouped_order.compute_items_ordered_nb() return render( request, "order/grouped_order_detail.html", { "grouped_order": grouped_order, - "error_message": "Veuillez commander au moins un produit", + "error_message": error_message, }, ) - else: - order.compute_order_price() - # Always return an http.HttpResponseRedirect after successfully dealing - # with POST data. This prevents data from being posted twice if a - # user hits the Back button. - return http.HttpResponseRedirect( - reverse("order:order_confirm", args=(grouped_order.pk, order.pk)) + + # check if the order contains articles + error_message = validate_articles_ordered_nb(order) + + # Redisplay the form with error messages if there is an error + if error_message: + order.delete() + author.delete() + return render( + request, + "order/grouped_order_detail.html", + { + "grouped_order": grouped_order, + "error_message": error_message, + }, ) + # Redirect to confirmation page + order.compute_order_price() + grouped_order.compute_items_ordered_nb() + # Always return an http.HttpResponseRedirect after successfully dealing + # with POST data. This prevents data from being posted twice if a + # user hits the Back button. + return http.HttpResponseRedirect( + reverse("order:order_confirm", args=(grouped_order.pk, order.pk)) + ) + + +def validate_item_ordered_nb(item, ordered_nb): + """Returns an error message if the ordered items are not available + or if the ordered nb is negative""" + if ordered_nb < 0: + return f"Veuillez commander un nombre positif de {item.name}" + if item.get_remaining_nb() and item.get_remaining_nb() - ordered_nb < 0: + return f"Trop de {item.name} commandés pour la quantité disponible" + return None + + +def validate_articles_ordered_nb(order): + """Return an error if no items are ordered""" + order.compute_order_articles_nb() + if order.articles_nb == 0: + return "Veuillez commander au moins un produit" + return None + class OrderDetailView(generic.DetailView): """Confirmation page after a user orders"""