Merge branch 'commandes-basiques' into 'develop'

Possibilité de commander 1 produit

See merge request hashbangfr/la_chariotte!1
This commit is contained in:
Laetitia Getti 2023-04-18 14:12:02 +00:00
commit 245d7a17a2
33 changed files with 911 additions and 51 deletions

10
.coveragerc Normal file
View 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

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
__pycache__/
coverage.xml
.coverage

View file

@ -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."

View file

@ -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()

View file

View 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)

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class OrderConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "la_chariotte.order"

View 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')),
],
),
]

View 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),
),
]

View 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')),
],
),
]

View 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),
),
]

View file

@ -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')),
],
),
]

View 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),
),
]

View file

@ -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'),
),
]

View 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,
),
]

View file

@ -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',
),
]

View file

@ -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",
),
]

View 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}"

View 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>

View 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>

View 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>

View 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()

View 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

View 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"),
]

View 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()

View file

@ -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"

View file

@ -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")),
]

View file

@ -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()

View file

@ -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()

View file

@ -4,3 +4,45 @@ version = "0.0.1"
description = "Web application for organising grouped orders"
readme = "readMe.md"
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
)/
)
'''

View file

@ -1,3 +1,35 @@
# La Chariotte
## 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 .
```