Compare commits

..

2 commits

Author SHA1 Message Date
selfhoster1312 ACAB
8d7ba5e616 Merge branch 'feat-distribution-place' into 'develop'
Draft: feature: Add placekey relationship for distribution spots

See merge request la-chariotte/la-chariotte!129
2025-03-02 19:15:45 +00:00
selfhoster1312
f2d8d9b9c2 feature: Add placekey relationship for distribution spots 2025-03-02 15:23:27 +01:00
29 changed files with 365 additions and 337 deletions

View file

View file

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

View file

@ -0,0 +1,35 @@
import datetime
from django import forms
from django.contrib.auth import get_user_model
from django.forms.utils import to_current_timezone
from django.utils import timezone
from la_chariotte.lieu.models import Lieu
class LieuForm(forms.ModelForm):
class Meta:
model = Lieu
fields = [
"name",
"description",
"url",
]
widgets = {
"name": forms.TextInput(
attrs={"placeholder": "ex : Centre social Kropotkine"}
),
"description": forms.Textarea(
attrs={"placeholder": "Plus d'infos sur le lieu ? (facultatif)"}
),
"url": forms.TextInput(attrs={"placeholder": "raccourci"}),
}
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super().__init__(*args, **kwargs)
def save(self, commit=True):
self.instance.orga = get_user_model().objects.get(id=self.user.pk)
return super().save(commit=commit)

View file

@ -0,0 +1,28 @@
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Lieu",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, verbose_name="Nom du lieu de distribution")),
("url", models.CharField(max_length=20, verbose_name='Portion du lien pour le lieu', unique=True)),
("orga", models.ForeignKey(on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Organisateur·ice')),
("description", models.TextField(blank=True, null=True, verbose_name="Description")),
],
),
]

View file

View file

@ -0,0 +1,27 @@
import random
import base36
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils import timezone
from la_chariotte.order.models import GroupedOrder
from la_chariotte.settings import AUTH_USER_MODEL
class Lieu(models.Model):
name = models.CharField(max_length=100, verbose_name="Nom du lieu de distribution")
orga = models.ForeignKey(
AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="Organisateur·ice"
)
url = models.CharField(
max_length=20, verbose_name="Portion du lien pour le lieu", unique=True
)
description = models.TextField("Description", null=True, blank=True)
def __str__(self): # pragma: no cover
return self.name
def get_absolute_url(self):
return reverse("lieu:lieu_update", kwargs={"url": self.url})

View file

@ -7,11 +7,11 @@
{% block content_title %}Lieux de distribution que vous organisez{% endblock %} {% block content_title %}Lieux de distribution que vous organisez{% endblock %}
</p> </p>
<div class="buttons is-pulled-right"> <div class="buttons is-pulled-right">
<a class="button is-primary" href="{% url 'order:place_create' %}"> <a class="button is-primary" href="{% url 'lieu:create_lieu' %}">
<i class="fa fa-plus-circle mr-3" aria-hidden="true"></i> <i class="fa fa-plus-circle mr-3" aria-hidden="true"></i>
Créer un nouveau lieu de distribution</a> Créer un nouveau lieu de distribution</a>
</div> </div>
{% if context.places %} {% if lieu_context.lieu_liste %}
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -23,21 +23,21 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for place in context.places %} {% for lieu in lieu_context.lieu_liste %}
<tr> <tr>
<td> <td>
<a title="Détail du lieu de distribution" href="{% url 'order:place_update' place.code %}">{{ place }}</a> <a title="Détail du lieu de distribution" href="{% url 'lieu:lieu_update' lieu.url %}">{{ lieu }}</a>
</td> </td>
<td> <td>
{% if place.code in context.orders.keys %} {% if lieu.url in lieu_context.commandes.keys %}
{% for place_url, place_orders in context.orders.items %} {% for lieu_url, lieu_commandes in lieu_context.commandes.items %}
{% if place_url == place.code %} {% if lieu_url == lieu.url %}
{% for order in place_orders %} {% for commande in lieu_commandes %}
{% url 'order:grouped_order_detail' code=order.code as order_url %} {% url 'order:grouped_order_detail' code=commande.code as order_url %}
{% if order_url %} {% if order_url %}
<a href="{{ order_url }}">{{order.name}}</a><br> <a href="{{ order_url }}">{{commande.name}}</a><br>
{% else %} {% else %}
{{ order.name }}<br> {{ commande.name }}<br>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@ -47,10 +47,10 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ place.description }} {{ lieu.description }}
</td> </td>
<td> <td>
{{ place.code }} {{ lieu.url }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -9,14 +9,14 @@
{% block content_title %}Créer un lieu de distribution{% endblock %} {% block content_title %}Créer un lieu de distribution{% endblock %}
</p> </p>
<div class="box"> <div class="box">
<p class="title">Nouveau lieu</p> <p class="title">Nouvelle lieu</p>
<div class="columns"> <div class="columns">
<div class="column is-8"> <div class="column is-8">
<form method="post" onsubmit="deadlinePassedCheck(event)"> <form method="post" onsubmit="deadlinePassedCheck(event)">
{% csrf_token %} {% csrf_token %}
{{ form | crispy }} {{ form | crispy }}
<div class="buttons"> <div class="buttons">
<a class="button is-light" href="{% url 'order:place_index' %}">Annuler</a> <a class="button is-light" href="{% url 'lieu:index' %}">Annuler</a>
<input class="button is-primary" type="submit" value="Suivant"> <input class="button is-primary" type="submit" value="Suivant">
</div> </div>
</form> </form>

View file

@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}{{ lieu }}{% endblock %}
{% block content %}
<p class="desktop-hidden mobile-content-title">
{% block content_title %}Modifier le lieu de livraison{% endblock %}
</p>
<div class="box">
<p class="title">{{ lieu.name }} - modifier</p>
<form method="post">{% csrf_token %}
{{ form | crispy }}
<div class="buttons">
<a class="button is-light" href="{% url 'lieu:index' %}">Annuler</a>
<input class="button is-primary" type="submit" value="Suivant">
</div>
</form>
</div>
{% endblock %}

View file

@ -9,11 +9,11 @@
{% block content_title %}Modifier le lieu de livraison{% endblock %} {% block content_title %}Modifier le lieu de livraison{% endblock %}
</p> </p>
<div class="box"> <div class="box">
<p class="title">{{ place.name }} - modifier</p> <p class="title">{{ lieu.name }} - modifier</p>
<form method="post">{% csrf_token %} <form method="post">{% csrf_token %}
{{ form | crispy }} {{ form | crispy }}
<div class="buttons"> <div class="buttons">
<a class="button is-light" href="{% url 'order:place_index' %}">Annuler</a> <a class="button is-light" href="{% url 'lieu:index' %}">Annuler</a>
<input class="button is-primary" type="submit" value="Suivant"> <input class="button is-primary" type="submit" value="Suivant">
</div> </div>
</form> </form>

14
la_chariotte/lieu/urls.py Normal file
View file

@ -0,0 +1,14 @@
from django.urls import path
from . import views
app_name = "lieu"
urlpatterns = [
path("", views.IndexView.as_view(), name="index"),
path(
"<str:url>/",
views.LieuUpdateView.as_view(),
name="lieu_update",
),
path("creer", views.LieuCreateView.as_view(), name="create_lieu"),
]

View file

@ -0,0 +1,6 @@
from .lieu import (
IndexView,
LieuCreateView,
LieuDetailView,
LieuUpdateView,
)

View file

@ -0,0 +1,87 @@
import csv
import json
from django import http
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.views import generic
from django_weasyprint import WeasyTemplateResponseMixin
from icalendar import Calendar, Event, vCalAddress, vText
from la_chariotte.order.models import GroupedOrder
from la_chariotte.order.views.mixins import UserIsOrgaMixin
from ..forms import LieuForm
from ..models import Lieu
class IndexView(LoginRequiredMixin, generic.ListView):
"""View showing all the grouped orders managed by the authenticated user"""
template_name = "lieu/index.html"
context_object_name = "lieu_context"
def get_queryset(self):
lieux = Lieu.objects.filter(orga=self.request.user)
commandes_par_lieu = dict()
for lieu in lieux:
orders = GroupedOrder.objects.all().filter(placekey=lieu)
if orders:
commandes_par_lieu[lieu.url] = orders
return {
"lieu_liste": lieux,
"commandes": commandes_par_lieu,
}
class LieuDetailView(generic.DetailView):
model = Lieu
template_name = "lieu/lieu_detail.html"
context_object_name = "lieu_context"
form_class = LieuForm
def get_object(self, queryset=None):
return get_object_or_404(Lieu, url=self.kwargs.get("url"))
def get_context_data(self, **kwargs):
lieu = self.get_object()
context = super().get_context_data(**kwargs)
return context
class LieuUpdateView(UserIsOrgaMixin, generic.UpdateView):
model = Lieu
template_name = "lieu/lieu_update.html"
context_object_name = "lieu"
form_class = LieuForm
# Can't change URL after creation
form_class.base_fields["url"].disabled = True
def get_object(self, queryset=None):
return get_object_or_404(Lieu, url=self.kwargs.get("url"))
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
class LieuCreateView(LoginRequiredMixin, generic.CreateView):
model = Lieu
form_class = LieuForm
template_name = "lieu/lieu_create.html"
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
def form_valid(self, form):
self.object = form.save()
return super().form_valid(form)

View file

@ -22,7 +22,7 @@
<p><strong>Rendez-vous pour la distribution</strong> : <p><strong>Rendez-vous pour la distribution</strong> :
le {{ order.grouped_order.delivery_date }}{% if order.grouped_order.delivery_slot %}, {{ order.grouped_order.delivery_slot }}{% endif %} le {{ order.grouped_order.delivery_date }}{% if order.grouped_order.delivery_slot %}, {{ order.grouped_order.delivery_slot }}{% endif %}
{% if order.place %}<br>Lieu : {{ order.place }}{% endif %} {% if order.grouped_order.place %}<br>Lieu : {{ order.grouped_order.place }}{% endif %}
</p> </p>
<p><strong>Une question sur cette commande groupée ?</strong><br>Vous pouvez contacter l'organisateur·ice de la commande, <strong>{{ order.grouped_order.orga }}</strong> : <p><strong>Une question sur cette commande groupée ?</strong><br>Vous pouvez contacter l'organisateur·ice de la commande, <strong>{{ order.grouped_order.orga }}</strong> :

View file

@ -6,7 +6,8 @@ from django.contrib.auth import get_user_model
from django.forms.utils import to_current_timezone from django.forms.utils import to_current_timezone
from django.utils import timezone from django.utils import timezone
from la_chariotte.order.models import GroupedOrder, Item, Place from la_chariotte.lieu.models import Lieu
from la_chariotte.order.models import GroupedOrder, Item
class GroupedOrderForm(forms.ModelForm): class GroupedOrderForm(forms.ModelForm):
@ -23,10 +24,9 @@ class GroupedOrderForm(forms.ModelForm):
label="Numéro de téléphone obligatoire pour les participants", label="Numéro de téléphone obligatoire pour les participants",
required=False, required=False,
) )
places = forms.ModelMultipleChoiceField( placekey = forms.ModelMultipleChoiceField(
label="Lieux de distribution", label="Lieux de distribution",
# TODO: filter own places queryset=Lieu.objects.all(),
queryset=Place.objects.all(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False, required=False,
) )
@ -39,9 +39,10 @@ class GroupedOrderForm(forms.ModelForm):
"deadline_time", "deadline_time",
"delivery_date", "delivery_date",
"delivery_slot", "delivery_slot",
"places", "place",
"description", "description",
"is_phone_mandatory", "is_phone_mandatory",
"placekey",
] ]
widgets = { widgets = {
"name": forms.TextInput( "name": forms.TextInput(
@ -54,6 +55,7 @@ class GroupedOrderForm(forms.ModelForm):
"delivery_slot": forms.TextInput( "delivery_slot": forms.TextInput(
attrs={"placeholder": "14h - 17h (facultatif)"} attrs={"placeholder": "14h - 17h (facultatif)"}
), ),
"place": forms.TextInput(attrs={"placeholder": "(facultatif)"}),
"description": forms.Textarea( "description": forms.Textarea(
attrs={ attrs={
"placeholder": "Plus d'infos sur la commande groupée ? (facultatif)" "placeholder": "Plus d'infos sur la commande groupée ? (facultatif)"
@ -110,32 +112,3 @@ class JoinGroupedOrderForm(forms.Form):
"Désolé, nous ne trouvons aucune commande avec ce code" "Désolé, nous ne trouvons aucune commande avec ce code"
) )
return form_code return form_code
class PlaceForm(forms.ModelForm):
class Meta:
model = Place
fields = [
"name",
"description",
"code",
]
widgets = {
"name": forms.TextInput(
attrs={"placeholder": "ex : Centre social Kropotkine"}
),
"description": forms.Textarea(
attrs={"placeholder": "Plus d'infos sur le lieu ? (facultatif)"}
),
"code": forms.TextInput(
attrs={"placeholder": "Identifiant unique du lieu (raccourci)"}
),
}
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super().__init__(*args, **kwargs)
def save(self, commit=True):
self.instance.orga = get_user_model().objects.get(id=self.user.pk)
return super().save(commit=commit)

View file

@ -0,0 +1,23 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("order", "0029_set_phone_mandatory_for_existing_orders"),
("lieu", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="groupedorder",
name="placekey",
field=models.ManyToManyField(
verbose_name="Lieu de distribution",
to="lieu.lieu",
),
),
migrations.AddField(
model_name="order",
name="placekey",
field=models.ForeignKey(blank=True, null=True, on_delete=models.deletion.CASCADE, to='lieu.lieu'),
),
]

View file

@ -1,75 +0,0 @@
from django.conf import settings
from django.db import migrations, models
import base36
import random
def random_code():
return base36.dumps(
random.randint(pow(36, code_length - 2), pow(36, code_length - 1) - 1)
)
# Migrate existing GroupedOrder `place` to the new `place` table
def link_existing_place(apps, schema_editor):
GroupedOrder = apps.get_model('order', 'GroupedOrder')
Place = apps.get_model('order', 'Place')
for grouped_order in GroupedOrder.objects.all():
if grouped_order.place:
# Generate new random code for this existing place
code = random_code()
while Place.objects.all().filter(code=code):
# Random code already exists, try a new random code
code = random_code()
place = Place.objects.create(
name=grouped_order.place,
code=code,
)
grouped_order.places.add(place)
grouped_order.save()
class Migration(migrations.Migration):
dependencies = [
("order", "0029_set_phone_mandatory_for_existing_orders"),
]
operations = [
migrations.CreateModel(
name="Place",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, verbose_name="Nom du lieu de distribution")),
("code", models.CharField(max_length=20, verbose_name="Identifiant unique du lieu (raccourci)", unique=True)),
("orga", models.ForeignKey(on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Organisateur·ice')),
("description", models.TextField(blank=True, null=True, verbose_name="Description")),
],
),
migrations.AddField(
model_name="groupedorder",
name="places",
field=models.ManyToManyField(
to="order.place",
verbose_name="Lieux de distribution",
related_name="orders",
)
),
migrations.RunPython(link_existing_place),
migrations.RemoveField(
model_name='groupedorder',
name='place',
),
migrations.AddField(
model_name="order",
name="place",
field=models.ForeignKey(blank=True, null=True, on_delete=models.deletion.CASCADE, to='order.place'),
),
]

View file

@ -22,9 +22,14 @@ class GroupedOrder(models.Model):
) )
deadline = models.DateTimeField("Date limite de commande") deadline = models.DateTimeField("Date limite de commande")
# Associate with zero/more saved distribution places # Try to associate with a saved distribution place in DB
places = models.ManyToManyField( placekey = models.ManyToManyField(
"order.Place", verbose_name="Lieux de distribution", related_name="orders" "lieu.Lieu",
verbose_name="Lieu de distribution",
)
# Alternatively, propose a free-text distribution place field
place = models.CharField(
max_length=100, null=True, blank=True, verbose_name="Lieu de livraison"
) )
description = models.TextField("Description", null=True, blank=True) description = models.TextField("Description", null=True, blank=True)
@ -57,13 +62,6 @@ class GroupedOrder(models.Model):
) )
self.code = f"{base_36_pk}{random_string}"[:code_length] self.code = f"{base_36_pk}{random_string}"[:code_length]
@property
def has_places(self):
# Check whether this GroupedOrder has any distribution Place enabled
if len(self.places.all()) > 0:
return True
return False
@property @property
def total_price(self): def total_price(self):
price = 0 price = 0
@ -133,8 +131,8 @@ class Order(models.Model):
author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE) author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE)
created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True) created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True)
note = models.TextField(max_length=200, null=True, blank=True) note = models.TextField(max_length=200, null=True, blank=True)
place = models.ForeignKey( placekey = models.ForeignKey(
"order.place", on_delete=models.CASCADE, null=True, blank=True "lieu.lieu", on_delete=models.CASCADE, null=True, blank=True
) )
@property @property
@ -209,22 +207,3 @@ class OrderedItem(models.Model):
def __str__(self): # pragma: no cover def __str__(self): # pragma: no cover
return f"{self.nb} {self.item}, dans la commande {self.order.pk}" return f"{self.nb} {self.item}, dans la commande {self.order.pk}"
class Place(models.Model):
name = models.CharField(max_length=100, verbose_name="Nom du lieu de distribution")
orga = models.ForeignKey(
AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="Organisateur·ice"
)
code = models.CharField(
max_length=20,
verbose_name="Identifiant unique du lieu (raccourci)",
unique=True,
)
description = models.TextField("Description", null=True, blank=True)
def __str__(self): # pragma: no cover
return self.name
def get_absolute_url(self):
return reverse("order:place_update", kwargs={"code": self.code})

View file

@ -27,6 +27,10 @@
{% endif %} {% endif %}
</div> </div>
<div class="column"> <div class="column">
{% if grouped_order.place %}
<p><i class="fa fa-map-pin mr-3" aria-label="Lieu" title="Lieu"
aria-hidden="true"></i>{{ grouped_order.place }}</p>
{% endif %}
<p><i class="fa fa-calendar-check-o mr-3" aria-label="Date limite de commande" <p><i class="fa fa-calendar-check-o mr-3" aria-label="Date limite de commande"
title="Date limite de commande" aria-hidden="true"></i> title="Date limite de commande" aria-hidden="true"></i>
Commandes avant le {{ grouped_order.deadline|date:'d M Y' }} à {{ grouped_order.deadline|date:'H:i' }} Commandes avant le {{ grouped_order.deadline|date:'d M Y' }} à {{ grouped_order.deadline|date:'H:i' }}
@ -167,14 +171,15 @@
<p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label> <p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label>
<textarea id="note" rows=3 name="note">{{ note }}</textarea></p> <textarea id="note" rows=3 name="note">{{ note }}</textarea></p>
{% if places %} {% if placekey %}
<label for="place">Point de distribution:</label> <label for="placekey">Point de distribution:</label>
<select name="place" id="place-select"> <select name="placekey" id="placekey-select">
{% for place in places %} {% for place in placekey %}
<option value="{{ place.code }}">{{ place.name }}</option> <option value="{{ place.url }}">{{ place.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
{% endif %} {% endif %}
<hr> <hr>
<div class="buttons"> <div class="buttons">

View file

@ -184,7 +184,7 @@
<header class="modal-card-head has-background-info"> <header class="modal-card-head has-background-info">
<div class="modal-card-title-container"> <div class="modal-card-title-container">
<p class="modal-card-title mb-2">Commande de {{ order.author }}</p> <p class="modal-card-title mb-2">Commande de {{ order.author }}</p>
{% if order.place %}<p class="has-text-grey-dark">Lieu: {{ order.place }}</p>{% endif %} {% if order.placekey %}<p class="has-text-grey-dark">Lieu: {{ order.placekey }}</p>{% endif %}
<p class="has-text-grey-dark">Le {{ order.created_date|date:'d M Y' }} à {{ order.created_date|date:'H:i' }}</p> <p class="has-text-grey-dark">Le {{ order.created_date|date:'d M Y' }} à {{ order.created_date|date:'H:i' }}</p>
</div> </div>
<button class="delete" aria-label="close"></button> <button class="delete" aria-label="close"></button>

View file

@ -75,10 +75,10 @@
<h2 style="text-align: center"> <h2 style="text-align: center">
{{ grouped_order.name }} - {{ grouped_order.delivery_date }} {{ grouped_order.name }} - {{ grouped_order.delivery_date }}
</h2> </h2>
{% if places %} {% if by_lieu %}
{% for place_name, place_orders in places.items %} {% for lieu_name, ordered_dict in lieux.items %}
<h3>{{ place_name }}</h3> <h3>{{ lieu_name }}</h3>
{% include 'order/grouped_order_sheet_list.html' with orders_dict=place_orders items=items grouped_order=grouped_order %} {% include 'order/grouped_order_sheet_list.html' with orders_dict=lieu_orders items=items grouped_order=grouped_order %}
{% endfor %} {% endfor %}
{% else %} {% else %}
{% include 'order/grouped_order_sheet_list.html' with orders_dict=orders_dict items=items grouped_order=grouped_order %} {% include 'order/grouped_order_sheet_list.html' with orders_dict=orders_dict items=items grouped_order=grouped_order %}

View file

@ -4,88 +4,77 @@ from . import views
app_name = "order" app_name = "order"
urlpatterns = [ urlpatterns = [
path("commande/", views.IndexView.as_view(), name="index"), path("", views.IndexView.as_view(), name="index"),
path( path(
"commande/<str:code>/", "<str:code>/",
views.GroupedOrderDetailView.as_view(), views.GroupedOrderDetailView.as_view(),
name="grouped_order_detail", name="grouped_order_detail",
), ),
path( path(
"commande/<str:code>/ics/", "<str:code>/ics/",
views.GroupedOrderEventView.as_view(), views.GroupedOrderEventView.as_view(),
name="grouped_order_event", name="grouped_order_event",
), ),
path( path(
"commande/<str:code>/gerer", "<str:code>/gerer",
views.GroupedOrderOverview.as_view(), views.GroupedOrderOverview.as_view(),
name="grouped_order_overview", name="grouped_order_overview",
), ),
path("commande/<str:code>/commander/", views.place_order, name="order"), path("<str:code>/commander/", views.place_order, name="order"),
path( path(
"commande/<str:code>/<int:pk>/confirmation/", "<str:code>/<int:pk>/confirmation/",
views.OrderDetailView.as_view(), views.OrderDetailView.as_view(),
name="order_confirm", name="order_confirm",
), ),
path( path(
"commande/<str:code>/gerer/<int:pk>/supprimer", "<str:code>/gerer/<int:pk>/supprimer",
views.OrderDeleteView.as_view(), views.OrderDeleteView.as_view(),
name="order_delete", name="order_delete",
), ),
path("creer", views.GroupedOrderCreateView.as_view(), name="create_grouped_order"),
path( path(
"commande/creer", "<str:code>/gerer-produits",
views.GroupedOrderCreateView.as_view(),
name="create_grouped_order",
),
path(
"commande/<str:code>/gerer-produits",
views.GroupedOrderAddItemsView.as_view(), views.GroupedOrderAddItemsView.as_view(),
name="manage_items", name="manage_items",
), ),
path( path(
"commande/<str:code>/modifier", "<str:code>/modifier",
views.GroupedOrderUpdateView.as_view(), views.GroupedOrderUpdateView.as_view(),
name="update_grouped_order", name="update_grouped_order",
), ),
path( path(
"commande/<str:code>/supprimer", "<str:code>/supprimer",
views.GroupedOrderDeleteView.as_view(), views.GroupedOrderDeleteView.as_view(),
name="delete_grouped_order", name="delete_grouped_order",
), ),
path( path(
"commande/<str:code>/dupliquer", "<str:code>/dupliquer",
views.GroupedOrderDuplicateView.as_view(), views.GroupedOrderDuplicateView.as_view(),
name="duplicate_grouped_order", name="duplicate_grouped_order",
), ),
path( path(
"commande/<str:code>/gerer-produits/nouveau", "<str:code>/gerer-produits/nouveau",
views.ItemCreateView.as_view(), views.ItemCreateView.as_view(),
name="item_create", name="item_create",
), ),
path( path(
"commande/<str:code>/gerer-produits/<int:pk>/supprimer", "<str:code>/gerer-produits/<int:pk>/supprimer",
views.ItemDeleteView.as_view(), views.ItemDeleteView.as_view(),
name="item_delete", name="item_delete",
), ),
path( path(
"commande/<str:code>/gerer/imprimer", "<str:code>/gerer/imprimer",
views.DownloadGroupedOrderSheetView.as_view(), views.DownloadGroupedOrderSheetView.as_view(),
name="grouped_order_sheet", name="grouped_order_sheet",
), ),
path( path(
"commande/<str:code>/gerer/liste-mails", "<str:code>/gerer/liste-mails",
views.ExportGroupOrderEmailAdressesToDownloadView.as_view(), views.ExportGroupOrderEmailAdressesToDownloadView.as_view(),
name="email_list", name="email_list",
), ),
path( path(
"commande/<str:code>/gerer/csv", "<str:code>/gerer/csv",
views.ExportGroupedOrderToCSVView.as_view(), views.ExportGroupedOrderToCSVView.as_view(),
name="grouped_order_csv_export", name="grouped_order_csv_export",
), ),
path("lieu/", views.PlaceIndexView.as_view(), name="place_index"),
path(
"lieu/<str:code>/",
views.PlaceUpdateView.as_view(),
name="place_update",
),
path("lieu/creer", views.PlaceCreateView.as_view(), name="place_create"),
] ]

View file

@ -16,4 +16,3 @@ from .grouped_order import (
) )
from .item import ItemCreateView, ItemDeleteView from .item import ItemCreateView, ItemDeleteView
from .order import OrderDeleteView, OrderDetailView, place_order from .order import OrderDeleteView, OrderDetailView, place_order
from .place import PlaceCreateView, PlaceIndexView, PlaceUpdateView

View file

@ -11,8 +11,10 @@ from django.views import generic
from django_weasyprint import WeasyTemplateResponseMixin from django_weasyprint import WeasyTemplateResponseMixin
from icalendar import Calendar, Event, vCalAddress, vText from icalendar import Calendar, Event, vCalAddress, vText
from la_chariotte.lieu.models import Lieu
from ..forms import GroupedOrderForm, Item, JoinGroupedOrderForm from ..forms import GroupedOrderForm, Item, JoinGroupedOrderForm
from ..models import GroupedOrder, OrderAuthor, Place from ..models import GroupedOrder, OrderAuthor
from .mixins import UserIsOrgaMixin from .mixins import UserIsOrgaMixin
@ -75,8 +77,7 @@ class GroupedOrderEventView(generic.DetailView):
event.add("dtstart", self.object.delivery_date) event.add("dtstart", self.object.delivery_date)
event.add("dtend", self.object.delivery_date) event.add("dtend", self.object.delivery_date)
event.add("date", self.object.delivery_date) event.add("date", self.object.delivery_date)
# TODO event.add("location", vText(self.object.place))
# event.add("location", vText(self.object.place))
description = "" description = ""
if self.object.delivery_slot: if self.object.delivery_slot:
@ -137,7 +138,7 @@ class GroupedOrderDetailView(generic.DetailView):
"order_author": order_author, "order_author": order_author,
# Used to set if the phone is required in the form # Used to set if the phone is required in the form
"is_phone_mandatory": grouped_order.is_phone_mandatory, "is_phone_mandatory": grouped_order.is_phone_mandatory,
"places": grouped_order.places.all(), "placekey": grouped_order.placekey.all(),
} }
) )
return context return context
@ -202,7 +203,7 @@ class GroupedOrderUpdateView(UserIsOrgaMixin, generic.UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(GroupedOrderUpdateView, self).get_context_data(**kwargs) context = super(GroupedOrderUpdateView, self).get_context_data(**kwargs)
context["places"] = Place.objects.filter(orga=self.request.user) context["places"] = Lieu.objects.filter(orga=self.request.user)
return context return context
@ -221,11 +222,10 @@ class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
orga=self.request.user, orga=self.request.user,
delivery_date=initial_grouped_order.delivery_date, delivery_date=initial_grouped_order.delivery_date,
deadline=initial_grouped_order.deadline, deadline=initial_grouped_order.deadline,
place=initial_grouped_order.place,
description=initial_grouped_order.description, description=initial_grouped_order.description,
) )
# duplicate the places set
new_grouped_order.places.set(initial_grouped_order.places.all())
# create a unique code for the new grouped order # create a unique code for the new grouped order
new_grouped_order.create_code_from_pk() new_grouped_order.create_code_from_pk()
new_grouped_order.save() new_grouped_order.save()
@ -304,6 +304,8 @@ class GroupedOrderExportView(UserIsOrgaMixin, generic.DetailView):
context = super(GroupedOrderExportView, self).get_context_data(**kwargs) context = super(GroupedOrderExportView, self).get_context_data(**kwargs)
grouped_order = self.get_object() grouped_order = self.get_object()
by_lieu = len(grouped_order.placekey.all()) > 0
items = [ items = [
item item
for item in grouped_order.item_set.all().order_by("name") for item in grouped_order.item_set.all().order_by("name")
@ -324,14 +326,14 @@ class GroupedOrderExportView(UserIsOrgaMixin, generic.DetailView):
context["items"] = items context["items"] = items
context["orders_dict"] = orders_dict context["orders_dict"] = orders_dict
context["by_lieu"] = by_lieu
if grouped_order.has_places: if by_lieu:
places = dict() lieux = dict()
for order, order_items in orders_dict.items(): for order, order_items in orders_dict.items():
if order.place.name not in places: if order.placekey.name not in lieux:
lieux[order.place.name] = dict() lieux[order.placekey.name] = dict()
lieux[order.place.name][order] = order_items lieux[order.placekey.name][order] = order_items
context["places"] = places context["lieux"] = lieux
return context return context
@ -372,6 +374,7 @@ class ExportGroupOrderEmailAdressesToDownloadView(UserPassesTestMixin, generic.V
class ExportGroupedOrderToCSVView(GroupedOrderExportView): class ExportGroupedOrderToCSVView(GroupedOrderExportView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
grouped_order = self.get_object() grouped_order = self.get_object()
enableLieu = len(grouped_order.placekey.all()) > 0
super(ExportGroupedOrderToCSVView, self).get(self, request, *args, **kwargs) super(ExportGroupedOrderToCSVView, self).get(self, request, *args, **kwargs)
context = self.get_context_data() context = self.get_context_data()
@ -393,7 +396,7 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView):
row.append("Note") row.append("Note")
row.append("Date") row.append("Date")
row.append("Heure") row.append("Heure")
if grouped_order.has_places: if enableLieu:
row.append("Lieu") row.append("Lieu")
writer.writerow(row) writer.writerow(row)
@ -417,8 +420,8 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView):
row.append(order.note) row.append(order.note)
row.append(order.created_date.strftime("%d/%m/%Y")) row.append(order.created_date.strftime("%d/%m/%Y"))
row.append(order.created_date.strftime("%H:%M")) row.append(order.created_date.strftime("%H:%M"))
if grouped_order.has_places: if enableLieu:
row.append(order.place.name) row.append(order.placekey.name)
writer.writerow(row) writer.writerow(row)
# write total row # write total row

View file

@ -5,14 +5,16 @@ 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 la_chariotte.lieu.models import Lieu
from la_chariotte.mail.utils import send_order_confirmation_mail from la_chariotte.mail.utils import send_order_confirmation_mail
from ..models import GroupedOrder, Order, OrderAuthor, OrderedItem, Place from ..models import GroupedOrder, Order, OrderAuthor, OrderedItem
def place_order(request, code): def place_order(request, code):
# Creates an AnonymousUser and an Order with related OrderedItems # Creates an AnonymousUser and an Order with related OrderedItems
grouped_order = get_object_or_404(GroupedOrder, code=code) grouped_order = get_object_or_404(GroupedOrder, code=code)
places = grouped_order.placekey.all()
# Handle permissions # Handle permissions
user_is_orga = request.user == grouped_order.orga user_is_orga = request.user == grouped_order.orga
@ -47,21 +49,17 @@ def place_order(request, code):
email = request.POST["email"] email = request.POST["email"]
note = request.POST["note"] note = request.POST["note"]
# Make sure requested place is valid in this group order (and exists at all) # Make sure requested location is valid in this group order (and exists at all)
# If no places are enabled for this group order, chosen place is always None # If no placekey is enabled for this group order, placekey is always null
if grouped_order.has_places: if len(places) == 0:
places = grouped_order.places.all() placekey = None
# Return 404 if the requested place does not exist at all else:
place = get_object_or_404(Place, code=request.POST["place"]) placekey = get_object_or_404(Lieu, url=request.POST["placekey"])
if place not in places: if placekey not in places:
# Return 404 is the requested place exists but is not enabled for this
# GroupedOrder instance
raise http.Http404( raise http.Http404(
"Le lieu demandé n'est pas valide pour cette commande: %s" "Le lieu demandé n'est pas valide pour cette commande: %s"
% request.POST["place"] % requested_placekey
) )
else:
place = None
author = OrderAuthor.objects.create( author = OrderAuthor.objects.create(
first_name=first_name, last_name=last_name, email=email, phone=phone first_name=first_name, last_name=last_name, email=email, phone=phone
@ -71,7 +69,7 @@ def place_order(request, code):
grouped_order=grouped_order, grouped_order=grouped_order,
note=note, note=note,
created_date=timezone.now(), created_date=timezone.now(),
place=place, placekey=placekey,
) )
# add items to the order # add items to the order
@ -98,7 +96,7 @@ def place_order(request, code):
"error_message": error_message, "error_message": error_message,
"note": order.note, "note": order.note,
"author": author, "author": author,
"place": place, "placekey": placekey,
}, },
) )
@ -117,7 +115,7 @@ def place_order(request, code):
"error_message": error_message, "error_message": error_message,
"note": order.note, "note": order.note,
"author": author, "author": author,
"place": place, "placekey": placekey,
}, },
) )

View file

@ -1,83 +0,0 @@
import csv
import json
from django import http
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.views import generic
from django_weasyprint import WeasyTemplateResponseMixin
from icalendar import Calendar, Event, vCalAddress, vText
from la_chariotte.order.models import GroupedOrder
from la_chariotte.order.views.mixins import UserIsOrgaMixin
from ..forms import PlaceForm
from ..models import Place
class PlaceIndexView(LoginRequiredMixin, generic.ListView):
"""View showing all the distribution places managed by the authenticated user"""
template_name = "place/index.html"
context_object_name = "context"
def get_queryset(self):
places = Place.objects.filter(orga=self.request.user)
print(places)
# Let's filter orders by distribution place (for UI grouping)
orders = dict()
for place in places:
# TODO: maybe filter out finished GroupedOrder?
if place.orders.all():
orders[place.code] = place.orders.all()
# orders[place.code] = orders
return {
"places": places,
"orders": orders,
}
class PlaceUpdateView(UserIsOrgaMixin, generic.UpdateView):
"""View showing details and allowing updates to a distribution place"""
model = Place
template_name = "place/place_update.html"
context_object_name = "place"
form_class = PlaceForm
# Prevent URL change after creation
form_class.base_fields["code"].disabled = True
def get_object(self, queryset=None):
return get_object_or_404(Place, code=self.kwargs.get("code"))
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
class PlaceCreateView(LoginRequiredMixin, generic.CreateView):
"""View for creating a new distribution place"""
model = Place
form_class = PlaceForm
template_name = "place/place_create.html"
# Allow setting URL for creation
form_class.base_fields["code"].disabled = False
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
def form_valid(self, form):
self.object = form.save()
return super().form_valid(form)

View file

@ -27,6 +27,7 @@ if os.getenv("ALLOWED_HOSTS"):
# Applications & middlewares # Applications & middlewares
INSTALLED_APPS = [ INSTALLED_APPS = [
"la_chariotte.lieu",
"la_chariotte.order", "la_chariotte.order",
"la_chariotte.accounts", "la_chariotte.accounts",
"la_chariotte.mail", "la_chariotte.mail",

View file

@ -920,7 +920,7 @@ class TestGroupedOrderUpdateView:
grouped_order.save() grouped_order.save()
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "gr order test" assert models.GroupedOrder.objects.first().name == "gr order test"
assert models.GroupedOrder.objects.first().places.count() == 0 assert models.GroupedOrder.objects.first().place == None
# get the update form # get the update form
update_grouped_order_url = reverse( update_grouped_order_url = reverse(
@ -942,7 +942,7 @@ class TestGroupedOrderUpdateView:
) )
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "gr order test" assert models.GroupedOrder.objects.first().name == "gr order test"
assert models.GroupedOrder.objects.first().places.count() == 0 assert models.GroupedOrder.objects.first().place == None
# get the update form # get the update form
update_grouped_order_url = reverse( update_grouped_order_url = reverse(
@ -954,9 +954,6 @@ class TestGroupedOrderUpdateView:
# post the update form # post the update form
date = timezone.now().date() + datetime.timedelta(days=42) date = timezone.now().date() + datetime.timedelta(days=42)
deadline = timezone.now() + datetime.timedelta(days=32) deadline = timezone.now() + datetime.timedelta(days=32)
place = models.Place.objects.create(
code="foobar", name="quelque part", orga=auth.get_user(client_log)
)
response = client_log.post( response = client_log.post(
update_grouped_order_url, update_grouped_order_url,
{ {
@ -964,15 +961,14 @@ class TestGroupedOrderUpdateView:
"deadline_date": deadline.date(), "deadline_date": deadline.date(),
"deadline_time": deadline.time().strftime("%H:%M"), "deadline_time": deadline.time().strftime("%H:%M"),
"delivery_date": date, "delivery_date": date,
"places": place.id, "place": "quelque part",
}, },
) )
# assert response.content.decode() == ""
assert response.status_code == 302 assert response.status_code == 302
assert response.url.endswith("gerer-produits") assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "titre test modifié" assert models.GroupedOrder.objects.first().name == "titre test modifié"
assert models.GroupedOrder.objects.first().places.first().name == "quelque part" assert models.GroupedOrder.objects.first().place == "quelque part"
def test_update_grouped_order__delivery_date_passed(self, client_log): def test_update_grouped_order__delivery_date_passed(self, client_log):
""" """
@ -987,7 +983,7 @@ class TestGroupedOrderUpdateView:
) )
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "gr order test" assert models.GroupedOrder.objects.first().name == "gr order test"
assert models.GroupedOrder.objects.first().places.count() == 0 assert models.GroupedOrder.objects.first().place == None
# get the update form # get the update form
update_grouped_order_url = reverse( update_grouped_order_url = reverse(
@ -999,9 +995,6 @@ class TestGroupedOrderUpdateView:
# post the update form # post the update form
date = timezone.now().date() + datetime.timedelta(days=-1) date = timezone.now().date() + datetime.timedelta(days=-1)
deadline = timezone.now() + datetime.timedelta(days=-3) deadline = timezone.now() + datetime.timedelta(days=-3)
place = models.Place.objects.create(
code="foobar", name="quelque part", orga=auth.get_user(client_log)
)
response = client_log.post( response = client_log.post(
update_grouped_order_url, update_grouped_order_url,
{ {
@ -1009,14 +1002,14 @@ class TestGroupedOrderUpdateView:
"deadline_date": deadline.date(), "deadline_date": deadline.date(),
"deadline_time": deadline.time().strftime("%H:%M"), "deadline_time": deadline.time().strftime("%H:%M"),
"delivery_date": date, "delivery_date": date,
"places": place.id, "place": "quelque part",
}, },
) )
assert response.status_code == 302 assert response.status_code == 302
assert response.url.endswith("gerer-produits") assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "titre test modifié" assert models.GroupedOrder.objects.first().name == "titre test modifié"
assert models.GroupedOrder.objects.first().places.first().name == "quelque part" assert models.GroupedOrder.objects.first().place == "quelque part"
def test_update_grouped_order__not_orga(self, client_log, other_user): def test_update_grouped_order__not_orga(self, client_log, other_user):
"""A user that is not organiszer of the GO accesses update page. """A user that is not organiszer of the GO accesses update page.
@ -1322,9 +1315,7 @@ class TestGroupedOrderDuplicateView:
assert new_grouped_order.name == "gr order test - copie" assert new_grouped_order.name == "gr order test - copie"
assert new_grouped_order.delivery_date == grouped_order.delivery_date assert new_grouped_order.delivery_date == grouped_order.delivery_date
assert new_grouped_order.deadline == grouped_order.deadline assert new_grouped_order.deadline == grouped_order.deadline
assert [x.code for x in new_grouped_order.places.all()] == [ assert new_grouped_order.place == grouped_order.place
x.code for x in grouped_order.places.all()
]
assert new_grouped_order.orga == auth.get_user(client_log) assert new_grouped_order.orga == auth.get_user(client_log)
assert new_grouped_order.description == grouped_order.description assert new_grouped_order.description == grouped_order.description
assert new_grouped_order.item_set.count() == grouped_order.item_set.count() assert new_grouped_order.item_set.count() == grouped_order.item_set.count()

View file

@ -29,8 +29,8 @@ from la_chariotte.order.views.stats import stats
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
# No route specified because there are multiple subroutes defined in order.urls path("commande/", include("la_chariotte.order.urls")),
path("", include("la_chariotte.order.urls")), path("lieu/", include("la_chariotte.lieu.urls")),
path("comptes/", include("la_chariotte.accounts.urls")), path("comptes/", include("la_chariotte.accounts.urls")),
# Some paths for accounts are easier to leave here # Some paths for accounts are easier to leave here
# - PasswordResetView sends the mail # - PasswordResetView sends the mail