mirror of
https://framagit.org/la-chariotte/la-chariotte.git
synced 2025-05-01 19:32:26 +02:00
Merge branch 'commandes-basiques' into 'develop'
Possibilité de commander 1 produit See merge request hashbangfr/la_chariotte!1
This commit is contained in:
commit
245d7a17a2
33 changed files with 911 additions and 51 deletions
10
.coveragerc
Normal file
10
.coveragerc
Normal file
|
@ -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
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,4 @@
|
|||
|
||||
__pycache__/
|
||||
coverage.xml
|
||||
.coverage
|
||||
la_chariotte.egg-info/
|
||||
la_chariotte.egg-info/
|
||||
|
|
|
@ -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."
|
||||
|
|
Binary file not shown.
|
@ -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()
|
||||
|
|
0
la_chariotte/order/__init__.py
Normal file
0
la_chariotte/order/__init__.py
Normal file
8
la_chariotte/order/admin.py
Normal file
8
la_chariotte/order/admin.py
Normal file
|
@ -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)
|
6
la_chariotte/order/apps.py
Normal file
6
la_chariotte/order/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrderConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "la_chariotte.order"
|
31
la_chariotte/order/migrations/0001_initial.py
Normal file
31
la_chariotte/order/migrations/0001_initial.py
Normal file
|
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
18
la_chariotte/order/migrations/0002_grouped_order_name.py
Normal file
18
la_chariotte/order/migrations/0002_grouped_order_name.py
Normal file
|
@ -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),
|
||||
),
|
||||
]
|
22
la_chariotte/order/migrations/0003_item.py
Normal file
22
la_chariotte/order/migrations/0003_item.py
Normal file
|
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
18
la_chariotte/order/migrations/0004_item_ordered_nb.py
Normal file
18
la_chariotte/order/migrations/0004_item_ordered_nb.py
Normal file
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
18
la_chariotte/order/migrations/0006_item_ordered_nb.py
Normal file
18
la_chariotte/order/migrations/0006_item_ordered_nb.py
Normal file
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
20
la_chariotte/order/migrations/0008_grouped_order_deadline.py
Normal file
20
la_chariotte/order/migrations/0008_grouped_order_deadline.py
Normal file
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
]
|
0
la_chariotte/order/migrations/__init__.py
Normal file
0
la_chariotte/order/migrations/__init__.py
Normal file
60
la_chariotte/order/models.py
Normal file
60
la_chariotte/order/models.py
Normal file
|
@ -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}"
|
36
la_chariotte/order/templates/order/grouped_order_detail.html
Normal file
36
la_chariotte/order/templates/order/grouped_order_detail.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Commandes groupées {{ grouped_order.id }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Détail de la commande groupée {{ grouped_order.id }}, avec la liste des produits associés</p>
|
||||
<br>
|
||||
<p>{{ grouped_order }}
|
||||
<p>Organisateur·ice : {{ grouped_order.orga }}</p>
|
||||
<p>Date de livraison : {{ grouped_order.delivery_date }}</p>
|
||||
|
||||
les produits disponibles pour cette commande groupée :
|
||||
|
||||
<ul>
|
||||
{% for item in grouped_order.item_set.all %}
|
||||
<li>
|
||||
{{ item }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<form action="{% url 'order:order' grouped_order.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend><h1>{{ grouped_order }}</h1></legend>
|
||||
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
|
||||
{% for item in grouped_order.item_set.all %}
|
||||
<input type="radio" name="item" id="item{{ forloop.counter }}" value="{{ item.id }}">
|
||||
<label for="item{{ forloop.counter }}">{{ item.name }}</label><br>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<input type="submit" value="Order">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
34
la_chariotte/order/templates/order/grouped_order_orga.html
Normal file
34
la_chariotte/order/templates/order/grouped_order_orga.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Commandes groupées {{ grouped_order.id }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Vue d'organisation de la commande groupée {{ grouped_order.id }}, avec les produits commandés et les commandes individuelles</p>
|
||||
<br>
|
||||
<p>{{ grouped_order }}
|
||||
<p>Organisateur·ice : {{ grouped_order.orga }}</p>
|
||||
<p>Date de livraison : {{ grouped_order.delivery_date }}</p>
|
||||
|
||||
les produits commandés pour cette commande groupée :
|
||||
<ul>
|
||||
{% for item in grouped_order.item_set.all %}
|
||||
<li>
|
||||
{{ item }}, avec {{ item.ordered_nb }} commande{{ item.ordered_nb|pluralize }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
les commandes passées pour cette commande groupée :
|
||||
<ul>
|
||||
{% for order in grouped_order.order_set.all %}
|
||||
<li>
|
||||
{{ order }} : {{ order.ordered_items.count }} produits commandés
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<a href={% url 'order:order' grouped_order.id %}>Retour à la page de commande</a>
|
||||
</body>
|
||||
</html>
|
49
la_chariotte/order/templates/order/index.html
Normal file
49
la_chariotte/order/templates/order/index.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Commandes groupées - index</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Index des commandes que l'utilisateur·ice connecté·e organise</p>
|
||||
<p>Pour l'instant, index de toutes les commandes groupées qui existent</p>
|
||||
|
||||
{% 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 %}
|
||||
<p>Commandes groupées à venir :</p>
|
||||
<ul>
|
||||
{% for gr_order in grouped_order_list.incoming_grouped_orders %}
|
||||
<li>
|
||||
<a href="{% url 'order:grouped_order_detail' gr_order.id %}"
|
||||
>{{gr_order.name}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if grouped_order_list.crossed_deadline_grouped_orders %}
|
||||
<p>Livraison à venir, date limite de commande dépassée :</p>
|
||||
<ul>
|
||||
{% for gr_order in grouped_order_list.crossed_deadline_grouped_orders %}
|
||||
<li>
|
||||
<a href="{% url 'order:grouped_order_detail' gr_order.id %}"
|
||||
>{{gr_order.name}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if grouped_order_list.old_grouped_orders %}
|
||||
<p>Livraison passée :</p>
|
||||
<ul>
|
||||
{% for gr_order in grouped_order_list.old_grouped_orders %}
|
||||
<li>
|
||||
<a href="{% url 'order:grouped_order_detail' gr_order.id %}"
|
||||
>{{gr_order.name}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>Pas de commande groupée pour l'instant</p>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
50
la_chariotte/order/tests/test_models.py
Normal file
50
la_chariotte/order/tests/test_models.py
Normal file
|
@ -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()
|
193
la_chariotte/order/tests/test_views.py
Normal file
193
la_chariotte/order/tests/test_views.py
Normal file
|
@ -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
|
15
la_chariotte/order/urls.py
Normal file
15
la_chariotte/order/urls.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "order"
|
||||
urlpatterns = [
|
||||
path("", views.IndexView.as_view(), name="index"),
|
||||
path(
|
||||
"<int:pk>/", views.GroupedOrderDetailView.as_view(), name="grouped_order_detail"
|
||||
),
|
||||
path(
|
||||
"<int:pk>/orga", views.GroupedOrderOrgaView.as_view(), name="grouped_order_orga"
|
||||
),
|
||||
path("<int:grouped_order_id>/commander/", views.order, name="order"),
|
||||
]
|
99
la_chariotte/order/views.py
Normal file
99
la_chariotte/order/views.py
Normal file
|
@ -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()
|
|
@ -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"
|
||||
|
|
|
@ -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")),
|
||||
]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -3,4 +3,46 @@ name = "la chariotte"
|
|||
version = "0.0.1"
|
||||
description = "Web application for organising grouped orders"
|
||||
readme = "readMe.md"
|
||||
license = {file = "LICENSE"}
|
||||
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
|
||||
)/
|
||||
)
|
||||
'''
|
||||
|
|
34
readMe.md
34
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/).
|
||||
## 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 .
|
||||
```
|
||||
|
|
Loading…
Reference in a new issue