diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..73d8e64 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +[run] +omit = + */migrations/* + */admin.py + */tests/* + */urls.py + */apps.py + la_chariotte/settings.py + la_chariotte/asgi.py + la_chariotte/wsgi.py diff --git a/.gitignore b/.gitignore index 5e25d49..5c78ec5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ - __pycache__/ coverage.xml .coverage -la_chariotte.egg-info/ \ No newline at end of file +la_chariotte.egg-info/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 425e5c1..c4732e2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,8 +36,8 @@ tests: - source $CACHE_PATH/venv/bin/activate - pip install -U -r requirements.txt - pip install -U -r dev-requirements.txt - # - pytest --create-db --cov --cov-report=xml - # - if [ "$CI_COMMIT_REF_NAME" = 'main' ] ; then exit 0 ; fi - # - if [ "$CI_COMMIT_REF_NAME" = 'develop' ] ; git fetch origin main ; then diff-cover coverage.xml --fail-under=90 && exit 0 ; fi - # - git fetch origin develop ; diff-cover coverage.xml --fail-under=90 --compare-branch origin/develop + - pytest --create-db --cov --cov-report=xml + - if [ "$CI_COMMIT_REF_NAME" = 'main' ] ; then exit 0 ; fi + - if [ "$CI_COMMIT_REF_NAME" = 'develop' ] ; git fetch origin main ; then diff-cover coverage.xml --fail-under=90 && exit 0 ; fi + - git fetch origin develop ; diff-cover coverage.xml --fail-under=90 --compare-branch origin/develop - echo "Tests done." diff --git a/la_chariotte/__pycache__/__init__.cpython-310.pyc b/la_chariotte/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index ece3371..0000000 Binary files a/la_chariotte/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/la_chariotte/asgi.py b/la_chariotte/asgi.py index cbce385..5baa79f 100644 --- a/la_chariotte/asgi.py +++ b/la_chariotte/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'la_chariotte.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "la_chariotte.settings") application = get_asgi_application() diff --git a/la_chariotte/order/__init__.py b/la_chariotte/order/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/la_chariotte/order/admin.py b/la_chariotte/order/admin.py new file mode 100644 index 0000000..5aab8b9 --- /dev/null +++ b/la_chariotte/order/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from .models import GroupedOrder, Item, Order, OrderedItem + +admin.site.register(GroupedOrder) +admin.site.register(Order) +admin.site.register(Item) +admin.site.register(OrderedItem) diff --git a/la_chariotte/order/apps.py b/la_chariotte/order/apps.py new file mode 100644 index 0000000..337993a --- /dev/null +++ b/la_chariotte/order/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OrderConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "la_chariotte.order" diff --git a/la_chariotte/order/migrations/0001_initial.py b/la_chariotte/order/migrations/0001_initial.py new file mode 100644 index 0000000..d72f72e --- /dev/null +++ b/la_chariotte/order/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.7 on 2023-03-14 15:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Grouped_order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('orga', models.CharField(max_length=100)), + ('date', models.DateField(verbose_name='Date de livraison')), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('author', models.CharField(max_length=100, verbose_name='Personne qui passe la commande')), + ('grouped_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='order.grouped_order')), + ], + ), + ] diff --git a/la_chariotte/order/migrations/0002_grouped_order_name.py b/la_chariotte/order/migrations/0002_grouped_order_name.py new file mode 100644 index 0000000..87ff618 --- /dev/null +++ b/la_chariotte/order/migrations/0002_grouped_order_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-03-14 16:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='grouped_order', + name='name', + field=models.CharField(max_length=100, null=True), + ), + ] diff --git a/la_chariotte/order/migrations/0003_item.py b/la_chariotte/order/migrations/0003_item.py new file mode 100644 index 0000000..3de7cad --- /dev/null +++ b/la_chariotte/order/migrations/0003_item.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.7 on 2023-03-15 15:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0002_grouped_order_name'), + ] + + operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('grouped_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='order.grouped_order')), + ], + ), + ] diff --git a/la_chariotte/order/migrations/0004_item_ordered_nb.py b/la_chariotte/order/migrations/0004_item_ordered_nb.py new file mode 100644 index 0000000..382f464 --- /dev/null +++ b/la_chariotte/order/migrations/0004_item_ordered_nb.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-03-15 16:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0003_item'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='ordered_nb', + field=models.IntegerField(default=0), + ), + ] diff --git a/la_chariotte/order/migrations/0005_remove_item_ordered_nb_alter_order_grouped_order_and_more.py b/la_chariotte/order/migrations/0005_remove_item_ordered_nb_alter_order_grouped_order_and_more.py new file mode 100644 index 0000000..70d001d --- /dev/null +++ b/la_chariotte/order/migrations/0005_remove_item_ordered_nb_alter_order_grouped_order_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.1.7 on 2023-03-17 14:25 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0004_item_ordered_nb'), + ] + + operations = [ + migrations.RemoveField( + model_name='item', + name='ordered_nb', + ), + migrations.AlterField( + model_name='order', + name='grouped_order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_set', to='order.grouped_order'), + ), + migrations.CreateModel( + name='OrderedItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nb', models.PositiveSmallIntegerField(default=0)), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ordered_items', to='order.item')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='order.order')), + ], + ), + ] diff --git a/la_chariotte/order/migrations/0006_item_ordered_nb.py b/la_chariotte/order/migrations/0006_item_ordered_nb.py new file mode 100644 index 0000000..6989454 --- /dev/null +++ b/la_chariotte/order/migrations/0006_item_ordered_nb.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-03-17 14:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0005_remove_item_ordered_nb_alter_order_grouped_order_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='ordered_nb', + field=models.IntegerField(default=0), + ), + ] diff --git a/la_chariotte/order/migrations/0007_alter_ordereditem_item_alter_ordereditem_order.py b/la_chariotte/order/migrations/0007_alter_ordereditem_item_alter_ordereditem_order.py new file mode 100644 index 0000000..4af4531 --- /dev/null +++ b/la_chariotte/order/migrations/0007_alter_ordereditem_item_alter_ordereditem_order.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.7 on 2023-03-17 15:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0006_item_ordered_nb'), + ] + + operations = [ + migrations.AlterField( + model_name='ordereditem', + name='item', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='order.item'), + ), + migrations.AlterField( + model_name='ordereditem', + name='order', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ordered_items', to='order.order'), + ), + ] diff --git a/la_chariotte/order/migrations/0008_grouped_order_deadline.py b/la_chariotte/order/migrations/0008_grouped_order_deadline.py new file mode 100644 index 0000000..ae453d0 --- /dev/null +++ b/la_chariotte/order/migrations/0008_grouped_order_deadline.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.7 on 2023-03-23 14:38 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0007_alter_ordereditem_item_alter_ordereditem_order'), + ] + + operations = [ + migrations.AddField( + model_name='grouped_order', + name='deadline', + field=models.DateTimeField(default=datetime.datetime(2023, 3, 23, 14, 38, 17, 365192, tzinfo=datetime.timezone.utc), verbose_name='Date limite de commande'), + preserve_default=False, + ), + ] diff --git a/la_chariotte/order/migrations/0009_rename_date_grouped_order_delivery_date.py b/la_chariotte/order/migrations/0009_rename_date_grouped_order_delivery_date.py new file mode 100644 index 0000000..1998119 --- /dev/null +++ b/la_chariotte/order/migrations/0009_rename_date_grouped_order_delivery_date.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-03-23 17:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0008_grouped_order_deadline'), + ] + + operations = [ + migrations.RenameField( + model_name='grouped_order', + old_name='date', + new_name='delivery_date', + ), + ] diff --git a/la_chariotte/order/migrations/0010_rename_grouped_order_groupedorder.py b/la_chariotte/order/migrations/0010_rename_grouped_order_groupedorder.py new file mode 100644 index 0000000..e068ba2 --- /dev/null +++ b/la_chariotte/order/migrations/0010_rename_grouped_order_groupedorder.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2 on 2023-04-11 14:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("order", "0009_rename_date_grouped_order_delivery_date"), + ] + + operations = [ + migrations.RenameModel( + old_name="Grouped_order", + new_name="GroupedOrder", + ), + ] diff --git a/la_chariotte/order/migrations/__init__.py b/la_chariotte/order/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/la_chariotte/order/models.py b/la_chariotte/order/models.py new file mode 100644 index 0000000..4c4881f --- /dev/null +++ b/la_chariotte/order/models.py @@ -0,0 +1,60 @@ +from django.db import models +from django.utils import timezone + + +class GroupedOrder(models.Model): + name = models.CharField(max_length=100, null=True) # optionnal + orga = models.CharField(max_length=100) # a changer, utiliser ForeignKey de user + delivery_date = models.DateField("Date de livraison") + deadline = models.DateTimeField("Date limite de commande") + + 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() + + def is_to_be_delivered(self): + """Returns True if the grouped order has not been delivered yet - False if it's old""" + return self.delivery_date >= timezone.now().date() + + def __str__(self): # pragma: no cover + return ( + self.name + if self.name + else f"Commande groupée {self.pk} du {self.date} organisée par {self.orga}" + ) + + +class Order(models.Model): + grouped_order = models.ForeignKey( + GroupedOrder, on_delete=models.CASCADE, related_name="order_set" + ) + author = models.CharField( + max_length=100, verbose_name="Personne qui passe la commande" + ) # a changer, utiliser ForeignKey de user + + def __str__(self): # pragma: no cover + return f"Commande de {self.author} pour la commande groupée {self.grouped_order.pk}" + + +class Item(models.Model): + name = models.CharField(max_length=100) + grouped_order = models.ForeignKey( + GroupedOrder, on_delete=models.CASCADE + ) # à transformer en manytomany quand il y aura un catalogue + ordered_nb = models.IntegerField(default=0) + + def __str__(self): # pragma: no cover + return f"{self.name} dans la commande groupée {self.grouped_order.pk}" + + +class OrderedItem(models.Model): + """Item in one specific Order, and its number""" + + nb = models.PositiveSmallIntegerField(default=0) # works up to 32767 + order = models.ForeignKey( + Order, on_delete=models.CASCADE, related_name="ordered_items" + ) + item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name="orders") + + def __str__(self): # pragma: no cover + return f"{self.nb} {self.item}, dans la commande {self.order.pk}" diff --git a/la_chariotte/order/templates/order/grouped_order_detail.html b/la_chariotte/order/templates/order/grouped_order_detail.html new file mode 100644 index 0000000..09613de --- /dev/null +++ b/la_chariotte/order/templates/order/grouped_order_detail.html @@ -0,0 +1,36 @@ + + +
+ +Détail de la commande groupée {{ grouped_order.id }}, avec la liste des produits associés
+{{ grouped_order }} +
Organisateur·ice : {{ grouped_order.orga }}
+Date de livraison : {{ grouped_order.delivery_date }}
+ + les produits disponibles pour cette commande groupée : + +Vue d'organisation de la commande groupée {{ grouped_order.id }}, avec les produits commandés et les commandes individuelles
+{{ grouped_order }} +
Organisateur·ice : {{ grouped_order.orga }}
+Date de livraison : {{ grouped_order.delivery_date }}
+ + les produits commandés pour cette commande groupée : +Index des commandes que l'utilisateur·ice connecté·e organise
+Pour l'instant, index de toutes les commandes groupées qui existent
+ + {% if grouped_order_list.incoming_grouped_orders or grouped_order_list.crossed_deadline_grouped_orders or grouped_order_list.old_grouped_orders %} + {% if grouped_order_list.incoming_grouped_orders %} +Commandes groupées à venir :
+Livraison à venir, date limite de commande dépassée :
+Livraison passée :
+Pas de commande groupée pour l'instant
+ {% endif %} + + diff --git a/la_chariotte/order/tests/test_models.py b/la_chariotte/order/tests/test_models.py new file mode 100644 index 0000000..d5fff17 --- /dev/null +++ b/la_chariotte/order/tests/test_models.py @@ -0,0 +1,50 @@ +import datetime + +from django.utils import timezone + +from la_chariotte.order.models import GroupedOrder + + +class TestGroupedOrdersModel: + """Tests for Grouped orders model""" + + def test_is_ongoing_with_ongoing_grouped_order(self): + """ + is_ongoing() returns True if the deadline is not crossed + """ + deadline = timezone.now() + datetime.timedelta(days=10) + ongoing_gr_order = GroupedOrder(deadline=deadline) + assert ongoing_gr_order.is_ongoing() + + def test_is_ongoing_with_old_grouped_order(self): + """ + is_ongoing() returns False if the deadline is crossed + """ + deadline = timezone.now() - datetime.timedelta(hours=1) + old_gr_order = GroupedOrder(deadline=deadline) + assert not old_gr_order.is_ongoing() + + def test_is_to_be_delivered_with_today_delivery(self): + """ + is_to_be_delivered() returns True if the delivery date is today + """ + del_date = timezone.now().date() + gr_order = GroupedOrder(delivery_date=del_date) + assert gr_order.is_to_be_delivered() + + def test_is_to_be_delivered_with_old_grouped_order(self): + """ + is_to_be_delivered() returns False if the delivery date is passed + """ + del_date = timezone.now().date() - datetime.timedelta(days=1) + old_gr_order = GroupedOrder(delivery_date=del_date) + assert not old_gr_order.is_to_be_delivered() + + def test_is_to_be_delivered_with_crossed_deadline_grouped_order(self): + """ + is_to_be_delivered() returns True even if the deadline is crossed + """ + deadline = timezone.now() - datetime.timedelta(hours=1) + del_date = timezone.now().date() + datetime.timedelta(days=1) + old_gr_order = GroupedOrder(deadline=deadline, delivery_date=del_date) + assert old_gr_order.is_to_be_delivered() diff --git a/la_chariotte/order/tests/test_views.py b/la_chariotte/order/tests/test_views.py new file mode 100644 index 0000000..9e241bc --- /dev/null +++ b/la_chariotte/order/tests/test_views.py @@ -0,0 +1,193 @@ +import datetime + +import pytest +from django.urls import reverse +from django.utils import timezone + +from la_chariotte.order.models import GroupedOrder, Item, Order + +pytestmark = pytest.mark.django_db + + +def create_grouped_order(days_before_delivery_date, days_before_deadline, name): + """ + Creates a grouped order. + """ + date = timezone.now().date() + datetime.timedelta(days=days_before_delivery_date) + deadline = timezone.now() + datetime.timedelta(days=days_before_deadline) + return GroupedOrder.objects.create( + name=name, orga="test orga", delivery_date=date, deadline=deadline + ) + + +class TestGroupedOrderIndexView: + def test_no_grouped_orders(self, client): + """ + If no grouped order exist, an appropriate message is displayed + """ + response = client.get(reverse("order:index")) + assert response.status_code == 200 + assert "Pas de commande groupée pour l'instant" in response.content.decode() + assert len(response.context["grouped_order_list"]["old_grouped_orders"]) == 0 + assert ( + len( + response.context["grouped_order_list"][ + "crossed_deadline_grouped_orders" + ] + ) + == 0 + ) + assert ( + len(response.context["grouped_order_list"]["incoming_grouped_orders"]) == 0 + ) + + def test_grouped_orders_in_right_section(self, client): + """ + According to their delivery date and deadline, grouped orders are placed in the correct section : several gr orders + """ + future_grouped_order = create_grouped_order( + days_before_delivery_date=5, days_before_deadline=2, name="future" + ) + crossed_deadline_gr_order = create_grouped_order( + days_before_delivery_date=2, + days_before_deadline=-1, + name="crossed deadline", + ) + old_gr_order = create_grouped_order( + days_before_delivery_date=-1, days_before_deadline=-3, name="old" + ) + response = client.get(reverse("order:index")) + assert response.status_code == 200 + assert "Pas de commande groupée pour l'instant" not in response.content.decode() + assert "Commandes groupées à venir" in response.content.decode() + assert "Livraison à venir" in response.content.decode() + assert "Livraison passée" in response.content.decode() + assert len(response.context["grouped_order_list"]["old_grouped_orders"]) == 1 + assert ( + len( + response.context["grouped_order_list"][ + "crossed_deadline_grouped_orders" + ] + ) + == 1 + ) + assert ( + len(response.context["grouped_order_list"]["incoming_grouped_orders"]) == 1 + ) + assert ( + response.context["grouped_order_list"]["old_grouped_orders"][0] + == old_gr_order + ) + assert ( + response.context["grouped_order_list"]["crossed_deadline_grouped_orders"][0] + == crossed_deadline_gr_order + ) + assert ( + response.context["grouped_order_list"]["incoming_grouped_orders"][0] + == future_grouped_order + ) + + def test_grouped_orders_in_right_section__with_only_old(self, client): + """ + According to their delivery date and deadline, grouped orders are placed in correct section : only old gr order + """ + old_gr_order = create_grouped_order( + days_before_delivery_date=-1, days_before_deadline=-3, name="passée" + ) + response = client.get(reverse("order:index")) + assert response.status_code == 200 + assert "Pas de commande groupée pour l'instant" not in response.content.decode() + assert "Commandes groupées à venir" not in response.content.decode() + assert "Livraison à venir" not in response.content.decode() + assert "Livraison passée" in response.content.decode() + assert len(response.context["grouped_order_list"]["old_grouped_orders"]) == 1 + assert ( + len( + response.context["grouped_order_list"][ + "crossed_deadline_grouped_orders" + ] + ) + == 0 + ) + assert ( + len(response.context["grouped_order_list"]["incoming_grouped_orders"]) == 0 + ) + assert ( + response.context["grouped_order_list"]["old_grouped_orders"][0] + == old_gr_order + ) + + def test_grouped_orders_in_right_section__with_only_future(self, client): + """ + According to their delivery date and deadline, grouped orders are placed in correct section : only incoming gr order + """ + future_grouped_order = create_grouped_order( + days_before_delivery_date=5, days_before_deadline=2, name="future" + ) + response = client.get(reverse("order:index")) + assert response.status_code == 200 + assert "Pas de commande groupée pour l'instant" not in response.content.decode() + assert "Commandes groupées à venir" in response.content.decode() + assert "Livraison à venir" not in response.content.decode() + assert "Livraison passée" not in response.content.decode() + assert len(response.context["grouped_order_list"]["old_grouped_orders"]) == 0 + assert ( + len( + response.context["grouped_order_list"][ + "crossed_deadline_grouped_orders" + ] + ) + == 0 + ) + assert ( + len(response.context["grouped_order_list"]["incoming_grouped_orders"]) == 1 + ) + assert ( + response.context["grouped_order_list"]["incoming_grouped_orders"][0] + == future_grouped_order + ) + + +class TestGroupedOrderDetailView: + def test_order_item(self, client): + """ + From the OrderDetailView, we order an item using the order form, and it creates an Order woth and Ordered_item inside + """ + grouped_order = create_grouped_order( + days_before_delivery_date=5, days_before_deadline=2, name="gr order test" + ) + item = Item.objects.create(name="test item", grouped_order=grouped_order) + 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 + order_url = reverse( + "order:order", + kwargs={ + "grouped_order_id": grouped_order.pk, + }, + ) + response = client.post( + order_url, + { + "item": item.pk, + }, + ) + assert response.status_code == 302 + assert response.url == reverse( + "order:grouped_order_orga", + kwargs={ + "pk": grouped_order.pk, + }, + ) + item.refresh_from_db() + assert item.ordered_nb == 1 + order = Order.objects.first() + assert order.ordered_items.count() == 1 diff --git a/la_chariotte/order/urls.py b/la_chariotte/order/urls.py new file mode 100644 index 0000000..a769aa3 --- /dev/null +++ b/la_chariotte/order/urls.py @@ -0,0 +1,15 @@ +from django.urls import path + +from . import views + +app_name = "order" +urlpatterns = [ + path("", views.IndexView.as_view(), name="index"), + path( + "