From 2083142d47cb140846dace77fa672b3c12cc9678 Mon Sep 17 00:00:00 2001 From: Laetitia Getti Date: Tue, 8 Aug 2023 13:29:53 +0200 Subject: [PATCH] generate a 6 digit long unique code for each grouped order --- .../migrations/0025_groupedorder_code.py | 45 +++++++++++++++++++ la_chariotte/order/models.py | 20 +++++++++ la_chariotte/order/tests/test_models.py | 15 ++++++- .../test_views/test_views_grouped_order.py | 1 + la_chariotte/order/tests/utils.py | 4 +- la_chariotte/order/views/grouped_order.py | 6 +++ requirements.txt | 2 + 7 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 la_chariotte/order/migrations/0025_groupedorder_code.py diff --git a/la_chariotte/order/migrations/0025_groupedorder_code.py b/la_chariotte/order/migrations/0025_groupedorder_code.py new file mode 100644 index 0000000..d8f72ca --- /dev/null +++ b/la_chariotte/order/migrations/0025_groupedorder_code.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.1 on 2023-08-08 09:20 + +from django.db import migrations, models +import base36 +import random + + +def create_code_from_pk(pk): + """When a grouped order is created, we compute a unique code that will be used in url path + How we generate this code : + 1. The instance pk, written in base36 (max 5 digits for now - we assume that there will not be more than 60466175 grouped orders) + 2. A random int written in base36 (5 digits long) + 3. Only the 6 first digits of this string + The use of pk in the beginning of the string guarantees the uniqueness, and the random part makes that we cannot guess the url path. + """ + base_36_pk = base36.dumps(pk) + random_string = base36.dumps(random.randint(1727605,60466175)) # generates a 5 digits long string + return f"{base_36_pk}{random_string}" + +def set_code_default(apps, schema_editor): + """Provides a default code to existing grouped orders during migration""" + GroupedOrder = apps.get_model("order","GroupedOrder") + for grouped_order in GroupedOrder.objects.all().iterator(): + grouped_order.code = create_code_from_pk(grouped_order.pk) + grouped_order.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("order", "0024_alter_item_options"), + ] + + operations = [ + migrations.AddField( + model_name="groupedorder", + name="code", + field=models.CharField(auto_created=True, null=True), + ), + migrations.RunPython(set_code_default), + migrations.AlterField( + model_name="groupedorder", + name="code", + field=models.CharField(auto_created=True), + ), + ] diff --git a/la_chariotte/order/models.py b/la_chariotte/order/models.py index 7434e7c..a368885 100644 --- a/la_chariotte/order/models.py +++ b/la_chariotte/order/models.py @@ -1,3 +1,6 @@ +import random + +import base36 from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse @@ -5,6 +8,8 @@ from django.utils import timezone from la_chariotte.settings import AUTH_USER_MODEL +CODE_LENGTH = 6 + class GroupedOrder(models.Model): name = models.CharField( @@ -20,6 +25,21 @@ class GroupedOrder(models.Model): ) description = models.TextField("Description", null=True, blank=True) total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) + code = models.CharField(auto_created=True) + + def create_code_from_pk(self): + """When a grouped order is created, we compute a unique code that will be used in url path + How we generate this code : + 1. The instance pk, written in base36 (max 5 digits for now - we assume that there will not be more than 60466175 grouped orders) + 2. A random int written in base36 (5 digits long) + 3. Only the 6 first digits of this string + The use of pk in the beginning of the string guarantees the uniqueness, and the random part makes that we cannot guess the url path. + """ + base_36_pk = base36.dumps(self.pk) + random_string = base36.dumps( + random.randint(pow(36, CODE_LENGTH - 2), pow(36, CODE_LENGTH - 1) - 1) + ) # generates a 5 digits long string + self.code = f"{base_36_pk}{random_string}"[:CODE_LENGTH] def compute_total_price(self): price = 0 diff --git a/la_chariotte/order/tests/test_models.py b/la_chariotte/order/tests/test_models.py index 602fe35..d1dfe2e 100644 --- a/la_chariotte/order/tests/test_models.py +++ b/la_chariotte/order/tests/test_models.py @@ -1,10 +1,14 @@ import datetime +import pytest from django.contrib import auth from django.urls import reverse from django.utils import timezone -from la_chariotte.order.models import GroupedOrder, Item +from la_chariotte.order.models import CODE_LENGTH, GroupedOrder, Item +from la_chariotte.order.tests.utils import create_grouped_order + +pytestmark = pytest.mark.django_db class TestGroupedOrderModel: @@ -124,6 +128,15 @@ class TestGroupedOrderModel: "La date de livraison ne doit pas être dans le passé" in response.content.decode() assert GroupedOrder.objects.count() == 0 + def test_create_unique_code_from_pk(self, client_log): + grouped_order = create_grouped_order( + days_before_delivery_date=5, + days_before_deadline=2, + name="future", + orga_user=auth.get_user(client_log), + ) + assert len(grouped_order.code) == CODE_LENGTH + class TestItemModel: """Tests for Item model""" diff --git a/la_chariotte/order/tests/test_views/test_views_grouped_order.py b/la_chariotte/order/tests/test_views/test_views_grouped_order.py index 9344d3c..11bebc6 100644 --- a/la_chariotte/order/tests/test_views/test_views_grouped_order.py +++ b/la_chariotte/order/tests/test_views/test_views_grouped_order.py @@ -791,6 +791,7 @@ class TestGroupedOrderCreateView: assert response.status_code == 302 assert response.url.endswith("gerer-produits") assert models.GroupedOrder.objects.count() == 1 + assert models.GroupedOrder.objects.first().code != "" def test_create_grouped_order__anonymous_user(self, client): create_gorder_url = reverse("order:create_grouped_order") diff --git a/la_chariotte/order/tests/utils.py b/la_chariotte/order/tests/utils.py index 51cbf2e..f2747ce 100644 --- a/la_chariotte/order/tests/utils.py +++ b/la_chariotte/order/tests/utils.py @@ -18,9 +18,11 @@ def create_grouped_order( """ date = timezone.now().date() + datetime.timedelta(days=days_before_delivery_date) deadline = timezone.now() + datetime.timedelta(days=days_before_deadline) - return models.GroupedOrder.objects.create( + grouped_order = models.GroupedOrder.objects.create( name=name, orga=orga_user, delivery_date=date, deadline=deadline ) + grouped_order.create_code_from_pk() + return grouped_order def order_items_in_grouped_order(grouped_order): diff --git a/la_chariotte/order/views/grouped_order.py b/la_chariotte/order/views/grouped_order.py index 8104fff..ffd169b 100644 --- a/la_chariotte/order/views/grouped_order.py +++ b/la_chariotte/order/views/grouped_order.py @@ -122,6 +122,12 @@ class GroupedOrderCreateView(LoginRequiredMixin, generic.CreateView): kwargs["user"] = self.request.user return kwargs + def form_valid(self, form): + """If the form is valid, generate a unique code and save""" + self.object = form.save() + self.object.create_code_from_pk() + return super().form_valid(form) + class GroupedOrderUpdateView(UserPassesTestMixin, generic.UpdateView): """View for updating a grouped order""" diff --git a/requirements.txt b/requirements.txt index 5aac60a..53cc4c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,8 @@ asn1crypto==1.5.1 # oscrypto # pyhanko # pyhanko-certvalidator +base36==0.1.1 + # à la main certifi==2023.5.7 # via # requests