mirror of
https://framagit.org/la-chariotte/la-chariotte.git
synced 2025-05-02 03:42:26 +02:00
Merge branch 'authentication' into 'develop'
Authentification See merge request hashbangfr/la_chariotte!6
This commit is contained in:
commit
0c7f489331
26 changed files with 359 additions and 38 deletions
|
@ -33,3 +33,10 @@ Si il y a des erreurs BLACK, on peut lancer black pour linter le code :
|
||||||
```bash
|
```bash
|
||||||
black .
|
black .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Architecture de l'application
|
||||||
|
|
||||||
|
Les différentes applications Django créées sont :
|
||||||
|
|
||||||
|
- Order, pour gérer tout ce qui tourne autour des commandes
|
||||||
|
- Accounts, pour gérer la création de comptes. Pour la connexion, la déconnexion et le changement de mot de passe, on utilise l'application auth intégrée à Django.
|
3
conftest.py
Normal file
3
conftest.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pytest_plugins = [
|
||||||
|
"la_chariotte.helpers.fixtures",
|
||||||
|
]
|
|
@ -12,6 +12,12 @@ black==23.3.0
|
||||||
# via pytest-black
|
# via pytest-black
|
||||||
build==0.10.0
|
build==0.10.0
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
|
chardet==4.0.0
|
||||||
|
# via diff-cover
|
||||||
|
# via flake8-black
|
||||||
|
build==0.10.0
|
||||||
|
# via pip-tools
|
||||||
|
# via pytest-black
|
||||||
chardet==4.0.0
|
chardet==4.0.0
|
||||||
# via diff-cover
|
# via diff-cover
|
||||||
click==8.1.3
|
click==8.1.3
|
||||||
|
@ -32,6 +38,16 @@ inflect==3.0.2
|
||||||
# via
|
# via
|
||||||
# diff-cover
|
# diff-cover
|
||||||
# jinja2-pluralize
|
# jinja2-pluralize
|
||||||
|
flake8==6.0.0
|
||||||
|
# via flake8-black
|
||||||
|
flake8-black==0.3.6
|
||||||
|
# via la-chariotte (pyproject.toml)
|
||||||
|
importlib-metadata==6.1.0
|
||||||
|
# via inflect
|
||||||
|
inflect==3.0.2
|
||||||
|
# via
|
||||||
|
# diff-cover
|
||||||
|
# jinja2-pluralize
|
||||||
iniconfig==2.0.0
|
iniconfig==2.0.0
|
||||||
# via pytest
|
# via pytest
|
||||||
isort==5.12.0
|
isort==5.12.0
|
||||||
|
@ -44,6 +60,8 @@ jinja2-pluralize==0.3.0
|
||||||
# via diff-cover
|
# via diff-cover
|
||||||
markupsafe==2.1.2
|
markupsafe==2.1.2
|
||||||
# via jinja2
|
# via jinja2
|
||||||
|
mccabe==0.7.0
|
||||||
|
# via flake8
|
||||||
mypy-extensions==1.0.0
|
mypy-extensions==1.0.0
|
||||||
# via black
|
# via black
|
||||||
packaging==23.0
|
packaging==23.0
|
||||||
|
@ -63,6 +81,13 @@ pluggy==1.0.0
|
||||||
# pytest
|
# pytest
|
||||||
psycopg2-binary==2.9.6
|
psycopg2-binary==2.9.6
|
||||||
# via la-chariotte (pyproject.toml)
|
# via la-chariotte (pyproject.toml)
|
||||||
|
pygments==2.14.0
|
||||||
|
# via diff-cover
|
||||||
|
# via la-chariotte (pyproject.toml)
|
||||||
|
pycodestyle==2.10.0
|
||||||
|
# via flake8
|
||||||
|
pyflakes==3.0.1
|
||||||
|
# via flake8
|
||||||
pygments==2.14.0
|
pygments==2.14.0
|
||||||
# via diff-cover
|
# via diff-cover
|
||||||
pyproject-hooks==1.0.0
|
pyproject-hooks==1.0.0
|
||||||
|
@ -95,6 +120,14 @@ tomli==2.0.1
|
||||||
# pytest
|
# pytest
|
||||||
wheel==0.40.0
|
wheel==0.40.0
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
|
zipp==3.15.0
|
||||||
|
# via importlib-metadata
|
||||||
|
# coverage
|
||||||
|
# flake8-black
|
||||||
|
# pyproject-hooks
|
||||||
|
# pytest
|
||||||
|
wheel==0.40.0
|
||||||
|
# via pip-tools
|
||||||
zipp==3.15.0
|
zipp==3.15.0
|
||||||
# via importlib-metadata
|
# via importlib-metadata
|
||||||
|
|
||||||
|
|
0
la_chariotte/accounts/__init__.py
Normal file
0
la_chariotte/accounts/__init__.py
Normal file
6
la_chariotte/accounts/apps.py
Normal file
6
la_chariotte/accounts/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "la_chariotte.accounts"
|
0
la_chariotte/accounts/migrations/__init__.py
Normal file
0
la_chariotte/accounts/migrations/__init__.py
Normal file
3
la_chariotte/accounts/models.py
Normal file
3
la_chariotte/accounts/models.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
14
la_chariotte/accounts/templates/registration/login.html
Normal file
14
la_chariotte/accounts/templates/registration/login.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Connexion{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Connexion</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit">Se connecter</button>
|
||||||
|
<a href="{% url 'accounts:signup' %}">Créer un compte</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
13
la_chariotte/accounts/templates/registration/signup.html
Normal file
13
la_chariotte/accounts/templates/registration/signup.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Créer un compte{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Créer un compte</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit">Valider</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
9
la_chariotte/accounts/urls.py
Normal file
9
la_chariotte/accounts/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "accounts"
|
||||||
|
urlpatterns = [
|
||||||
|
path("inscription/", views.SignUpView.as_view(), name="signup"),
|
||||||
|
path("", include("django.contrib.auth.urls")),
|
||||||
|
]
|
9
la_chariotte/accounts/views.py
Normal file
9
la_chariotte/accounts/views.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views import generic
|
||||||
|
|
||||||
|
|
||||||
|
class SignUpView(generic.CreateView):
|
||||||
|
form_class = UserCreationForm
|
||||||
|
success_url = reverse_lazy("accounts:login")
|
||||||
|
template_name = "registration/signup.html"
|
18
la_chariotte/helpers/fixtures.py
Normal file
18
la_chariotte/helpers/fixtures.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client_log(client, django_user_model):
|
||||||
|
username = "test@user.fr"
|
||||||
|
password = "azertypassword"
|
||||||
|
user = django_user_model.objects.create_user(username=username, password=password)
|
||||||
|
client.login(username=username, password=password)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def other_user(django_user_model):
|
||||||
|
username = "other@user.fr"
|
||||||
|
password = "azertypassword"
|
||||||
|
user = django_user_model.objects.create_user(username=username, password=password)
|
||||||
|
return user
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 4.2 on 2023-04-18 13:08
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("order", "0010_rename_grouped_order_groupedorder"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="groupedorder",
|
||||||
|
name="orga",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,10 +1,15 @@
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
class GroupedOrder(models.Model):
|
class GroupedOrder(models.Model):
|
||||||
name = models.CharField(max_length=100, null=True) # optionnal
|
name = models.CharField(
|
||||||
orga = models.CharField(max_length=100) # a changer, utiliser ForeignKey de user
|
max_length=100, null=True, verbose_name="Titre de la commande"
|
||||||
|
) # optionnal
|
||||||
|
orga = models.ForeignKey(
|
||||||
|
User, on_delete=models.CASCADE, verbose_name="Organisateur·ice"
|
||||||
|
)
|
||||||
delivery_date = models.DateField("Date de livraison")
|
delivery_date = models.DateField("Date de livraison")
|
||||||
deadline = models.DateTimeField("Date limite de commande")
|
deadline = models.DateTimeField("Date limite de commande")
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,19 @@
|
||||||
<p>Organisateur·ice : {{ grouped_order.orga }}</p>
|
<p>Organisateur·ice : {{ grouped_order.orga }}</p>
|
||||||
<p>Date de livraison : {{ grouped_order.delivery_date }}</p>
|
<p>Date de livraison : {{ grouped_order.delivery_date }}</p>
|
||||||
|
|
||||||
les produits disponibles pour cette commande groupée :
|
{% if not user.is_authenticated %}
|
||||||
|
<p>Vous êtes l'organisateur·ice de cette commande groupée ?
|
||||||
|
<a href="{% url 'order:grouped_order_overview' grouped_order.id %}">
|
||||||
|
Connectez-vous pour accéder à la page de gestion</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user == grouped_order.orga %}
|
||||||
|
<a href="{% url 'order:grouped_order_overview' grouped_order.id %}">
|
||||||
|
Page de gestion de la commande groupée</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>les produits disponibles pour cette commande groupée : </p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for item in grouped_order.item_set.all %}
|
{% for item in grouped_order.item_set.all %}
|
||||||
|
|
|
@ -29,6 +29,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<a href={% url 'order:order' grouped_order.id %}>Retour à la page de commande</a>
|
<a href={% url 'order:grouped_order_detail' grouped_order.id %}>Retour à la page de commande</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -6,7 +6,6 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p>Index des commandes que l'utilisateur·ice connecté·e organise</p>
|
<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 or grouped_order_list.crossed_deadline_grouped_orders or grouped_order_list.old_grouped_orders %}
|
||||||
{% if grouped_order_list.incoming_grouped_orders %}
|
{% if grouped_order_list.incoming_grouped_orders %}
|
||||||
|
|
|
@ -13,6 +13,7 @@ class TestGroupedOrdersModel:
|
||||||
is_ongoing() returns True if the deadline is not crossed
|
is_ongoing() returns True if the deadline is not crossed
|
||||||
"""
|
"""
|
||||||
deadline = timezone.now() + datetime.timedelta(days=10)
|
deadline = timezone.now() + datetime.timedelta(days=10)
|
||||||
|
|
||||||
ongoing_gr_order = GroupedOrder(deadline=deadline)
|
ongoing_gr_order = GroupedOrder(deadline=deadline)
|
||||||
assert ongoing_gr_order.is_ongoing()
|
assert ongoing_gr_order.is_ongoing()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -9,23 +11,35 @@ from la_chariotte.order.models import GroupedOrder, Item, Order
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def create_grouped_order(days_before_delivery_date, days_before_deadline, name):
|
def create_grouped_order(
|
||||||
|
days_before_delivery_date, days_before_deadline, name, orga_user
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Creates a grouped order.
|
Creates a grouped order.
|
||||||
"""
|
"""
|
||||||
date = timezone.now().date() + datetime.timedelta(days=days_before_delivery_date)
|
date = timezone.now().date() + datetime.timedelta(days=days_before_delivery_date)
|
||||||
deadline = timezone.now() + datetime.timedelta(days=days_before_deadline)
|
deadline = timezone.now() + datetime.timedelta(days=days_before_deadline)
|
||||||
return GroupedOrder.objects.create(
|
return GroupedOrder.objects.create(
|
||||||
name=name, orga="test orga", delivery_date=date, deadline=deadline
|
name=name, orga=orga_user, delivery_date=date, deadline=deadline
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestGroupedOrderIndexView:
|
class TestGroupedOrderIndexView:
|
||||||
def test_no_grouped_orders(self, client):
|
def test_anonymous_user_redirection(self, client):
|
||||||
|
"""
|
||||||
|
If the user is anonymous, they are redirected to login view
|
||||||
|
"""
|
||||||
|
assert auth.get_user(client).is_anonymous
|
||||||
|
response = client.get(reverse("order:index"))
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url.startswith(reverse("accounts:login"))
|
||||||
|
assert response.url.endswith(reverse("order:index"))
|
||||||
|
|
||||||
|
def test_no_grouped_orders(self, client_log):
|
||||||
"""
|
"""
|
||||||
If no grouped order exist, an appropriate message is displayed
|
If no grouped order exist, an appropriate message is displayed
|
||||||
"""
|
"""
|
||||||
response = client.get(reverse("order:index"))
|
response = client_log.get(reverse("order:index"))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "Pas de commande groupée pour l'instant" in response.content.decode()
|
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"]["old_grouped_orders"]) == 0
|
||||||
|
@ -41,22 +55,29 @@ class TestGroupedOrderIndexView:
|
||||||
len(response.context["grouped_order_list"]["incoming_grouped_orders"]) == 0
|
len(response.context["grouped_order_list"]["incoming_grouped_orders"]) == 0
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_grouped_orders_in_right_section(self, client):
|
def test_grouped_orders_in_right_section(self, client_log):
|
||||||
"""
|
"""
|
||||||
According to their delivery date and deadline, grouped orders are placed in the correct section : several gr orders
|
According to their delivery date and deadline, grouped orders are placed in the correct section : several gr orders
|
||||||
"""
|
"""
|
||||||
future_grouped_order = create_grouped_order(
|
future_grouped_order = create_grouped_order(
|
||||||
days_before_delivery_date=5, days_before_deadline=2, name="future"
|
days_before_delivery_date=5,
|
||||||
|
days_before_deadline=2,
|
||||||
|
name="future",
|
||||||
|
orga_user=auth.get_user(client_log),
|
||||||
)
|
)
|
||||||
crossed_deadline_gr_order = create_grouped_order(
|
crossed_deadline_gr_order = create_grouped_order(
|
||||||
days_before_delivery_date=2,
|
days_before_delivery_date=2,
|
||||||
days_before_deadline=-1,
|
days_before_deadline=-1,
|
||||||
name="crossed deadline",
|
name="crossed deadline",
|
||||||
|
orga_user=auth.get_user(client_log),
|
||||||
)
|
)
|
||||||
old_gr_order = create_grouped_order(
|
old_gr_order = create_grouped_order(
|
||||||
days_before_delivery_date=-1, days_before_deadline=-3, name="old"
|
days_before_delivery_date=-1,
|
||||||
|
days_before_deadline=-3,
|
||||||
|
name="old",
|
||||||
|
orga_user=auth.get_user(client_log),
|
||||||
)
|
)
|
||||||
response = client.get(reverse("order:index"))
|
response = client_log.get(reverse("order:index"))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "Pas de commande groupée pour l'instant" not in response.content.decode()
|
assert "Pas de commande groupée pour l'instant" not in response.content.decode()
|
||||||
assert "Commandes groupées à venir" in response.content.decode()
|
assert "Commandes groupées à venir" in response.content.decode()
|
||||||
|
@ -87,14 +108,17 @@ class TestGroupedOrderIndexView:
|
||||||
== future_grouped_order
|
== future_grouped_order
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_grouped_orders_in_right_section__with_only_old(self, client):
|
def test_grouped_orders_in_right_section__with_only_old(self, client_log):
|
||||||
"""
|
"""
|
||||||
According to their delivery date and deadline, grouped orders are placed in correct section : only old gr order
|
According to their delivery date and deadline, grouped orders are placed in correct section : only old gr order
|
||||||
"""
|
"""
|
||||||
old_gr_order = create_grouped_order(
|
old_gr_order = create_grouped_order(
|
||||||
days_before_delivery_date=-1, days_before_deadline=-3, name="passée"
|
days_before_delivery_date=-1,
|
||||||
|
days_before_deadline=-3,
|
||||||
|
name="passée",
|
||||||
|
orga_user=auth.get_user(client_log),
|
||||||
)
|
)
|
||||||
response = client.get(reverse("order:index"))
|
response = client_log.get(reverse("order:index"))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "Pas de commande groupée pour l'instant" not in response.content.decode()
|
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 "Commandes groupées à venir" not in response.content.decode()
|
||||||
|
@ -117,14 +141,17 @@ class TestGroupedOrderIndexView:
|
||||||
== old_gr_order
|
== old_gr_order
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_grouped_orders_in_right_section__with_only_future(self, client):
|
def test_grouped_orders_in_right_section__with_only_future(self, client_log):
|
||||||
"""
|
"""
|
||||||
According to their delivery date and deadline, grouped orders are placed in correct section : only incoming gr order
|
According to their delivery date and deadline, grouped orders are placed in correct section : only incoming gr order
|
||||||
"""
|
"""
|
||||||
future_grouped_order = create_grouped_order(
|
future_grouped_order = create_grouped_order(
|
||||||
days_before_delivery_date=5, days_before_deadline=2, name="future"
|
days_before_delivery_date=5,
|
||||||
|
days_before_deadline=2,
|
||||||
|
name="future",
|
||||||
|
orga_user=auth.get_user(client_log),
|
||||||
)
|
)
|
||||||
response = client.get(reverse("order:index"))
|
response = client_log.get(reverse("order:index"))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "Pas de commande groupée pour l'instant" not in response.content.decode()
|
assert "Pas de commande groupée pour l'instant" not in response.content.decode()
|
||||||
assert "Commandes groupées à venir" in response.content.decode()
|
assert "Commandes groupées à venir" in response.content.decode()
|
||||||
|
@ -147,14 +174,45 @@ class TestGroupedOrderIndexView:
|
||||||
== future_grouped_order
|
== future_grouped_order
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_gr_order_not_organized_by_logged_user(self, client_log, other_user):
|
||||||
|
"""
|
||||||
|
If the logged user is not the organizer of a grouped order they don't see it on IndexView
|
||||||
|
"""
|
||||||
|
logged_user = auth.get_user(client_log)
|
||||||
|
future_grouped_order = create_grouped_order(
|
||||||
|
days_before_delivery_date=5,
|
||||||
|
days_before_deadline=2,
|
||||||
|
name="future",
|
||||||
|
orga_user=other_user,
|
||||||
|
)
|
||||||
|
assert future_grouped_order.orga is not logged_user
|
||||||
|
response = client_log.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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestGroupedOrderDetailView:
|
class TestGroupedOrderDetailView:
|
||||||
def test_order_item(self, client):
|
def test_order_item(self, client, other_user):
|
||||||
"""
|
"""
|
||||||
From the OrderDetailView, we order an item using the order form, and it creates an Order woth and Ordered_item inside
|
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(
|
grouped_order = create_grouped_order(
|
||||||
days_before_delivery_date=5, days_before_deadline=2, name="gr order test"
|
days_before_delivery_date=5,
|
||||||
|
days_before_deadline=2,
|
||||||
|
name="gr order test",
|
||||||
|
orga_user=other_user,
|
||||||
)
|
)
|
||||||
item = Item.objects.create(name="test item", grouped_order=grouped_order)
|
item = Item.objects.create(name="test item", grouped_order=grouped_order)
|
||||||
detail_url = reverse(
|
detail_url = reverse(
|
||||||
|
@ -182,7 +240,7 @@ class TestGroupedOrderDetailView:
|
||||||
)
|
)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert response.url == reverse(
|
assert response.url == reverse(
|
||||||
"order:grouped_order_orga",
|
"order:grouped_order_overview",
|
||||||
kwargs={
|
kwargs={
|
||||||
"pk": grouped_order.pk,
|
"pk": grouped_order.pk,
|
||||||
},
|
},
|
||||||
|
@ -191,3 +249,60 @@ class TestGroupedOrderDetailView:
|
||||||
assert item.ordered_nb == 1
|
assert item.ordered_nb == 1
|
||||||
order = Order.objects.first()
|
order = Order.objects.first()
|
||||||
assert order.ordered_items.count() == 1
|
assert order.ordered_items.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroupedOrderOrgaView:
|
||||||
|
def test_user_not_logged_redirect(self, client, other_user):
|
||||||
|
"""
|
||||||
|
A user that is not logged cannot see the GroupedOrderOrgaView. They get redirected to the login view
|
||||||
|
"""
|
||||||
|
grouped_order = create_grouped_order(
|
||||||
|
days_before_delivery_date=5,
|
||||||
|
days_before_deadline=2,
|
||||||
|
name="gr order test",
|
||||||
|
orga_user=other_user,
|
||||||
|
)
|
||||||
|
orga_view_url = reverse(
|
||||||
|
"order:grouped_order_overview",
|
||||||
|
kwargs={
|
||||||
|
"pk": grouped_order.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert auth.get_user(client).is_anonymous
|
||||||
|
response = client.get(orga_view_url)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url.startswith(reverse("accounts:login"))
|
||||||
|
assert response.url.endswith(
|
||||||
|
reverse(
|
||||||
|
"order:grouped_order_overview",
|
||||||
|
kwargs={
|
||||||
|
"pk": grouped_order.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_user_not_orga_redirect(self, client_log, other_user):
|
||||||
|
"""
|
||||||
|
A user that is not orga cannot see the GroupedOrderOrgaView.
|
||||||
|
They get a 403 forbidden error
|
||||||
|
"""
|
||||||
|
grouped_order = create_grouped_order(
|
||||||
|
days_before_delivery_date=5,
|
||||||
|
days_before_deadline=2,
|
||||||
|
name="gr order test",
|
||||||
|
orga_user=other_user,
|
||||||
|
)
|
||||||
|
orga_view_url = reverse(
|
||||||
|
"order:grouped_order_overview",
|
||||||
|
kwargs={
|
||||||
|
"pk": grouped_order.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
detail_view_url = reverse(
|
||||||
|
"order:grouped_order_detail",
|
||||||
|
kwargs={
|
||||||
|
"pk": grouped_order.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response = client_log.get(orga_view_url)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
|
@ -9,7 +9,9 @@ urlpatterns = [
|
||||||
"<int:pk>/", views.GroupedOrderDetailView.as_view(), name="grouped_order_detail"
|
"<int:pk>/", views.GroupedOrderDetailView.as_view(), name="grouped_order_detail"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<int:pk>/orga", views.GroupedOrderOrgaView.as_view(), name="grouped_order_orga"
|
"<int:pk>/gerer",
|
||||||
|
views.GroupedOrderOrgaView.as_view(),
|
||||||
|
name="grouped_order_overview",
|
||||||
),
|
),
|
||||||
path("<int:grouped_order_id>/commander/", views.order, name="order"),
|
path("<int:grouped_order_id>/commander/", views.order, name="order"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
|
||||||
from .models import GroupedOrder, Item, Order, OrderedItem
|
from .models import GroupedOrder, Item, Order, OrderedItem
|
||||||
|
|
||||||
|
|
||||||
class IndexView(generic.ListView):
|
class IndexView(LoginRequiredMixin, generic.ListView):
|
||||||
"""Vue de toutes les commandes groupées existantes - plus tard, de toutes les commandes groupées de l'utilisateur connecté"""
|
"""View showing all the grouped orders organized by the authenticated user"""
|
||||||
|
|
||||||
template_name = "order/index.html"
|
template_name = "order/index.html"
|
||||||
context_object_name = "grouped_order_list"
|
context_object_name = "grouped_order_list"
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
"""Only grouped orders organized by logged user"""
|
||||||
|
logged_user_grouped_orders = GroupedOrder.objects.filter(orga=self.request.user)
|
||||||
|
|
||||||
"""3 grouped_order status : incoming, crossed_deadline, and old"""
|
"""3 grouped_order status : incoming, crossed_deadline, and old"""
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
today = now.date()
|
today = now.date()
|
||||||
|
|
||||||
"""Return the 5 most recent old grouped orders"""
|
"""Return the 5 most recent old grouped orders"""
|
||||||
old_grouped_orders = GroupedOrder.objects.filter(
|
old_grouped_orders = logged_user_grouped_orders.filter(
|
||||||
# is_to_be_delivered=False
|
# is_to_be_delivered=False
|
||||||
delivery_date__lt=today
|
delivery_date__lt=today
|
||||||
).order_by("-delivery_date")[
|
).order_by("-delivery_date")[
|
||||||
|
@ -28,13 +32,13 @@ class IndexView(generic.ListView):
|
||||||
|
|
||||||
"""Return all grouped orders, that have crossed their ordering deadline but the delivery date is still to come"""
|
"""Return all grouped orders, that have crossed their ordering deadline but the delivery date is still to come"""
|
||||||
crossed_dealine_grouped_orders = (
|
crossed_dealine_grouped_orders = (
|
||||||
GroupedOrder.objects.filter(delivery_date__gte=today)
|
logged_user_grouped_orders.filter(delivery_date__gte=today)
|
||||||
.filter(deadline__lt=now)
|
.filter(deadline__lt=now)
|
||||||
.order_by("-delivery_date")
|
.order_by("-delivery_date")
|
||||||
) # delivery date >= today (not delivered) and deadline < today (we cannot order)
|
) # delivery date >= today (not delivered) and deadline < today (we cannot order)
|
||||||
|
|
||||||
"""Return all incoming grouped orders"""
|
"""Return all incoming grouped orders"""
|
||||||
incoming_grouped_orders = GroupedOrder.objects.filter(
|
incoming_grouped_orders = logged_user_grouped_orders.filter(
|
||||||
deadline__gte=now
|
deadline__gte=now
|
||||||
).order_by(
|
).order_by(
|
||||||
"deadline"
|
"deadline"
|
||||||
|
@ -47,18 +51,23 @@ class IndexView(generic.ListView):
|
||||||
|
|
||||||
|
|
||||||
class GroupedOrderDetailView(generic.DetailView):
|
class GroupedOrderDetailView(generic.DetailView):
|
||||||
"""Vue de détail d'une commande groupée - possibilité de commander si elle est en cours"""
|
"""Detail view for a grouped order - possibility to order if it is ongoing - No permissions restriction"""
|
||||||
|
|
||||||
model = GroupedOrder
|
model = GroupedOrder
|
||||||
template_name = "order/grouped_order_detail.html"
|
template_name = "order/grouped_order_detail.html"
|
||||||
context_object_name = "grouped_order"
|
context_object_name = "grouped_order"
|
||||||
|
|
||||||
|
|
||||||
class GroupedOrderOrgaView(generic.DetailView):
|
class GroupedOrderOrgaView(UserPassesTestMixin, generic.DetailView):
|
||||||
"""Vue de supervision d'une commande groupée"""
|
"""Overview of a grouped order, for the organizer"""
|
||||||
|
|
||||||
model = GroupedOrder
|
model = GroupedOrder
|
||||||
template_name = "order/grouped_order_orga.html"
|
template_name = "order/grouped_order_overview.html"
|
||||||
|
context_object_name = "grouped_order"
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
"""Accessible only if the requesting user is the grouped order organizer"""
|
||||||
|
return self.get_object().orga == self.request.user
|
||||||
|
|
||||||
|
|
||||||
def order(
|
def order(
|
||||||
|
@ -86,7 +95,7 @@ def order(
|
||||||
# with POST data. This prevents data from being posted twice if a
|
# with POST data. This prevents data from being posted twice if a
|
||||||
# user hits the Back button.
|
# user hits the Back button.
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
reverse("order:grouped_order_orga", args=(grouped_order.pk,))
|
reverse("order:grouped_order_overview", args=(grouped_order.pk,))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ ALLOWED_HOSTS = []
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"la_chariotte.order",
|
"la_chariotte.order",
|
||||||
|
"la_chariotte.accounts",
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
|
@ -52,10 +53,14 @@ MIDDLEWARE = [
|
||||||
|
|
||||||
ROOT_URLCONF = "la_chariotte.urls"
|
ROOT_URLCONF = "la_chariotte.urls"
|
||||||
|
|
||||||
|
LOGIN_URL = "accounts:login"
|
||||||
|
LOGIN_REDIRECT_URL = "home"
|
||||||
|
LOGOUT_REDIRECT_URL = "home"
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
"DIRS": [],
|
"DIRS": [BASE_DIR / "la_chariotte" / "templates"],
|
||||||
"APP_DIRS": True,
|
"APP_DIRS": True,
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
|
|
13
la_chariotte/templates/base.html
Normal file
13
la_chariotte/templates/base.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>{% block title %}{% endblock %} - La Chariotte</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
19
la_chariotte/templates/home.html
Normal file
19
la_chariotte/templates/home.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Accueil{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
Hi {{ user.username }}!
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'order:index' %}">Mes commandes groupées</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'accounts:logout' %}">Se déconnecter</a>
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>You are not logged in</p>
|
||||||
|
<a href="{% url 'accounts:login' %}">Se connecter</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -15,8 +15,11 @@ Including another URLconf
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("commande/", include("la_chariotte.order.urls")),
|
path("commande/", include("la_chariotte.order.urls")),
|
||||||
|
path("comptes/", include("la_chariotte.accounts.urls")),
|
||||||
|
path("", TemplateView.as_view(template_name="home.html"), name="home"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
[project]
|
[project]
|
||||||
name = "la chariotte"
|
name = "la_chariotte"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
description = "Web application for organising grouped orders"
|
description = "Web application for organising grouped orders"
|
||||||
|
authors = [{name = "Laetitia Getti", email = "laetitia@chariotte.fr"}]
|
||||||
readme = "readMe.md"
|
readme = "readMe.md"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
authors = [{name = "Hashbang", email = "support@hashbang.fr"}]
|
classifiers = ["License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django>=4,<5",
|
"django>=4,<5",
|
||||||
"psycopg2>=2,<3",
|
"psycopg2-binary>=2,<3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
Loading…
Reference in a new issue