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 @@ + + + + + Commandes groupées {{ grouped_order.id }} + + +

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 : + + +
+ {% csrf_token %} +
+

{{ grouped_order }}

+ {% if error_message %}

{{ error_message }}

{% endif %} + {% for item in grouped_order.item_set.all %} + +
+ {% endfor %} +
+ +
+ + diff --git a/la_chariotte/order/templates/order/grouped_order_orga.html b/la_chariotte/order/templates/order/grouped_order_orga.html new file mode 100644 index 0000000..0cee30b --- /dev/null +++ b/la_chariotte/order/templates/order/grouped_order_orga.html @@ -0,0 +1,34 @@ + + + + + Commandes groupées {{ grouped_order.id }} + + +

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 : + + + les commandes passées pour cette commande groupée : + + + Retour à la page de commande + + diff --git a/la_chariotte/order/templates/order/index.html b/la_chariotte/order/templates/order/index.html new file mode 100644 index 0000000..26060cd --- /dev/null +++ b/la_chariotte/order/templates/order/index.html @@ -0,0 +1,49 @@ + + + + + Commandes groupées - index + + +

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 :

+ + {% endif %} + {% if grouped_order_list.crossed_deadline_grouped_orders %} +

Livraison à venir, date limite de commande dépassée :

+ + {% endif %} + {% if grouped_order_list.old_grouped_orders %} +

Livraison passée :

+ + {% endif %} + {% else %} +

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( + "/", views.GroupedOrderDetailView.as_view(), name="grouped_order_detail" + ), + path( + "/orga", views.GroupedOrderOrgaView.as_view(), name="grouped_order_orga" + ), + path("/commander/", views.order, name="order"), +] diff --git a/la_chariotte/order/views.py b/la_chariotte/order/views.py new file mode 100644 index 0000000..8c0e6e1 --- /dev/null +++ b/la_chariotte/order/views.py @@ -0,0 +1,99 @@ +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.utils import timezone +from django.views import generic + +from .models import GroupedOrder, Item, Order, OrderedItem + + +class IndexView(generic.ListView): + """Vue de toutes les commandes groupées existantes - plus tard, de toutes les commandes groupées de l'utilisateur connecté""" + + template_name = "order/index.html" + context_object_name = "grouped_order_list" + + def get_queryset(self): + """3 grouped_order status : incoming, crossed_deadline, and old""" + now = timezone.now() + today = now.date() + + """Return the 5 most recent old grouped orders""" + old_grouped_orders = GroupedOrder.objects.filter( + # is_to_be_delivered=False + delivery_date__lt=today + ).order_by("-delivery_date")[ + :5 + ] # delivery date < today (delivered) + + """Return all grouped orders, that have crossed their ordering deadline but the delivery date is still to come""" + crossed_dealine_grouped_orders = ( + GroupedOrder.objects.filter(delivery_date__gte=today) + .filter(deadline__lt=now) + .order_by("-delivery_date") + ) # delivery date >= today (not delivered) and deadline < today (we cannot order) + + """Return all incoming grouped orders""" + incoming_grouped_orders = GroupedOrder.objects.filter( + deadline__gte=now + ).order_by( + "deadline" + ) # dealine >= today (we can still order) + return { + "old_grouped_orders": old_grouped_orders, + "crossed_deadline_grouped_orders": crossed_dealine_grouped_orders, + "incoming_grouped_orders": incoming_grouped_orders, + } + + +class GroupedOrderDetailView(generic.DetailView): + """Vue de détail d'une commande groupée - possibilité de commander si elle est en cours""" + + model = GroupedOrder + template_name = "order/grouped_order_detail.html" + context_object_name = "grouped_order" + + +class GroupedOrderOrgaView(generic.DetailView): + """Vue de supervision d'une commande groupée""" + + model = GroupedOrder + template_name = "order/grouped_order_orga.html" + + +def order( + request, grouped_order_id +): # crée une commande (order) pour cette commande groupée, avec l'item selectionné dedans + grouped_order = get_object_or_404(GroupedOrder, pk=grouped_order_id) + try: + selected_item = grouped_order.item_set.get(pk=request.POST["item"]) + except (KeyError, Item.DoesNotExist): + # Redisplay the order form for this grouped order. + return render( + request, + "order/grouped_order_detail.html", + { + "grouped_order": grouped_order, + "error_message": "You didn't select an item.", + }, + ) + else: + order = Order.objects.create(author="Auteur teur", grouped_order=grouped_order) + OrderedItem.objects.create(nb=1, order=order, item=selected_item) + compute_ordered_nb(selected_item) + selected_item.save() + # Always return an HttpResponseRedirect after successfully dealing + # with POST data. This prevents data from being posted twice if a + # user hits the Back button. + return HttpResponseRedirect( + reverse("order:grouped_order_orga", args=(grouped_order.pk,)) + ) + + +def compute_ordered_nb(item): + """Calcule le nombre de produits de ce type commandés (pour cette commande groupée)""" + ordered_nb = 0 + for order in item.orders.all(): + ordered_nb += order.nb + item.ordered_nb = ordered_nb + item.save() diff --git a/la_chariotte/settings.py b/la_chariotte/settings.py index 68ea23d..159b46b 100644 --- a/la_chariotte/settings.py +++ b/la_chariotte/settings.py @@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """ - +import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -20,7 +20,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-f66vu+dj79c(5u(w2i0indkrlf$qtt!b$dmotnm%5!0a*9+=my' +SECRET_KEY = "django-insecure-f66vu+dj79c(5u(w2i0indkrlf$qtt!b$dmotnm%5!0a*9+=my" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,71 +31,80 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "la_chariotte.order", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'la_chariotte.urls' +ROOT_URLCONF = "la_chariotte.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'la_chariotte.wsgi.application' +WSGI_APPLICATION = "la_chariotte.wsgi.application" # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("DB_NAME", "chariotte-db"), } } +if os.getenv("DB_USER"): + DATABASES["default"]["USER"] = os.getenv("DB_USER") +if os.getenv("DB_PASSWORD"): + DATABASES["default"]["PASSWORD"] = os.getenv("DB_PASSWORD") +if os.getenv("DB_HOST"): + DATABASES["default"]["HOST"] = os.getenv("DB_HOST") +if os.getenv("DB_PORT"): + DATABASES["default"]["PORT"] = os.getenv("DB_PORT") # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -103,9 +112,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/4.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "Europe/Paris" USE_I18N = True @@ -115,9 +124,9 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = "static/" # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/la_chariotte/urls.py b/la_chariotte/urls.py index 1da3016..cdef8bf 100644 --- a/la_chariotte/urls.py +++ b/la_chariotte/urls.py @@ -14,8 +14,9 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import include, path urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), + path("commande/", include("la_chariotte.order.urls")), ] diff --git a/la_chariotte/wsgi.py b/la_chariotte/wsgi.py index e4e5d8a..0e3a6e0 100644 --- a/la_chariotte/wsgi.py +++ b/la_chariotte/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'la_chariotte.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "la_chariotte.settings") application = get_wsgi_application() diff --git a/manage.py b/manage.py index 0f7ee05..23c217b 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'la_chariotte.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "la_chariotte.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml index d9b0533..68763da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,4 +3,46 @@ name = "la chariotte" version = "0.0.1" description = "Web application for organising grouped orders" readme = "readMe.md" -license = {file = "LICENSE"} \ No newline at end of file +license = {file = "LICENSE"} +authors = [{name = "Hashbang", email = "support@hashbang.fr"}] +dependencies = [ + "django>=4,<5", + "psycopg2>=2,<3", +] + +[build-system] +requires = [ + "setuptools","wheel" +] +build-backend = "setuptools.build_meta" + +[project.optional-dependencies] +dev = [ + "pytest>=7,<8", + "pip-tools>=6,<7", + "pytest-isort>=3,<4", + "pytest-django>=4,<5", + "pytest-cov>=4,<5", + "diff-cover>=4,<5", + "pytest-black<1", +] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "la_chariotte.settings" +addopts = "--isort --black --reuse-db --cov-report xml --cov-report term-missing --cov=la_chariotte -p no:warnings" +isort_ignore = ["*migrations/*.py"] + +[tool.isort] +skip_glob = ["*migrations/*.py"] + +[tool.black] +line-length = 88 +exclude = ''' + +( + /( + | migrations + | static + )/ +) +''' diff --git a/readMe.md b/readMe.md index 0575610..b50408a 100644 --- a/readMe.md +++ b/readMe.md @@ -1,3 +1,35 @@ # La Chariotte -La Chariotte est une application web sous licence libre Affera GPL, développée et maintenue par [Hashbang](https://hashbang.fr/). \ No newline at end of file +## Présentation + +La Chariotte est une application web sous licence libre Affera GPL, développée et maintenue par [Hashbang](https://hashbang.fr/). + +## Développement + +Cloner le projet : +```bash +git clone https://gitlab.com/hashbangfr/la_chariotte.git +``` + +Installer les dépendances : +```bash +pip install -r requirements.txt +pip install -r dev-requirements.txt +``` + +## Lancer les tests + +Lancer les tests avec pytest : +```bash +pytest +``` + +Si il y a des erreurs ISORT, on peut lancer isort pour trier les fichiers : +```bash +isort . +``` + +Si il y a des erreurs BLACK, on peut lancer black pour linter le code : +```bash +black . +```