Compare commits

..

3 commits

Author SHA1 Message Date
selfhoster1312
4eac4f43b8 feature: Add place relationship for distribution spots 2025-03-03 21:24:43 +01:00
Laetitia
19c4a0b6ed feat: update gitlab links everywhere else 2025-02-16 10:35:00 +00:00
Laetitia
9589bcf48d feat: update gitlab link to framagit 2025-02-16 10:35:00 +00:00
33 changed files with 342 additions and 370 deletions

View file

@ -6,7 +6,7 @@ started.
The first step is to clone the project, you can find more information about that in
the [getting started guide](../install.md). Once that's done, you can:
- choose a task [on the board](https://gitlab.com/la-chariotte/la_chariotte/-/boards) and assign it to
- choose a task [on the board](https://framagit.org/la-chariotte/la-chariotte/-/boards) and assign it to
yourself - if you don't know which task to do, feel free to reach to
us.
- create a new branch **from develop** naming it to reflect what you want to do

View file

@ -11,7 +11,7 @@
- **docs.chariotte.fr**, the docs you are reading now. It's handled by [readthedocs.org](https://readthedocs.org).
- **chariotte.fr**, the main instance. It's deployed on Alwaysdata
- **blog.chariotte.fr**, our blog. It's [a static website](https://gitlab.com/la-chariotte/la-chariotte.gitlab.io) deployed on Gitlab pages.
- **blog.chariotte.fr**, our blog. It's [a static website](https://framagit.org/la-chariotte/la-chariotte.frama.io) deployed on Gitlab pages.
## The main instance

View file

@ -1,7 +1,7 @@
First, clone the project
```bash
git clone git@gitlab.com:la-chariotte/la_chariotte.git
git clone https://framagit.org/la-chariotte/la-chariotte.git
```
## Virtual environment

View file

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

View file

@ -1,35 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,22 +0,0 @@
{% 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

@ -1,14 +0,0 @@
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

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

View file

@ -1,87 +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 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> :
le {{ order.grouped_order.delivery_date }}{% if order.grouped_order.delivery_slot %}, {{ order.grouped_order.delivery_slot }}{% endif %}
{% if order.grouped_order.place %}<br>Lieu : {{ order.grouped_order.place }}{% endif %}
{% if order.place %}<br>Lieu : {{ order.place }}{% endif %}
</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> :

View file

@ -6,8 +6,7 @@ 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
from la_chariotte.order.models import GroupedOrder, Item
from la_chariotte.order.models import GroupedOrder, Item, Place
class GroupedOrderForm(forms.ModelForm):
@ -24,9 +23,10 @@ class GroupedOrderForm(forms.ModelForm):
label="Numéro de téléphone obligatoire pour les participants",
required=False,
)
placekey = forms.ModelMultipleChoiceField(
places = forms.ModelMultipleChoiceField(
label="Lieux de distribution",
queryset=Lieu.objects.all(),
# TODO: filter own places
queryset=Place.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
@ -39,10 +39,9 @@ class GroupedOrderForm(forms.ModelForm):
"deadline_time",
"delivery_date",
"delivery_slot",
"place",
"places",
"description",
"is_phone_mandatory",
"placekey",
]
widgets = {
"name": forms.TextInput(
@ -55,7 +54,6 @@ class GroupedOrderForm(forms.ModelForm):
"delivery_slot": forms.TextInput(
attrs={"placeholder": "14h - 17h (facultatif)"}
),
"place": forms.TextInput(attrs={"placeholder": "(facultatif)"}),
"description": forms.Textarea(
attrs={
"placeholder": "Plus d'infos sur la commande groupée ? (facultatif)"
@ -112,3 +110,32 @@ class JoinGroupedOrderForm(forms.Form):
"Désolé, nous ne trouvons aucune commande avec ce 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

@ -1,23 +0,0 @@
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

@ -0,0 +1,75 @@
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,14 +22,9 @@ class GroupedOrder(models.Model):
)
deadline = models.DateTimeField("Date limite de commande")
# Try to associate with a saved distribution place in DB
placekey = models.ManyToManyField(
"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"
# Associate with zero/more saved distribution places
places = models.ManyToManyField(
"order.Place", verbose_name="Lieux de distribution", related_name="orders"
)
description = models.TextField("Description", null=True, blank=True)
@ -62,6 +57,13 @@ class GroupedOrder(models.Model):
)
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
def total_price(self):
price = 0
@ -131,8 +133,8 @@ class Order(models.Model):
author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE)
created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True)
note = models.TextField(max_length=200, null=True, blank=True)
placekey = models.ForeignKey(
"lieu.lieu", on_delete=models.CASCADE, null=True, blank=True
place = models.ForeignKey(
"order.place", on_delete=models.CASCADE, null=True, blank=True
)
@property
@ -207,3 +209,22 @@ class OrderedItem(models.Model):
def __str__(self): # pragma: no cover
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,10 +27,6 @@
{% endif %}
</div>
<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"
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' }}
@ -171,16 +167,15 @@
<p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label>
<textarea id="note" rows=3 name="note">{{ note }}</textarea></p>
{% if placekey %}
<label for="placekey">Point de distribution:</label>
<select name="placekey" id="placekey-select">
{% for place in placekey %}
<option value="{{ place.url }}">{{ place.name }}</option>
{% endfor %}
</select>
{% endif %}
<hr>
{% if places %}
<label for="place">Point de distribution:</label>
<select name="place" id="place-select">
{% for place in places %}
<option value="{{ place.code }}">{{ place.name }}</option>
{% endfor %}
</select>
{% endif %}
<hr>
<div class="buttons">
<button id="submit" type="submit" value="Order" class="button is-primary">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,83 @@
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

@ -7,7 +7,7 @@ from sentry_sdk.integrations.django import DjangoIntegration
BASE_DIR = Path(__file__).resolve().parent.parent
BASE_URL = os.getenv("BASE_URL", "http://127.0.0.1:8000")
PROJECT_NAME = os.getenv("PROJECT_NAME", "La Chariotte")
GITLAB_LINK = "https://gitlab.com/la-chariotte/la_chariotte"
GITLAB_LINK = "https://framagit.org/la-chariotte/la-chariotte"
CONTACT_MAIL = "contact@chariotte.fr"
HELLOASSO_LINK = "https://www.helloasso.com/associations/la-chariotte/"
FEEDBACK_LINK = "https://framaforms.org/votre-avis-sur-la-chariotte-1709742328"
@ -27,7 +27,6 @@ if os.getenv("ALLOWED_HOSTS"):
# Applications & middlewares
INSTALLED_APPS = [
"la_chariotte.lieu",
"la_chariotte.order",
"la_chariotte.accounts",
"la_chariotte.mail",

View file

@ -920,7 +920,7 @@ class TestGroupedOrderUpdateView:
grouped_order.save()
assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "gr order test"
assert models.GroupedOrder.objects.first().place == None
assert models.GroupedOrder.objects.first().places.count() == 0
# get the update form
update_grouped_order_url = reverse(
@ -942,7 +942,7 @@ class TestGroupedOrderUpdateView:
)
assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "gr order test"
assert models.GroupedOrder.objects.first().place == None
assert models.GroupedOrder.objects.first().places.count() == 0
# get the update form
update_grouped_order_url = reverse(
@ -954,6 +954,9 @@ class TestGroupedOrderUpdateView:
# post the update form
date = timezone.now().date() + datetime.timedelta(days=42)
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(
update_grouped_order_url,
{
@ -961,14 +964,15 @@ class TestGroupedOrderUpdateView:
"deadline_date": deadline.date(),
"deadline_time": deadline.time().strftime("%H:%M"),
"delivery_date": date,
"place": "quelque part",
"places": place.id,
},
)
# assert response.content.decode() == ""
assert response.status_code == 302
assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "titre test modifié"
assert models.GroupedOrder.objects.first().place == "quelque part"
assert models.GroupedOrder.objects.first().places.first().name == "quelque part"
def test_update_grouped_order__delivery_date_passed(self, client_log):
"""
@ -983,7 +987,7 @@ class TestGroupedOrderUpdateView:
)
assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "gr order test"
assert models.GroupedOrder.objects.first().place == None
assert models.GroupedOrder.objects.first().places.count() == 0
# get the update form
update_grouped_order_url = reverse(
@ -995,6 +999,9 @@ class TestGroupedOrderUpdateView:
# post the update form
date = timezone.now().date() + datetime.timedelta(days=-1)
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(
update_grouped_order_url,
{
@ -1002,14 +1009,14 @@ class TestGroupedOrderUpdateView:
"deadline_date": deadline.date(),
"deadline_time": deadline.time().strftime("%H:%M"),
"delivery_date": date,
"place": "quelque part",
"places": place.id,
},
)
assert response.status_code == 302
assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "titre test modifié"
assert models.GroupedOrder.objects.first().place == "quelque part"
assert models.GroupedOrder.objects.first().places.first().name == "quelque part"
def test_update_grouped_order__not_orga(self, client_log, other_user):
"""A user that is not organiszer of the GO accesses update page.
@ -1315,7 +1322,9 @@ class TestGroupedOrderDuplicateView:
assert new_grouped_order.name == "gr order test - copie"
assert new_grouped_order.delivery_date == grouped_order.delivery_date
assert new_grouped_order.deadline == grouped_order.deadline
assert new_grouped_order.place == grouped_order.place
assert [x.code for x in new_grouped_order.places.all()] == [
x.code for x in grouped_order.places.all()
]
assert new_grouped_order.orga == auth.get_user(client_log)
assert new_grouped_order.description == grouped_order.description
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 = [
path("admin/", admin.site.urls),
path("commande/", include("la_chariotte.order.urls")),
path("lieu/", include("la_chariotte.lieu.urls")),
# No route specified because there are multiple subroutes defined in order.urls
path("", include("la_chariotte.order.urls")),
path("comptes/", include("la_chariotte.accounts.urls")),
# Some paths for accounts are easier to leave here
# - PasswordResetView sends the mail

View file

@ -1,7 +1,7 @@
site_name: La chariotte
site_description: An application for grouped-orders
repo_name: la-chariotte/la_chariotte
repo_url: https://gitlab.com/la-chariotte/la_chariotte
repo_url: https://framagit.org/la-chariotte/la-chariotte
nav:
- How-tos:
- Getting started: install.md