Compare commits

..

No commits in common. "develop" and "1.1.0" have entirely different histories.

74 changed files with 2817 additions and 9510 deletions

4
.gitignore vendored
View file

@ -3,8 +3,10 @@ coverage.xml
.coverage
la_chariotte.egg-info/
node_modules
/static/*
/media/*
local_settings.py
.venv
*venv*
mails.sqlite
package-lock.json
static

View file

@ -26,7 +26,8 @@ tests:
- pip cache purge
- pip install -e ".[dev]"
- pytest --create-db --cov --cov-report=xml
- if [ "$CI_COMMIT_REF_NAME" = 'develop' ] ; then git fetch origin develop ; diff-cover coverage.xml --fail-under=90 --compare-branch origin/develop --diff-range-notation '..' && exit 0 ; fi
- if [ "$CI_COMMIT_REF_NAME" = 'main' ] ; then exit 0 ; fi
- if [ "$CI_COMMIT_REF_NAME" = 'develop' ] ; then git fetch origin main ; diff-cover coverage.xml --fail-under=90 --compare-branch origin/main --diff-range-notation '..' && exit 0 ; fi
- git fetch origin develop ; diff-cover coverage.xml --fail-under=90 --compare-branch origin/develop --diff-range-notation '..'
- echo "Tests done."

View file

@ -1,97 +0,0 @@
# title
Short and specific: **a clear summary of the bug**.
It should also be **searchable**, so developers can find it in a pinch.
> Tips on writing great titles:
>
> 1. Keep it simple, but descriptive.
> 2. Dont abbreviate.
> 3. Make it searchable.
> 4. Focus on the technical problem.
> 5. Highlight the specific feature/component you have issues with.
## description
Small text to describe the bug with human words.
## steps to reproduce
**Describe how you found the bug**, so the developer can try to reproduce it.
> Tips on writing reproducible steps:
>
> 1. Use a numbered list so its easy to follow.
> 2. This is your chance to be comprehensive and (reasonably) verbose. Dont leave out any details!
## expected vs. actual results
Take some time to explain what **should** happen vs. what **actually** happened.
If you just describe the bug, some people might think youre describing the expected behavior.
### expected results
### actual results
> Tips for writing expected vs. actual results:
>
> 1. Use a direct comparison format. For example, "*The button should turn green*" vs. "*The button is turning blue*".
> 2. Be precise. Simply stating "*it went wrong*" instead of *"the page loads indefinitely*" means you are leaving out very valuable information!
## visual proof/screenshot
Screenshots and annotations help developers **visualize the bug**, and pinpoint its location on the page.
> Tips on taking great screenshots:
>
> 1. Annotations go a long way towards driving your point across.
> 2. Highlight the problematic element. Dont be ambiguous.
> 3. Use big fonts, different colors, etc. - the bug needs to be even more obvious here than in your summary.
> 4. For complicated issues, record a short video! This adds a ton of helpful context when trying to reproduce bugs.
## priority
The urgency and **potential impact of the bug**. Determines how quickly it needs to be fixed.
> How to determine priority/severity:
>
> 1. Critical: blocking bugs that **directly prevent business**. Example: a checkout page not loading.
> 2. High: affects **major features**, but non-breaking. E.g.: the search bar on an e-commerce website.
> 3. Medium: noticeable bugs that **disrupt normal use**. E.g.: broken link, long loading times.
> 4. Low: **small issues** and enhancements: typos, missing images...
## environment
For developers to reproduce and fix your bug, theyll need to know your **browser version**, **screen size**, **operating system**...
Some bugs only occur within specific environments.
- source url:
- operating system:
- browser:
- viewport:
> How to find your environment info:
>
> 1. **Browser and version**: look for a “Help” or “About” option in your browsers menu.
> 2. **Operating system**: on PC, press the Windows key + Pause/break. On a Mac, click the Apple logo and choose “About this Mac”.
> 3. **Screen size**: look in your computers display settings, or search online for “screen resolution” along with your device model.
## console logs
This is where your web browser **shows errors or warnings**.
Console logs can help developers figure out what went wrong.
```log
console log
```
> How to access your console logs:
>
> 1. Right-click the page, select “Inspect” or “Inspect Element”, then click on the “Console” tab.
> 2. Try to make the bug happen again and see if any messages pop up there.
## credits https://www.perfectbugreport.io/

View file

@ -1,43 +1,4 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.3.0 (2024-11-02)
This is a small release, with a few features and bugfixes. The code is now
hosted on https://framagit.org rather than https://gitlab.com.
### Added
- Changed hour and date format in the generated CSV files
- Added the date in the grouped order dashboard
- Changed the display of the last possible date and hour to place an order
- Added the total number of ordered items in the grouped orders overview
- Removed header line and column for grouped order name in the CSV export for emails
### Fixed
- The "reset password" form is now fixed, it was broken since the last release.
## 1.2.0 (2024-07-12)
A small release, with a few features, a new footer and a `/stats` endpoint.
### Added
- Staff users are now able to see the overview page for a grouped order.
- Personal information are now autocompleted automatically
- A `/stats` endpoint has been added, making it possible to see some information
about the current instance.
- The footer has been changed to include more elements, links to the project and
documentation.
### Development
- The `main` branch is not used anymore, all the development happens on the
`develop` branch instead.
- All the tests have been grouped in the `tests` folder
- The `requirements.txt` file have been removed in favor of the `pyproject.toml` file.
- v1 :
- Basic functionalities for grouped orders

1326
LICENSE

File diff suppressed because it is too large Load diff

1213
chariotte-v0-data.sql Normal file

File diff suppressed because it is too large Load diff

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://framagit.org/la-chariotte/la-chariotte/-/boards) and assign it to
- choose a task [on the board](https://gitlab.com/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
@ -55,6 +55,16 @@ isort .
black .
```
## Working with emails
To test the appearance of emails, you can use [Sendria](https://github.com/msztolcman/sendria):
```bash
pip install sendria
sendria --db mails.sqlite
$NAVIGATOR http://127.0.0.1:1080
```
## Docs
We're using [MkDocs](https://www.mkdocs.org/) for this documentation. If you want to

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://framagit.org/la-chariotte/la-chariotte.frama.io) deployed on Gitlab pages.
- **blog.chariotte.fr**, our blog. It's [a static website](https://gitlab.com/la-chariotte/la-chariotte.gitlab.io) deployed on Gitlab pages.
## The main instance
@ -144,12 +144,9 @@ cd la_chariotte
# Get the code
git fetch
git checkout tag # if we're using a tag, otherwise, just checkout the main branch
pip install -e .
npm install
python manage.py migrate --settings=prod_settings
python manage.py compilescss --settings=prod_settings
python manage.py collectstatic --settings=prod_settings
python manage.py updatedb
python manage.py collectstatic
```
Then you'll need to restart the server from AD's interface.

View file

@ -22,21 +22,22 @@ classDiagram
name
deadline : DateTime
delivery_date : Date
delivery_slot
place
description
orga : CustomUser
total_price
}
class Item{
name
grouped_order : GroupedOrder
ordered_nb
price
total_price
max_limit
}
class Order{
grouped_order : GroupedOrder
author : OrderAuthor
price
created_date
note
}

View file

@ -1,7 +1,7 @@
First, clone the project
```bash
git clone https://framagit.org/la-chariotte/la-chariotte.git
git clone git@gitlab.com:la-chariotte/la_chariotte.git
```
## Virtual environment
@ -73,7 +73,7 @@ CREATE DATABASE chariotte
## Create a configuration file
Create a local configuration file named "local_settings.py" at the project root :
Create a local configuration file:
```python title="local_settings.py"
from la_chariotte.settings import *
@ -102,13 +102,3 @@ To create a superuser, who will have access to the admin interface (/admin):
```shell
python manage.py createsuperuser
```
## Working with emails
To test the appearance of emails, you can use [Sendria](https://github.com/msztolcman/sendria):
```bash
pip install sendria
sendria --db mails.sqlite
$NAVIGATOR http://127.0.0.1:1080
```

View file

@ -1 +1 @@
__version__ = "1.3.0"
__version__ = "1.1.0"

View file

@ -8,6 +8,6 @@
</p>
<div class="box">
<p>Votre mot de passe a été correctement réinitialisé. Vous pouvez maintenant vous connecter.</p>
<a class="button is-primary" href="{% url 'accounts:login' %}">Se connecter</a>
<button class="button is-primary" href="{% url 'accounts:login'%}">Se connecter</button>
</div>
{% endblock %}

View file

@ -9,8 +9,8 @@
{% if validlink %}
<form method="POST">
<p>Veuillez entrer un nouveau mot de passe tout neuf.</p>
{{ form | crispy }}
{% csrf_token %}
{% crispy form %}
<input class="button is-primary"
type="submit"
value="Changer le mot de passe">

View file

@ -8,8 +8,7 @@
<div class="box">
<form method="POST">
<p>Entrez votre mail pour recevoir les instructions pour le réinitialiser.</p>
{{ form | crispy }}
{% csrf_token %}
{% crispy form %}
<div class="buttons">
<input class="button is-primary" type="submit" value="Envoyer">
</div>

View file

@ -4,7 +4,6 @@ import pytest
from django.utils import timezone
from la_chariotte.order.models import GroupedOrder, Item
from la_chariotte.tests.utils import create_grouped_order
@pytest.fixture
@ -24,18 +23,6 @@ def other_user(django_user_model):
return user
@pytest.fixture
def authenticated_user_with_name(django_user_model):
username = "toto@gmail.com"
password = "tata"
first_name = "boule"
last_name = "bill"
user = django_user_model.objects.create_user(
username=username, password=password, first_name=first_name, last_name=last_name
)
return user
@pytest.fixture
def simple_grouped_order(other_user):
date = timezone.now().date() + datetime.timedelta(days=30)
@ -52,19 +39,6 @@ def simple_grouped_order(other_user):
return grouped_order
@pytest.fixture
def connected_grouped_order(client, authenticated_user_with_name):
client.force_login(authenticated_user_with_name)
grouped_order = create_grouped_order(
name="Test grouped order",
orga_user=authenticated_user_with_name,
days_before_delivery_date=30,
days_before_deadline=5,
)
item = Item.objects.create(name="test item", grouped_order=grouped_order, price=2)
return grouped_order
@pytest.fixture(autouse=True)
def password_hasher_setup(settings):
# Use a weaker password hasher during tests, for speed

View file

@ -19,7 +19,7 @@ def test_send_order_confirmation_mail(mailoutbox, simple_grouped_order, client):
"code": simple_grouped_order.code,
},
)
client.post(
response = client.post(
order_url,
{
f"quantity_{item.pk}": [4, 0],

View file

@ -18,10 +18,6 @@ class GroupedOrderForm(forms.ModelForm):
widget=forms.TimeInput(attrs={"type": "time"}),
initial=datetime.time(hour=23, minute=59, second=59),
)
is_phone_mandatory = forms.BooleanField(
label="Numéro de téléphone obligatoire pour les participants",
required=False,
)
class Meta:
model = GroupedOrder
@ -33,7 +29,6 @@ class GroupedOrderForm(forms.ModelForm):
"delivery_slot",
"place",
"description",
"is_phone_mandatory",
]
widgets = {
"name": forms.TextInput(

View file

@ -1,36 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-31 21:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("order", "0026_groupedorder_delivery_slot"),
]
operations = [
migrations.RemoveField(
model_name="groupedorder",
name="total_price",
),
migrations.RemoveField(
model_name="item",
name="ordered_nb",
),
migrations.RemoveField(
model_name="order",
name="articles_nb",
),
migrations.RemoveField(
model_name="order",
name="price",
),
migrations.AlterField(
model_name="order",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, verbose_name="Date et heure de commande"
),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.2.16 on 2024-12-08 15:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("order", "0027_remove_groupedorder_total_price_and_more"),
]
operations = [
migrations.AddField(
model_name="groupedorder",
name="is_phone_mandatory",
field=models.BooleanField(
default=False, verbose_name="Numéro de téléphone obligatoire"
),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.2.16 on 2024-12-08 15:41
from django.db import migrations
def set_phone_mandatory(apps, schema_editor):
"""For continuity, force mandatory phone for the orders that were created so far."""
GroupedOrder = apps.get_model("order", "GroupedOrder")
for grouped_order in GroupedOrder.objects.all():
grouped_order.is_phone_mandatory = True
grouped_order.save()
class Migration(migrations.Migration):
dependencies = [
("order", "0028_groupedorder_is_phone_mandatory"),
]
operations = [
migrations.RunPython(set_phone_mandatory),
]

View file

@ -25,10 +25,8 @@ class GroupedOrder(models.Model):
max_length=100, null=True, blank=True, verbose_name="Lieu de livraison"
)
description = models.TextField("Description", null=True, blank=True)
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
code = models.CharField(auto_created=True)
is_phone_mandatory = models.BooleanField(
default=False, verbose_name="Numéro de téléphone obligatoire"
)
def create_code_from_pk(self):
"""When a grouped order is created, a unique code is generated, to be used to
@ -54,18 +52,16 @@ class GroupedOrder(models.Model):
)
self.code = f"{base_36_pk}{random_string}"[:code_length]
@property
def total_price(self):
def compute_total_price(self):
price = 0
for order in self.order_set.all():
price += order.price
return price
self.total_price = price
self.save()
def get_total_ordered_items(self):
total_nb = 0
def compute_items_ordered_nb(self):
for item in self.item_set.all():
total_nb += item.ordered_nb
return total_nb
item.compute_ordered_nb()
def is_open(self):
return self.deadline >= timezone.now()
@ -101,6 +97,8 @@ class GroupedOrder(models.Model):
class OrderAuthor(models.Model):
"""Used when a user orders (with or without an account)"""
# TODO faire le lien avec CustomUser (CustomUser hérite de OrderAuthor),
# pour ensuite préremplir quand on est connecté·e
first_name = models.CharField(verbose_name="Prénom")
last_name = models.CharField(verbose_name="Nom")
phone = models.CharField(
@ -121,24 +119,26 @@ class Order(models.Model):
GroupedOrder, on_delete=models.CASCADE, related_name="order_set"
)
author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE)
created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True)
articles_nb = models.PositiveIntegerField(default=0)
price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
created_date = models.DateTimeField("Date de la commande", auto_now_add=True)
note = models.TextField(max_length=200, null=True, blank=True)
@property
def articles_nb(self):
def compute_order_articles_nb(self):
"""Computes the number of articles in this order"""
articles_nb = 0
for ord_item in self.ordered_items.all():
articles_nb += ord_item.nb
return articles_nb
self.articles_nb = articles_nb
self.save()
@property
def price(self):
def compute_order_price(self):
"""Computes the total price of the order"""
price = 0
for ord_item in self.ordered_items.all():
price += ord_item.get_price()
return price
self.price = price
self.save()
def __str__(self): # pragma: no cover
return (
@ -153,13 +153,15 @@ class Item(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2)
max_limit = models.PositiveSmallIntegerField(null=True, blank=True)
@property
def ordered_nb(self):
ordered_nb = models.IntegerField(default=0)
def compute_ordered_nb(self):
"""Computes the number of times this item has been ordered"""
ordered_nb = 0
for order in self.orders.all():
ordered_nb += order.nb
return ordered_nb
self.ordered_nb = ordered_nb
self.save()
def get_total_price(self):
"""Returns the total price of all orders on this item"""

View file

@ -1,7 +1,5 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Nouvelle commande groupée{% endblock %}
{% block content %}
@ -12,9 +10,8 @@
<p class="title">Nouvelle commande groupée</p>
<div class="columns">
<div class="column is-8">
<form method="post" onsubmit="deadlinePassedCheck(event)">
{% csrf_token %}
{{ form | crispy }}
<form method="post" onsubmit="deadlinePassedCheck(event)">{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<a class="button is-light" href="{% url 'order:index' %}">Annuler</a>
<input class="button is-primary" type="submit" value="Suivant">

View file

@ -33,7 +33,7 @@
{% 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' }}
Commandes avant le {{ grouped_order.deadline }}
</p>
<p><i class="fa fa-truck mr-3" aria-label="Date de livraison" title="Date de livraison"
aria-hidden="true"></i>
@ -77,8 +77,8 @@
<p>Il n'y a pas de produits disponibles dans cette commande !</p>
{% else %}
<p class="title">Commander</p>
<form id="inputs-parent" method="post" action="{% url 'order:order' grouped_order.code %}" onSubmit="document.getElementById('submit').disabled=true;">
{% csrf_token %}
<form id="inputs-parent" method="post" action="{% url 'order:order' grouped_order.code %}">
<!-- Tableau de commandes - sur ordi -->
<table class="table is-hidden-touch">
<thead>
@ -91,7 +91,7 @@
<tbody>
{% for item in grouped_order.item_set.all %}
<tr>
{% csrf_token %}
<td>{{ item.name }}</td>
<td>{{ item.price }} €</td>
<td><input name="quantity_{{ item.id }}" size="4" type="number" value="0" min="0"
@ -152,27 +152,25 @@
<div class="column">
<p><label for="first_name">Prénom : </label>
<input id="first_name" type="text" name="first_name" placeholder="Votre prénom"
value="{{ order_author.first_name }}" required></p>
value="{{ author.first_name }}" required></p>
<p><label for="first_name">Nom : </label>
<input id="last_name" type="text" name="last_name" placeholder="Votre nom"
value="{{ order_author.last_name }}" required></p>
value="{{ author.last_name }}" required></p>
</div>
<div class="column">
<p><label for="phone">Numéro de téléphone {% if not is_phone_mandatory %}<em>(facultatif)</em> {% endif %}:</label>
<input id="phone" type="tel" pattern="[0-9]{10}"
placeholder="0601020304" name="phone"
value="{{ order_author.phone }}"
{% if is_phone_mandatory %}required{% endif %}></p>
<p><label for="phone">Numéro de téléphone :</label>
<input id="phone" type="tel" pattern="[0-9]{10}" placeholder="0601020304" name="phone"
value="{{ author.phone }}" required></p>
<p><label for="email">Adresse mail : </label>
<input id="email" type="email" placeholder="exemple@mail.fr" name="email"
value="{{ order_author.email }}" required></p>
value="{{ author.email }}" required></p>
</div>
</div>
<p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label>
<textarea id="note" rows=3 name="note">{{ note }}</textarea></p>
<p><label for="note">Note à l'organisateur·ice</label>
<textarea id="note" rows=3 placeholder="(facultatif)" name="note">{{ note }}</textarea></p>
<div class="buttons">
<button id="submit" type="submit" value="Order" class="button is-primary">
<button type="submit" value="Order" class="button is-primary">
<i class="fa fa-shopping-basket mr-3" aria-hidden="true"></i>Commander
</button>
<p>Total : <span class="total">0</span></p>
@ -188,6 +186,7 @@
// Compute total price whenever a value in input is modified
document.getElementById("inputs-parent").addEventListener("change", function () {
inputs = [...document.getElementsByTagName("input")].filter(input => input.getAttribute("name").indexOf("quantity_") === 0); //filter the inputs to get the quantity inputs only
prices = {{ prices_dict | safe }};
let total_price = 0;

View file

@ -25,7 +25,7 @@
<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' }}
Commandes avant le {{ grouped_order.deadline }}
</p>
<p><i class="fa fa-truck mr-3" aria-label="Date de livraison" title="Date de livraison" aria-hidden="true"></i>
Livraison le {{ grouped_order.delivery_date }}{% if grouped_order.delivery_slot %}, {{ grouped_order.delivery_slot }}{% endif %}
@ -90,10 +90,10 @@
<div class="column">
<p>Pour vous aider à distribuer les produits le jour J, vous pouvez télécharger la liste des commandes
au format PDF pour l'<strong>imprimer</strong>, ou au format CSV pour l'<strong>afficher dans un tableur</strong> :</p>
<a class="button is-info" href="{% url 'order:grouped_order_sheet' grouped_order.code %}">
<a class="button is-info" href="{% url 'order:grouped_order_sheet' grouped_order.code %}" target="_blank">
<i class="fa fa-file-pdf-o mr-3" aria-hidden="true"></i>Commandes en PDF
</a>
<a class="button is-info" href="{% url 'order:grouped_order_csv_export' grouped_order.code %}">
<a class="button is-info" href="{% url 'order:grouped_order_csv_export' grouped_order.code %}" target="_blank">
<i class="fa fa-file-excel-o mr-3" aria-hidden="true"></i>Commandes en CSV
</a>
</div>
@ -126,7 +126,7 @@
<tfoot>
<th>Total</th>
<th></th>
<th>{{ total_ordered_items }}</th>
<th></th>
<th>{{ grouped_order.total_price }} €</th>
</tfoot>
</table>
@ -137,11 +137,11 @@
<div id="commandes" class="box tabcontent">
<div class="buttons is-pulled-right">
<a class="button is-info" href="{% url 'order:email_list' grouped_order.code %}?format=csv">
<a class="button is-info" href="{% url 'order:email_list' grouped_order.code %}?format=csv" target="_blank">
<i class="fa fa-file-excel-o mr-3" aria-hidden="true"></i>Emails en CSV
</a>
<input id="email_list" name="email_list" hidden="true" value="{{ emails_list|join:';' }}" />
<a class="button is-info" onclick="copyText('email_list')">
<a class="button is-info" onclick="copyText('email_list')" target="_blank">
<i class="fa fa-files-o mr-3" aria-hidden="true"></i>Copier les emails
</a>
</div>
@ -161,7 +161,7 @@
<tr>
<td>{{ order.author }}</td>
<td>{{ order.price }} €</td>
<td><a href="mailto:{{ order.author.email }}">{{ order.author.email }}</a>{% if order.author.phone %} / {{ order.author.phone }}{% endif %}</td>
<td><a href="mailto:{{ order.author.email }}">{{ order.author.email }}</a></td>
<td>
<button class="button is-info is-small js-modal-trigger" data-target="order-detail-{{ order.id }}">
Voir
@ -180,33 +180,40 @@
<!-- Order detail modal -->
<div id="order-detail-{{ order.id }}" class="modal">
<div class="modal-background"></div>
<div class="modal-card ">
<div class="modal-card">
<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>
<p class="has-text-grey-dark">Le {{ order.created_date|date:'d M Y' }} à {{ order.created_date|date:'H:i' }}</p>
<p class="modal-card-title">Commande de {{ order.author }}</p>
</div>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="columns">
<div class="column">
<div class="columns is-multiline">
<div class="column is-full">
{% for item in order.ordered_items.all %}
{{ item.nb }} × {{ item.item.name }}
<hr class="mb-0 mt-1">
<hr class="mb-1 mt-1">
{% endfor %}
</div>
{% if order.note %}
<div class="column is-4">
<div class="column is-full">
<p class="mini-title">Note à l'organisateur·ice</p>
<p>{{ order.note }}</p>
</div>
{% endif %}
<div class="column is-full">
<p class="mini-title">Numéro de téléphone</p>
<p>{{ order.author.phone }}</p>
</div>
</div>
{% endif %}
</section>
<footer class="modal-card-foot">
<p class="mini-title">Total à payer</p>
<p>{{ order.price }} €</p>
<div class="columns">
<div class="column is-half">
<p class="mini-title">Total à payer</p>
<p>{{ order.price }} €</p>
</div>
</div>
</footer>
</div>
</div>

View file

@ -21,22 +21,26 @@
}
{% if items|length > 10 %}
.item_name {
text-align:center;
white-space:nowrap;
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
-o-transform: rotate(-90deg);
transform: rotate(-90deg);
font-size: 8pt;
height:220px;
}
.item_name div {
width:200px;
-webkit-transform: translateX(-40%);
-moz-transform: translateX(-40%);
-ms-transform: translateX(-40%);
-o-transform: translateX(-40%);
transform: translateX(-40%);
margin:-10px -120% ;
display:inline-block;
}
.item_name div:before{
content:'';
width:0;
padding-top:110%;
display:inline-block;
vertical-align:middle;
}
{% endif %}
@ -49,19 +53,20 @@
border: 1px black solid;
border-collapse: collapse;
width: 100%;
table-layout: fixed
}
th, td {
vertical-align: center;
padding: 3px 2px 2px 2px;
border: 1px black solid;
font-weight: normal;
text-align: center;
word-wrap: break-word;
}
th {
page-break-inside: avoid;
word-break: break-all;
word-wrap: break-word;
font-weight: bold;
}
@ -80,7 +85,7 @@
<thead>
<tr>
<th style="font-size: 0.5em; width: 2em">OK</th>
<th style="text-align: center; width: 20%">Nom</th>
<th style="text-align: center">Nom</th>
{% for item in items %}
<th class="item_name" style="font-weight: normal;">
<div>{{ item.name }}</div>

View file

@ -1,7 +1,5 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Modifier la commande groupée{% endblock %}
{% block content %}
@ -11,14 +9,13 @@
<div class="box">
<p class="title">{{ grouped_order.name }} - modifier</p>
<form method="post" onsubmit="deadlinePassedCheck(event)">{% csrf_token %}
{{ form | crispy }}
{{ form.as_p }}
<div class="buttons">
<a class="button is-light" href="{% url 'order:index' %}">Annuler</a>
<input class="button is-primary" type="submit" value="Suivant">
</div>
</form>
</div>
{% endblock %}
{% block extra_js %}

View file

@ -6,8 +6,7 @@ from django.urls import reverse
from django.utils import timezone
from la_chariotte.order.models import GroupedOrder, Item
from .utils import create_grouped_order
from la_chariotte.order.tests.utils import create_grouped_order
pytestmark = pytest.mark.django_db

View file

@ -1,7 +1,7 @@
import pytest
from django.urls import reverse
from .utils import create_grouped_order
from la_chariotte.order.tests.utils import create_grouped_order
pytestmark = pytest.mark.django_db

View file

@ -4,14 +4,16 @@ from io import StringIO
import pytest
from django.contrib import auth
from django.contrib.auth import get_user
from django.forms.utils import to_current_timezone
from django.urls import reverse
from django.utils import timezone
from icalendar import Calendar, vText
from la_chariotte.order import models
from la_chariotte.tests.utils import create_grouped_order, order_items_in_grouped_order
from la_chariotte.order.tests.utils import (
create_grouped_order,
order_items_in_grouped_order,
)
pytestmark = pytest.mark.django_db
@ -215,32 +217,11 @@ class TestJoinGroupedOrderView:
response = client.post(join_url, {"code": "123456"})
assert (
"Désolé, nous ne trouvons aucune commande avec ce code"
in response.content.decode()
"nous ne trouvons aucune commande avec ce code" in response.content.decode()
)
class TestGroupedOrderDetailView:
def test_order_item_with_authenticated_user(self, client, connected_grouped_order):
detail_url = reverse(
"order:grouped_order_detail",
kwargs={
"code": connected_grouped_order.code,
},
)
response = client.get(detail_url)
assert response.status_code == 200
order_author = response.context[0]["order_author"]
current_user = get_user(client)
assert order_author is not None
assert order_author.first_name == current_user.first_name
assert order_author.email == current_user.username
assert order_author.last_name == current_user.last_name
def test_order_item(self, client, other_user):
"""
From the OrderDetailView, we order an item using the order form, and it creates an models.Order with an Ordered_item inside
@ -321,9 +302,7 @@ class TestGroupedOrderDetailView:
# OrderedItems are not created when the ordered quantity is 0
assert models.OrderedItem.objects.count() == 2
def test_order_item__no_articles_ordered(
self, client, authenticated_user_with_name
):
def test_order_item__no_articles_ordered(self, client, other_user):
"""
From the OrderDetailView, we order without having changed any item quantity.
An error is raised.
@ -333,7 +312,7 @@ class TestGroupedOrderDetailView:
days_before_delivery_date=5,
days_before_deadline=2,
name="gr order test",
orga_user=authenticated_user_with_name,
orga_user=other_user,
)
item = models.Item.objects.create(
name="test item 1", grouped_order=grouped_order, price=1
@ -359,10 +338,10 @@ class TestGroupedOrderDetailView:
order_url,
{
f"quantity_{item.pk}": [0, 0],
"first_name": {authenticated_user_with_name.first_name},
"last_name": {authenticated_user_with_name.last_name},
"first_name": "Prénom test",
"last_name": "Nom test",
"phone": "0645632569",
"email": {authenticated_user_with_name.email},
"email": "test@mail.fr",
"note": "test note",
},
)
@ -371,14 +350,12 @@ class TestGroupedOrderDetailView:
response.context["error_message"]
== "Veuillez commander au moins un produit"
)
assert (
response.context["author"].first_name
== authenticated_user_with_name.first_name
)
assert response.context["author"].first_name == "Prénom test"
content = response.content.decode()
assert authenticated_user_with_name.first_name in content
assert authenticated_user_with_name.last_name in content
assert authenticated_user_with_name.email in content
assert "Prénom test" in content
assert "Nom test" in content
assert "0645632569" in content
assert "test@mail.fr" in content
assert "test note" in content
assert not models.Order.objects.first()
assert not models.OrderAuthor.objects.first()
@ -662,36 +639,6 @@ class TestGroupedOrderDetailView:
assert order.price == 4
assert item.get_remaining_nb() == 16
def test_phone_not_required_display(self, client, other_user):
"""a user orders something without entering phone when it is required"""
grouped_order = create_grouped_order(
days_before_delivery_date=5,
days_before_deadline=2,
name="gr order test",
orga_user=other_user,
)
assert grouped_order.is_phone_mandatory == True
item = models.Item.objects.create(
name="test item 1", grouped_order=grouped_order, price=1, max_limit=2
)
detail_url = reverse(
"order:grouped_order_detail",
kwargs={
"code": grouped_order.code,
},
)
response = client.get(detail_url)
assert response.status_code == 200
assert "gr order test" in response.content.decode()
assert (
"Numéro de téléphone <em>(facultatif)</em>" not in response.content.decode()
)
grouped_order.is_phone_mandatory = False
grouped_order.save()
response = client.get(detail_url)
assert "gr order test" in response.content.decode()
assert "Numéro de téléphone <em>(facultatif)</em>" in response.content.decode()
class TestGroupedOrderOverview:
def test_get_overview(self, client_log):
@ -736,7 +683,6 @@ class TestGroupedOrderOverview:
assert response.status_code == 200
assert "test item" in response.content.decode()
assert {"test@mail.fr"} == response.context["emails_list"]
assert 4 == response.context["total_ordered_items"]
assert "gr order test" in response.content.decode()
item.refresh_from_db()
assert item.get_total_price() == 8
@ -791,25 +737,6 @@ class TestGroupedOrderOverview:
response = client_log.get(orga_view_url)
assert response.status_code == 403
def test_superuser_can_access_overview(self, other_user, admin_client):
"""
A superuser that is not orga can get the GroupedOrderOverview
"""
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={
"code": grouped_order.code,
},
)
response = admin_client.get(orga_view_url)
assert response.status_code == 200
def test_deadline_passed(self, client_log):
"""
If the deadline is passed, the user sees a message but cannot share link
@ -1426,7 +1353,7 @@ class TestGroupedOrderSheetView:
response = client_log.get(generate_sheet_url)
assert response.status_code == 200
assert response.context["grouped_order"] == grouped_order
assert len(response.context["items"]) == 0
assert response.context["items"].count() == 0
assert len(response.context["orders_dict"]) == 0
# we order some items in the grouped order
@ -1434,7 +1361,7 @@ class TestGroupedOrderSheetView:
response = client_log.get(generate_sheet_url)
assert response.status_code == 200
assert response.context["grouped_order"] == grouped_order
assert len(response.context["items"]) == 2
assert response.context["items"].count() == 2
assert response.context["orders_dict"][order] == [3, 2]
assert response.context["grouped_order"].total_price == 35
@ -1552,7 +1479,8 @@ class TestExportGroupOrderEmailAdressesToDownloadView:
assert response.status_code == 200
assert response["Content-Type"] == "text/csv"
content = response.content.decode()
assert "test@mail.fr\r\n" in content
assert "order_name,email" in content
assert "order_name,email\r\ngr order test,test@mail.fr\r\n" in content
def test_export_format_default(self, client_log):
grouped_order = create_grouped_order(
@ -1659,10 +1587,8 @@ class TestExportGroupedOrderToCSVView:
assert response.status_code == 200
content = response.content.decode("utf-8")
csv_reader = csv.reader(StringIO(content), delimiter=";")
csv_reader = csv.reader(StringIO(content))
body = list(csv_reader)
created_date = f"{timezone.now().strftime('%d/%m/%Y')}"
created_time = f"{timezone.now().strftime('%H:%M')}"
assert body == [
[
"",
@ -1672,47 +1598,11 @@ class TestExportGroupedOrderToCSVView:
"Prix de la commande",
"Mail",
"Téléphone",
"Note",
"Date",
"Heure",
],
["", "Prix unitaire TTC (€)", "2,00", "9,00"],
["Nom", "Prénom"],
[
"alescargot",
"bob",
"1",
"0",
"2,00",
"bob2@escargot.fr",
"'000",
"",
created_date,
created_time,
],
[
"alescargot",
"bobby",
"0",
"1",
"9,00",
"bob3@escargot.fr",
"'000",
"",
created_date,
created_time,
],
[
"lescargot",
"bob",
"3",
"2",
"24,00",
"bob@escargot.fr",
"'000",
"",
created_date,
created_time,
],
["alescargot", "bob", "1", "0", "2,00", "bob2@escargot.fr", "'000"],
["alescargot", "bobby", "0", "1", "9,00", "bob3@escargot.fr", "'000"],
["lescargot", "bob", "3", "2", "24,00", "bob@escargot.fr", "'000"],
["", "TOTAL", "4", "3", "35,00"],
]

View file

@ -3,8 +3,7 @@ from django.contrib import auth
from django.urls import reverse
from la_chariotte.order import models
from .utils import create_grouped_order
from la_chariotte.order.tests.utils import create_grouped_order
pytestmark = pytest.mark.django_db

View file

@ -3,8 +3,10 @@ from django.contrib import auth
from django.urls import reverse
from la_chariotte.order import models
from .utils import create_grouped_order, order_items_in_grouped_order
from la_chariotte.order.tests.utils import (
create_grouped_order,
order_items_in_grouped_order,
)
pytestmark = pytest.mark.django_db

View file

@ -9,20 +9,12 @@ pytestmark = pytest.mark.django_db
def create_grouped_order(
days_before_delivery_date,
days_before_deadline,
name,
orga_user,
is_phone_mandatory=True,
days_before_delivery_date, days_before_deadline, name, orga_user
):
date = timezone.now().date() + datetime.timedelta(days=days_before_delivery_date)
deadline = timezone.now() + datetime.timedelta(days=days_before_deadline)
grouped_order = models.GroupedOrder.objects.create(
name=name,
orga=orga_user,
delivery_date=date,
deadline=deadline,
is_phone_mandatory=is_phone_mandatory,
name=name, orga=orga_user, delivery_date=date, deadline=deadline
)
grouped_order.create_code_from_pk()
grouped_order.save()
@ -52,4 +44,11 @@ def order_items_in_grouped_order(grouped_order):
models.OrderedItem.objects.create(order=order, item=item_2, nb=2)
models.OrderedItem.objects.create(order=order_2, item=item_1, nb=1)
models.OrderedItem.objects.create(order=order_3, item=item_2, nb=1)
item_1.compute_ordered_nb()
item_2.compute_ordered_nb()
order.compute_order_price()
order_2.compute_order_price()
order_3.compute_order_price()
grouped_order.compute_total_price()
grouped_order.save()
return order

View file

@ -13,7 +13,6 @@ from icalendar import Calendar, Event, vCalAddress, vText
from ..forms import GroupedOrderForm, Item, JoinGroupedOrderForm
from ..models import GroupedOrder, OrderAuthor
from .mixins import UserIsOrgaMixin
class IndexView(LoginRequiredMixin, generic.ListView):
@ -112,36 +111,23 @@ class GroupedOrderDetailView(generic.DetailView):
return get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
def get_context_data(self, **kwargs):
grouped_order = self.get_object()
items = grouped_order.item_set.all()
items = self.get_object().item_set.all()
remaining_qty = {item.id: item.get_remaining_nb() for item in items}
prices_dict = {item.id: item.price for item in items}
if self.request.user.is_authenticated:
order_author = OrderAuthor(
first_name=self.request.user.first_name,
last_name=self.request.user.last_name,
email=self.request.user.username,
)
else:
order_author = None
context = super().get_context_data(**kwargs)
context.update(
{
# Used for the js display of total price of an order
"prices_dict": json.dumps(prices_dict, cls=DjangoJSONEncoder),
"remaining_qty": remaining_qty,
"order_author": order_author,
# Used to set if the phone is required in the form
"is_phone_mandatory": grouped_order.is_phone_mandatory,
}
)
return context
class GroupedOrderOverview(UserIsOrgaMixin, generic.DetailView):
class GroupedOrderOverview(UserPassesTestMixin, generic.DetailView):
model = GroupedOrder
template_name = "order/grouped_order_overview.html"
context_object_name = "grouped_order"
@ -150,8 +136,14 @@ class GroupedOrderOverview(UserIsOrgaMixin, generic.DetailView):
return get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
def test_func(self):
# Staff can see but not edit grouped orders
return super().test_func() or self.request.user.is_staff
# Restrict access to the manager
return self.get_object().orga == self.request.user
def get(self, request, *args, **kwargs):
# Compute grouped order total price before display
self.get_object().compute_total_price()
self.get_object().compute_items_ordered_nb()
return super().get(self, request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(GroupedOrderOverview, self).get_context_data(**kwargs)
@ -163,7 +155,6 @@ class GroupedOrderOverview(UserIsOrgaMixin, generic.DetailView):
order__in=self.get_object().order_set.all()
)
context["emails_list"] = set(participant.email for participant in participants)
context["total_ordered_items"] = self.get_object().get_total_ordered_items()
return context
@ -184,7 +175,7 @@ class GroupedOrderCreateView(LoginRequiredMixin, generic.CreateView):
return super().form_valid(form)
class GroupedOrderUpdateView(UserIsOrgaMixin, generic.UpdateView):
class GroupedOrderUpdateView(UserPassesTestMixin, generic.UpdateView):
model = GroupedOrder
template_name = "order/grouped_order_update.html"
context_object_name = "grouped_order"
@ -193,16 +184,24 @@ class GroupedOrderUpdateView(UserIsOrgaMixin, generic.UpdateView):
def get_object(self, queryset=None):
return get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
def test_func(self):
# Restrict access to the manager
return self.get_object().orga == self.request.user
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
class GroupedOrderDuplicateView(UserPassesTestMixin, generic.RedirectView):
def get_object(self, queryset=None):
return get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
def test_func(self):
# Restrict access to the manager
return self.get_object().orga == self.request.user
def get(self, request, *args, **kwargs):
# overwrite the ``get`` function to copy the initial grouped order before
# redirecting to the update view of the new grouped order
@ -225,6 +224,7 @@ class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
# duplicate each item and add it to new_grouped_order
for item in initial_grouped_order.item_set.all():
item.pk = None
item.ordered_nb = 0
item.save()
new_grouped_order.item_set.add(item)
@ -238,7 +238,7 @@ class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
)
class GroupedOrderDeleteView(UserIsOrgaMixin, generic.DeleteView):
class GroupedOrderDeleteView(UserPassesTestMixin, generic.DeleteView):
model = GroupedOrder
template_name = "order/grouped_order_confirm_delete.html"
context_object_name = "grouped_order"
@ -249,6 +249,10 @@ class GroupedOrderDeleteView(UserIsOrgaMixin, generic.DeleteView):
def get_success_url(self):
return reverse_lazy("order:index")
def test_func(self):
# Restrict access to the manager
return self.get_object().orga == self.request.user
def form_valid(self, form):
# Delete related OrderAuthors
grouped_order = self.get_object()
@ -280,27 +284,23 @@ class GroupedOrderAddItemsView(UserPassesTestMixin, generic.ListView):
return grouped_order.orga == self.request.user
class GroupedOrderExportView(UserIsOrgaMixin, generic.DetailView):
class GroupedOrderExportView(UserPassesTestMixin, generic.DetailView):
model = GroupedOrder
template_name = "order/grouped_order_sheet.html"
context_object_name = "grouped_order"
def test_func(self):
# Grouped order orga, superuser and staff can get this view
return super().test_func() or self.request.user.is_staff
def get_object(self, queryset=None):
return get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
def test_func(self):
# Restrict access to the manager
return self.get_object().orga == self.request.user
def get_context_data(self, **kwargs):
context = super(GroupedOrderExportView, self).get_context_data(**kwargs)
grouped_order = self.get_object()
items = [
item
for item in grouped_order.item_set.all().order_by("name")
if item.ordered_nb > 0
]
items = grouped_order.item_set.filter(ordered_nb__gt=0).order_by("name")
orders = grouped_order.order_set.all().order_by(
"author__last_name", "author__first_name"
)
@ -328,9 +328,9 @@ class DownloadGroupedOrderSheetView(WeasyTemplateResponseMixin, GroupedOrderExpo
class ExportGroupOrderEmailAdressesToDownloadView(UserPassesTestMixin, generic.View):
def test_func(self):
# Restrict access to the manager, superuser and staff
# Restrict access to the manager
origin = get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
return origin.orga == self.request.user or self.request.user.is_staff
return origin.orga == self.request.user
def get(self, request, *args, **kwargs):
grouped_order = get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
@ -344,9 +344,10 @@ class ExportGroupOrderEmailAdressesToDownloadView(UserPassesTestMixin, generic.V
response = http.HttpResponse(content_type="text/csv")
filename = f"commande _{grouped_order.name.replace(' ', '_')}"
response["Content-Disposition"] = f'attachment; filename="{filename}.csv"'
writer = csv.writer(response, delimiter=";")
writer = csv.writer(response)
writer.writerow(["order_name", "email"])
for email in email_list:
writer.writerow([email])
writer.writerow([grouped_order.name, email])
return response
else:
email_list = ";\n".join(email_list)
@ -361,10 +362,10 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView):
response = http.HttpResponse(
content_type="text/csv",
headers={
"Content-Disposition": f'attachment; filename="{context["object"].name}-commandes"'
"Content-Disposition": f'attachment; filename="{ context["object"].name }-commandes"'
},
)
writer = csv.writer(response, delimiter=";")
writer = csv.writer(response)
# write headers rows
row = ["", ""]
@ -373,9 +374,6 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView):
row.append("Prix de la commande")
row.append("Mail")
row.append("Téléphone")
row.append("Note")
row.append("Date")
row.append("Heure")
writer.writerow(row)
row = ["", "Prix unitaire TTC (€)"]
@ -395,9 +393,6 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView):
row.append(str(order.price).replace(".", ","))
row.append(order.author.email)
row.append(f"'{order.author.phone}")
row.append(order.note)
row.append(order.created_date.strftime("%d/%m/%Y"))
row.append(order.created_date.strftime("%H:%M"))
writer.writerow(row)
# write total row

View file

@ -29,8 +29,5 @@ class ItemDeleteView(UserPassesTestMixin, generic.DeleteView):
return reverse_lazy("order:manage_items", args=[self.object.grouped_order.code])
def test_func(self):
# Restrict access to the manager or a superuser
return (
self.get_object().grouped_order.orga == self.request.user
or self.request.user.is_superuser
)
# Restrict access to the manager
return self.get_object().grouped_order.orga == self.request.user

View file

@ -1,13 +0,0 @@
from django.contrib.auth.mixins import UserPassesTestMixin
class UserIsOrgaMixin(UserPassesTestMixin):
"""
The view is accessible only if the request user is orga or superuser
"""
def test_func(self):
return (
self.get_object().orga == self.request.user
or self.request.user.is_superuser
)

View file

@ -2,7 +2,6 @@ from django import http
from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.views import generic
from la_chariotte.mail.utils import send_order_confirmation_mail
@ -49,12 +48,7 @@ def place_order(request, code):
author = OrderAuthor.objects.create(
first_name=first_name, last_name=last_name, email=email, phone=phone
)
order = Order.objects.create(
author=author,
grouped_order=grouped_order,
note=note,
created_date=timezone.now(),
)
order = Order.objects.create(author=author, grouped_order=grouped_order, note=note)
# add items to the order
error_message = None
@ -72,6 +66,7 @@ def place_order(request, code):
if error_message:
order.delete()
author.delete()
grouped_order.compute_items_ordered_nb()
return render(
request,
"order/grouped_order_detail.html",
@ -102,6 +97,8 @@ def place_order(request, code):
)
# Send confirmation mail and redirect to confirmation page
order.compute_order_price()
grouped_order.compute_items_ordered_nb()
send_order_confirmation_mail(order)
# Redirect to prevent data from being posted twice when the user hits the Back
@ -120,6 +117,7 @@ def validate_item_ordered_nb(item, ordered_nb):
def validate_articles_ordered_nb(order):
"""Return an error if no items are ordered"""
order.compute_order_articles_nb()
if order.articles_nb == 0:
return "Veuillez commander au moins un produit"
return None
@ -138,8 +136,5 @@ class OrderDeleteView(UserPassesTestMixin, generic.DeleteView):
)
def test_func(self):
# Restrict access to the manager or a superuser
return (
self.get_object().grouped_order.orga == self.request.user
or self.request.user.is_superuser
)
"""Accessible only if the requesting user is the grouped order organizer"""
return self.get_object().grouped_order.orga == self.request.user

View file

@ -1,28 +0,0 @@
import json
from datetime import datetime, timedelta
from django.http import HttpResponse
from django.utils.timezone import make_aware
from la_chariotte.accounts.models import CustomUser
from la_chariotte.order.models import GroupedOrder, Order
def stats(request):
last_month = make_aware(datetime.today() - timedelta(days=30))
last_month_grouped_orders = GroupedOrder.objects.filter(
deadline__gte=last_month
).count()
return HttpResponse(
json.dumps(
{
"total_users": CustomUser.objects.count(),
"total_grouped_orders": GroupedOrder.objects.count(),
"total_orders": Order.objects.count(),
"last_month_grouped_orders": last_month_grouped_orders,
}
),
content_type="application/json",
)

View file

@ -7,10 +7,8 @@ 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://framagit.org/la-chariotte/la-chariotte"
GITLAB_LINK = "https://gitlab.com/hashbangfr/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"
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View file

@ -1,14 +0,0 @@
.accordion
transition: max-height 0.2s ease-out
border: none
outline: none
transition: 0.4s
.message-header
cursor: pointer
background: $white-ter
border: 1px solid $primary
color: $primary
p
margin: 0
.message-body
display: none

View file

@ -10,7 +10,3 @@
.formatted-text
white-space: pre-wrap
img.notice-img
border: $info 3px solid
margin-bottom: 1em

View file

@ -1,18 +1,6 @@
.footer
padding-bottom: 10px !important
padding: 50px 0
padding-left: 2em
font-size: 1em
logo > h2
font-size: 2rem
footer > .columns
margin-bottom: 1rem !important
margin-top: 0
footer .margin-auto
margin: auto
footer .logo
letter-spacing: 1px

View file

@ -17,4 +17,3 @@
@import "./base/titles"
@import "./base/tabs"
@import "./base/footer"
@import "./base/accordion"

View file

@ -1,6 +1,7 @@
{% load static %}
{% load settings %}
{% load sass_tags %}
<!DOCTYPE html>
<html lang="fr">
<head>
@ -93,9 +94,6 @@
<a class="navbar-item" href="{% url 'notice' %}">
<i class="fa fa-cog mr-3" aria-hidden="true"></i>Comment ça marche&nbsp;?
</a>
<a class="navbar-item" href="{% url 'faq' %}">
<i class="fa fa-question mr-3" aria-hidden="true"></i>FAQ
</a>
<a class="navbar-item" href="https://blog.chariotte.fr/last">
<i class="fa fa-newspaper-o mr-3" aria-hidden="true"></i>Actualités</a>
</div>
@ -123,62 +121,46 @@
{% block content %}{% endblock %}
</div>
</main>
<footer class="footer section">
<div class="container">
<div class="columns is-multiline is-justify-content-center">
<div class="column is-4-desktop is-4-tablet">
<div class="widget mb-0">
<div class="logo mb-4">
<p>
{% settings_value "PROJECT_NAME" %}
| <a href='{% settings_value "GITLAB_LINK" %}'>version {{ version }}</a>
</p>
</div>
<ul class="list-unstyled footer-menu lh-35">
<a href="mailto:{% settings_value "CONTACT_MAIL" %}"><i class="fa fa-envelope-square mr-2 text-muted"></i>
{% settings_value "CONTACT_MAIL" %}
</a>
</li>
<li>
<a href="{% url 'legal_notice' %}"><i class="fa fa-legal mr-2 text-muted"></i>Mentions légales</a>
</li>
</ul>
</div>
</div>
<div class="column is-2-desktop is-4-tablet">
<div class="widget mb-0">
<h4 class="mb-4">Suivez nous</h4>
<ul class="list-unstyled footer-socials">
<li>
<a href="https://blog.chariotte.fr"><i class="fa fa-rss-square mr-2 text-muted"></i>Chariotte news</a>
</li>
<li>
<a href="https://mastodon.scop.coop/@la_chariotte"><i class="fa fa-mastodon-square mr-2 text-muted"></i>Mastodon</a>
</li>
<li>
<a href="https://www.facebook.com/profile.php?id=100091984176981"><i class="fa fa-facebook-square mr-2 text-muted"></i>Facebook</a>
</li>
<li>
<a href="https://www.linkedin.com/company/la-chariotte-commandes-groupees/"><i class="fa fa-linkedin-square mr-2 text-muted"></i>LinkedIn</a>
</li>
</ul>
</div>
</div>
<div class="column is-3-desktop is-4-tablet">
<div class="widget mb-0">
<h4 class="mb-4">Soutenez-nous</h4>
<ul class="list-unstyled">
<li>
<a href="{% settings_value "FEEDBACK_LINK" %}"><i class="fa fa-comments mr-2 text-muted"></i>Que pensez-vous de la Chariotte ?</a>
</li>
<li><a href="{% settings_value "HELLOASSO_LINK" %}"><i class="fa fa-heart mr-2 text-muted"></i>Faire un (petit) don</a></li>
</ul>
<footer class="footer">
<div class="content columns has-text-centered">
<div class="column margin-auto">
<p>
<strong>La Chariotte</strong> | version {{ version }}
</p>
<p>
<a href="{% url 'legal_notice' %}">Mentions légales</a>
</p>
</div>
</div>
</footer>
</body>
</html>
<script>
<div class="column is-size-6 columns">
<div class="column margin-auto">
<p>Contact : <a href="mailto:{% settings_value "CONTACT_MAIL" %}">{% settings_value "CONTACT_MAIL" %}</a>
<p>Suivez <strong>la Chariotte</strong> sur :</p>
</div>
<div class="column">
<a href="https://www.facebook.com/profile.php?id=100091984176981"
target="_blank"
rel="noopener">
<i class="fa fa-facebook-square mr-2" aria-hidden="true"></i>Facebook
</a>
<br>
<a href="https://www.linkedin.com/company/la-chariotte-commandes-groupees/"
target="_blank"
rel="noopener">
<i class="fa fa-linkedin-square mr-3" aria-hidden="true"></i>Linkedin
</a>
<br>
<a href="https://mastodon.scop.coop/@la_chariotte"
target="_blank"
rel="noopener">
<i class="fa fa-mastodon-square mr-3" aria-hidden="true"></i>Mastodon
</a>
</div>
</div>
</div>
</footer>
</body>
</html>
<script>
// For responsive menu
document.addEventListener('DOMContentLoaded', () => {
@ -201,4 +183,4 @@
});
});
{% block extra_js %} {% endblock %}
</script>
</script>

View file

@ -31,7 +31,7 @@
<p>
La Chariotte est développée sous licence libre Affero GPL.
Cela signifie que vous pouvez
<a href="{{ gitlab }}" rel="noopener">aller voir le code</a> si vous êtes curieux, et y contribuer si vous êtes motivé·e !
<a href="{{ gitlab }}" rel="noopener" target="_blank">aller voir le code</a> si vous êtes curieux, et y contribuer si vous êtes motivé·e !
</p>
</div>
@ -45,7 +45,7 @@
Nous sommes très, très preneurs de vos suggestions, critiques constructives et demandes, et ouvert·e·s à toute question !
</p>
<p>
Faites des retours sur la version actuelle <a href="https://framaforms.org/vos-retours-sur-la-chariotte-1692345453" rel="noopener">ici</a>, ou par mail à <a href="mailto:{{ mail }}">{{ mail }}</a>.
Faites des retours sur la version actuelle <a href="https://framaforms.org/vos-retours-sur-la-chariotte-1692345453" rel="noopener" target="_blank">ici</a>, ou par mail à <a href="mailto:{{ mail }}">{{ mail }}</a>.
</p>
</div>

View file

@ -1,241 +0,0 @@
{% extends 'base.html' %}
{% block title %}FAQ{% endblock %}
{% block content %}
{% load static %}
{% load settings %}
<p class="desktop-hidden mobile-content-title">
{% block content_title %}La Chariotte - Facile A Qomprendre{% endblock %}
</p>
<div class="box">
<div class="accordion">
<article class="message">
<div class="message-header">
<p>C'est quoi l'outil la Chariotte ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
La Chariotte c'est une application web qui permet de simplifier la gestion et l'organisation de commandes groupées.
Voyez ça comme un excel amélioré spécifique aux besoins de l'organisation d'une commande groupée.
</div>
</article>
<article class="message">
<div class="message-header">
<p>C'est quoi la Chariotte plus largement ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
C'est avant tout un logiciel libre dont vous pouvez utiliser l'instance officielle sur chariotte.fr.
C'est aussi le nom de l'association responsable de la maintenance de ce logiciel. Tous bénévoles,
les membres de la Chariotte font vivre et améliorent continuellement l'outil pour répondre à vos besoins.
</div>
</article>
<article class="message">
<div class="message-header">
<p>Est-ce que je peux rejoindre une commande commande créée par une personne que je ne connais pas ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
C'est prévu a long terme mais pour l'instant non ! Aujourd'hui la Chariotte n'a pas pour visée de
mettre en relation des organisateurs/producteurs et des participants. Il n'est donc pas
possible de trouver une liste de commandes groupées existantes que vous pourriez rejoindre à volonté.
</div>
</article>
<article class="message">
<div class="message-header">
<p>Faut-il faire partie de l'asso pour utiliser la chariotte ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
Non !
</div>
</article>
<article class="message">
<div class="message-header">
<p>Puis-je rejoindre l'association ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
Bien sûr et avec grand plaisir !
Si vous êtes intéressés pour rejoindre cette aventure,
contactez-nous sur <a href="mailto:{% settings_value "CONTACT_MAIL" %}">contact@chariotte.fr</a>.
</div>
</article>
<article class="message">
<div class="message-header">
<p>C'est quoi un logiciel libre ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
La Chariotte est développée sous licence libre Affero GPL.
Cela signifie que vous pouvez <a href="{% settings_value "GITLAB_LINK" %}">aller voir le code</a>
si vous êtes curieux, y contribuer si vous êtes motivé·e, ou même créer votre propre instance si vous le souhaitez !
</div>
</article>
<article class="message">
<div class="message-header">
<p>C'est quoi une commande groupée ? Ça s'organise comment ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
C'est la mise en relation ponctuelle d'un producteur, ou plutôt ses produits,
avec des particuliers, pour permettre une consommation en circuit court et hors
des modes de consommation de masse conventionnels.
<br><br>
Pour faire simple, imaginons que vous connaissiez l'apicultrice du coin et que plusieurs de
vos voisins vous aient dit être prêts à acheter du miel.
Vous pouvez alors commander 40 pots de miel à votre amie et les redistribuer à tout le quartier,
tout ça en un seul voyage !
</div>
</article>
<article class="message">
<div class="message-header">
<p>Quelle est la différence avec une AMAP ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
Dans les faits, la plupart des AMAPs fonctionnent sur la base de commandes
groupées régulières et à destination d'un public d'adhérents fixe.<br>
La Chariotte n'a pas vocation à remplacer le système des AMAPs, qui apporte un revenu régulier aux producteur.ice.s !
L'idée est d'apporter une solution complémentaire pour les endroits où les AMAPs ne sont pas établies, ou pour un produit en particulier qui n'est pas distribué régulièrement dans votre AMAP.
Il arrive aussi parfois que la Chariotte soit choisie comme outil de gestion pour l'AMAP si elle n'en avait pas encore.
</div>
</article>
<article class="message">
<div class="message-header">
<p>Quel est l'avantage de la Chariotte par rapport au logiciel de mon AMAP ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
Nous avons conçu la Chariotte spécifiquement pour répondre aux besoins des
organisateur.ice.s et des participant.e.s à une commande groupée, en particulier des commandes groupées ponctuelles.
Il s'agit d'un outil spécialisé dont la seule ambition est de simplifier la vie de ses utilisateurs.
Libre à vous d'évaluer vos options et de choisir l'outil qui convient le mieux à votre organisation !
</div>
</article>
<article class="message">
<div class="message-header">
<p>Je veux organiser une commande groupée, où est-ce que je trouve des producteurs ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
La Chariotte ne fournit pour l'instant pas de liste de producteurs ;
c'est une bonne occasion de faire connaissance avec ceux près de chez vous !
</div>
</article>
<article class="message">
<div class="message-header">
<p>Suis-je obligé de créer un compte pour commander ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
Même pas ! La création d'un compte n'est obligatoire que pour les utilisateurs souhaitant organiser
leurs propres commandes groupées. La participation à une commande existante
se veut à destination du plus grand nombre et ne nécessite donc pas de compte sur le site.
</div>
</article>
<article class="message">
<div class="message-header">
<p>Comment puis-je soutenir la Chariotte ?</p>
<a>
<span class="icon">
<i class="fa fa-angle-down" aria-hidden="true"></i>
</span>
</a>
</div>
<div class="message-body">
<p>Nous sommes une jeune association, et nous avons besoin de trois choses : de la visibilité,
des retours d'utilisateurs et un peu d'argent !</li>
<ul><li>Vous pouvez nous rejoindre sur les réseaux (Facebook, Mastodon, LinkedIn) pour aimer
et partager nos posts, ou encore parler de la Chariotte à vos amis !</li>
<li>Si vous saisissez une occasion d'utiliser la Chariotte, nous avons besoin de vous !
N'hésitez pas à nous faire un rapport de votre expérience de la Chariotte en pointant
les bons et les mauvais côtés de l'application. Par exemple en remplissant
<a href="{% settings_value "FEEDBACK_LINK" %}">ce formulaire</a> !</li>
<li>Si le projet vous parle et que vous avez envie de nous soutenir de manière plus significative,
nous avons besoin d'un peu d'argent de poche pour soutenir les frais de l'association ;
<a href ="{% settings_value "HELLOASSO_LINK" %}">par ici</a> pour en savoir plus !</li>
</ul>
<p>Et enfin, si vous souhaitez en savoir plus sur l'association en elle-même et que vous
souhaitez faire partie intégrante du projet, la Chariotte est à la recherche de bénévoles motivé·e·s !
N'hésitez pas à nous écrire un mot sur contact@chariotte.fr pour en savoir plus et rentrer en contact directement avec nous.</p>
</div>
</article>
</div>
</div>
{% endblock %}
<script>
{% block extra_js %}
document.addEventListener('DOMContentLoaded', () => {
var acc = document.getElementsByClassName("message-header");
var i;
for (i = 0; i < acc.length; i++) {
acc[i].addEventListener("click", function() {
/* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */
this.classList.toggle("active");
/* Toggle between hiding and showing the active panel */
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
});
}
});
{% endblock %}
</script>

View file

@ -34,7 +34,7 @@
<p class="title">Licence</p>
<p>
La Chariotte est développée sous licence libre <strong>Affero GPL</strong>. Le code source est
consultable <a href="{{ gitlab }}" rel="noopener">sur Gitlab</a> et ouvert à toute contribution ou suggestion.
consultable <a href="{{ gitlab }}" rel="noopener" target="_blank">sur Gitlab</a> et ouvert à toute contribution ou suggestion.
</p>
<p class="title">Conditions d'utilisation</p>
<p>

View file

@ -9,126 +9,11 @@
{% block content_title %}La Chariotte - mode d'emploi{% endblock %}
</p>
<div class="box">
<p>Comment marche la Chariotte et qu'est-ce qu'on peut y faire ? Voilà comment vous pouvez :</p>
<ul>
<li><a href="#join_grouped_order">Rejoindre une commande et commander des produits</a></li>
<li><a href="#pay">Payer (pas encore !)</a></li>
<li><a href="#create_account">Créer un compte (nécessaire seulement pour organiser une commande groupée)</a></li>
<li><a href="#create_grouped_order">Créer une commande groupée</a></li>
<li><a href="#add_item">Ajouter des produits à une commande groupée</a></li>
<li><a href="#go_to_overview">Accéder à la page de gestion de ma commande groupée</a></li>
<li><a href="#delete_item">Modifier des produits d'une commande après sa création</a></li>
<li><a href="#edit_grouped_order">Modifier des informations de la commande</a></li>
<li><a href="#share_grouped_order">Partager une commande à des participants</a></li>
<li><a href="#print_orders_list">Imprimer ma liste de commandes</a></li>
<li><a href="#get_spreadsheet">Voir les commandes sous forme de tableur</a></li>
</ul>
</div>
<div class="box" id="join_grouped_order">
<p class="title">Rejoindre une commande et commander des produits</p>
<p>Pour rejoindre une commande groupée, il faut y avoir été invité.e par son organisateur.ice.
Si vous avez un code à 6 caractères, vous pouvez l'entrer dans l'encadré "Rejoindre une commande groupée"
sur la page d'accueil de la Chariotte.</p>
<img class="notice-img" src="{% static 'img/notice/join_grouped_order.png' %}"/>
<p>Sinon, cliquez sur le lien que l'organisateur.ice vous a envoyé. Vous arriverez sur la page de commande.</p>
<img class="notice-img" src="{% static 'img/notice/order_form.png' %}"/>
<p>Une fois la commande validée, vous recevrez un mail de confirmation.</p>
</div>
<div class="box" id="pay">
<p class="title">Payer (pas encore !)</p>
<p>Pour linstant, le paiement nest pas pris en compte par le site de La Chariotte,
cest lorganisateur.ice qui doit vous indiquer les modalités de paiement.</p>
</div>
<div class="box" id="create_account">
<p class="title">Créer un compte (nécessaire seulement pour organiser une commande groupée)</p>
<p>1. Cliquez sur “Créer un compte”.</p>
<img class="notice-img" src="{% static 'img/notice/create_account.jpg' %}"/>
<p>2. Remplissez toutes les informations du formulaire, puis cliquez sur valider.</p>
<img class="notice-img" src="{% static 'img/notice/create_account_form.jpg' %}"/>
<p>3. Vous pouvez maintenant vous connecter.</p>
</div>
<div class="box" id="create_grouped_order">
<p class="title">Créer une commande groupée</p>
<p>1. En ayant un compte (nécessaire seulement pour les organisateurs!), vous aurez accès à une page de gestion des commandes groupées en cliquant sur le menu “Mes commandes groupées”.</p>
<img class="notice-img" src="{% static 'img/notice/my_grouped_orders.jpg' %}"/>
<p>2. Cest depuis cette page que vous pourrez créer une nouvelle commande en cliquant sur le bouton “Créer une nouvelle commande groupée” : </p>
<img class="notice-img" src="{% static 'img/notice/create_grouped_order.jpg' %}"/>
<p>3. Quelques informations vous seront alors demandées :</p>
<img class="notice-img" src="{% static 'img/notice/create_grouped_order_form.jpg' %}"/>
<p>4. En cliquant sur "Suivant", votre commande groupée sera validée.</p>
</div>
<div class="box" id="add_item">
<p class="title">Ajouter des produits à une commande groupée</p>
<p>Directement après avoir validé la commande groupée, vous accédez à la page de gestion des produits.</p>
<img class="notice-img" src="{% static 'img/notice/add_item.png' %}"/>
<p>Ici, vous pouvez renseigner les produits qui constitueront votre commande groupée.
Indiquez le nom du produit, son prix et la quantité disponible.
Cliquez sur “Ajouter” pour valider lajout.</p>
<img class="notice-img" src="{% static 'img/notice/add_item_2.png' %}"/>
</div>
<div class="box" id="go_to_overview">
<p class="title">Accéder à la page de gestion de ma commande groupée</p>
<p>Depuis le menu “Mes commandes groupées”, puis en cliquant sur le nom de la commande groupée
que vous voulez gérer, vous pourrez cliquer ensuite sur “Gestion de la commande groupee”.
</p>
<img class="notice-img" src="{% static 'img/notice/go_to_overview.png' %}"/>
<p>Sinon depuis la page “Mes commandes groupées” il est aussi possible de cliquer directement sur
licone de la ligne "liste" de la commande groupée que vous voulez gérer.</p>
</div>
<div class="box" id="delete_item">
<p class="title">Modifier des produits d'une commande après sa création</p>
<p>1. Si vous souhaitez modifier une information concernant la commande (titre, date, description, lieu de livraison, …),
il faut dabord <a href="#go_to_overview">accéder à la page de gestion de la commande groupée</a>.</p>
<p>2. Ensuite, dans "Résumé de la commande”, cliquez sur “Gérer les produits”.</p>
<img class="notice-img" src="{% static 'img/notice/grouped_order_sum_up.png' %}"/>
<img class="notice-img" src="{% static 'img/notice/items_overview.png' %}"/>
<p>3. Vous êtes maintenant sur la page de gestion des produits depuis laquelle vous pouvez ajouter ou supprimer des produits.
Cliquez sur “Valider” pour sauvegarder les modifications.</p>
<p>Attention : actuellement, il n'est pas possible de supprimer un produit une fois qu'il a été commandé par un.e participant.e.
Si vous voulez vraiment le supprimer, il faudra d'abord supprimer les commandes des participant.e.s concerné.e.s.</p>
</div>
<div class="box" id="edit_grouped_order">
<p class="title">Modifier des informations de la commande</p>
<p>1. Si vous souhaitez modifier une information concernant la commande (titre, date, description, lieu de livraison, …),
il faut dabord <a href="#go_to_overview">accéder à la page de gestion de la commande groupée</a>.</p>
<p>2. Vous pouvez ensuite cliquer sur le bouton “Modifier la commande”
qui vous amenera vers le formulaire pre-rempli des informations de la commande. </p>
<img class="notice-img" src="{% static 'img/notice/edit_grouped_order.png' %}"/>
<p>3. Modifiez les informations que vous voulez, puis cliquez sur “Suivant” pour valider.</p>
</div>
<div class="box" id="share_grouped_order">
<p class="title">Partager une commande à des participants</p>
<p>Une fois votre commande bien préparée (quantité et prix des produits corrects, informations de la commande justes, …),
il faut maintenant la partager à des participants pour quils puissent commander.</p>
<p>1. Depuis la page de <a href="#go_to_overview">gestion de la commande</a>,
allez dans longlet “partager et exporter”.</p>
<img class="notice-img" src="{% static 'img/notice/share_grouped_order.png' %}"/>
<p>2. Vous pouvez copier le lien, et lenvoyer à vos connaissances.</p>
<img class="notice-img" src="{% static 'img/notice/copy_link.png' %}"/>
</div>
<div class="box" id="print_orders_list">
<p class="title">Imprimer ma liste de commandes</p>
<p>1. Pour cela, il faut dabord <a href="#go_to_overview">accéder à la page de gestion de la commande groupée</a>.</p>
<p>2. Ensuite, dans "Partager et exporter”, cliquez sur “Commandes en PDF”.</p>
<img class="notice-img" src="{% static 'img/notice/print_orders_list.png' %}"/>
<p>3. Cela lance le téléchargement de votre liste de commandes en PDF.</p>
</div>
<div class="box" id="get_spreadsheet">
<p class="title">Voir les commandes sous forme de tableur</p>
<p>1. Pour cela, il faut dabord <a href="#go_to_overview">accéder à la page de gestion de la commande groupée</a>.</p>
<p>2. Ensuite, dans "Partager et exporter”, cliquez sur “Commandes en CSV”.</p>
<img class="notice-img" src="{% static 'img/notice/get_spreadsheet.png' %}"/>
<p>3. Le format CSV vous permet d'ouvrir le fichier avec le tableur de votre choix (libreoffice, excel, ...).</p>
<p>Voilà un petit dessin pour vous expliquer le déroulé d'une commande groupée avec la Chariotte.</p>
<figure class="image">
<img src="{% static 'img/notice_1.jpg' %}"/>
<img src="{% static 'img/notice_2.jpg' %}"/>
<img src="{% static 'img/notice_3.jpg' %}"/>
</figure>
</div>
{% endblock %}

View file

@ -1,38 +0,0 @@
from datetime import datetime, timedelta
from django.contrib import auth
from django.urls import reverse
from .utils import create_grouped_order, order_items_in_grouped_order
def test_stats(client_log):
response = client_log.get(reverse("stats"))
assert response.status_code == 200
assert response.json() == {
"total_grouped_orders": 0,
"total_orders": 0,
"total_users": 1,
"last_month_grouped_orders": 0,
}
# Create a new grouped-order, place an order and check that the stats changed.
grouped_order = create_grouped_order(
days_before_delivery_date=5,
days_before_deadline=2,
name="gr order test",
orga_user=auth.get_user(client_log),
)
order_items_in_grouped_order(grouped_order)
grouped_order.deadline = datetime.now() - timedelta(days=10)
grouped_order.save()
response = client_log.get(reverse("stats"))
assert response.status_code == 200
assert response.json() == {
"total_grouped_orders": 1,
"total_orders": 3,
"total_users": 1,
"last_month_grouped_orders": 1,
}

View file

@ -25,7 +25,6 @@ from django.views.generic.base import TemplateView
from la_chariotte import settings
from la_chariotte.order.views import JoinGroupedOrderView
from la_chariotte.order.views.stats import stats
urlpatterns = [
path("admin/", admin.site.urls),
@ -62,11 +61,6 @@ urlpatterns = [
TemplateView.as_view(template_name="help/notice.html"),
name="notice",
),
path(
"faq",
TemplateView.as_view(template_name="help/faq.html"),
name="faq",
),
path(
"mentions-legales",
TemplateView.as_view(
@ -89,5 +83,4 @@ urlpatterns = [
),
name="about_chariotte",
),
path("stats/", stats, name="stats"),
]

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://framagit.org/la-chariotte/la-chariotte
repo_url: https://gitlab.com/la-chariotte/la_chariotte
nav:
- How-tos:
- Getting started: install.md

View file

@ -50,7 +50,7 @@ dev = [
]
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "local_settings"
DJANGO_SETTINGS_MODULE = "la_chariotte.settings"
addopts = "-x --ff --isort --black --reuse-db --cov-report xml --cov-report term-missing --cov=la_chariotte -p no:warnings"
isort_ignore = ["*migrations/*.py"]

218
requirements-dev.txt Normal file
View file

@ -0,0 +1,218 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --extra=dev --output-file=requirements-dev.txt
#
arabic-reshaper==3.0.0
# via xhtml2pdf
asgiref==3.6.0
# via django
asn1crypto==1.5.1
# via
# oscrypto
# pyhanko
# pyhanko-certvalidator
attrs==22.2.0
# via pytest
base36==0.1.1
# via la-chariotte (pyproject.toml)
black==23.3.0
# via pytest-black
brotli==1.0.9
# via fonttools
build==0.10.0
# via pip-tools
certifi==2023.7.22
# via
# requests
# sentry-sdk
cffi==1.15.1
# via
# cryptography
# weasyprint
chardet==4.0.0
# via diff-cover
charset-normalizer==3.2.0
# via requests
click==8.1.3
# via
# black
# pip-tools
# pyhanko
coverage[toml]==7.2.2
# via pytest-cov
cryptography==41.0.3
# via
# pyhanko
# pyhanko-certvalidator
cssselect2==0.7.0
# via
# svglib
# weasyprint
diff-cover==4.2.3
# via la-chariotte (pyproject.toml)
django==4.2
# via
# django-weasyprint
# la-chariotte (pyproject.toml)
django-weasyprint==2.2.1
# via la-chariotte (pyproject.toml)
fonttools[woff]==4.42.1
# via weasyprint
html2text==2020.1.16
# via la-chariotte (pyproject.toml)
html5lib==1.1
# via
# weasyprint
# xhtml2pdf
icalendar==5.0.7
# via la-chariotte (pyproject.toml)
idna==3.4
# via requests
importlib-metadata==6.1.0
# via inflect
inflect==3.0.2
# via
# diff-cover
# jinja2-pluralize
iniconfig==2.0.0
# via pytest
isort==5.12.0
# via pytest-isort
jinja2==3.1.2
# via
# diff-cover
# jinja2-pluralize
jinja2-pluralize==0.3.0
# via diff-cover
lxml==4.9.3
# via svglib
markupsafe==2.1.2
# via jinja2
mypy-extensions==1.0.0
# via black
oscrypto==1.3.0
# via pyhanko-certvalidator
packaging==23.0
# via
# black
# build
# pytest
pathspec==0.11.1
# via black
pillow==10.0.0
# via
# reportlab
# weasyprint
# xhtml2pdf
pip-tools==6.12.3
# via la-chariotte (pyproject.toml)
platformdirs==3.2.0
# via black
pluggy==1.0.0
# via
# diff-cover
# pytest
psycopg2-binary==2.9.6
# via la-chariotte (pyproject.toml)
pycparser==2.21
# via cffi
pydyf==0.7.0
# via weasyprint
pygments==2.14.0
# via diff-cover
pyhanko==0.20.0
# via xhtml2pdf
pyhanko-certvalidator==0.23.0
# via
# pyhanko
# xhtml2pdf
pypdf==3.15.4
# via xhtml2pdf
pyphen==0.14.0
# via weasyprint
pypng==0.20220715.0
# via qrcode
pyproject-hooks==1.0.0
# via build
pytest==7.2.2
# via
# la-chariotte (pyproject.toml)
# pytest-black
# pytest-cov
# pytest-django
# pytest-isort
pytest-black==0.3.12
# via la-chariotte (pyproject.toml)
pytest-cov==4.0.0
# via la-chariotte (pyproject.toml)
pytest-django==4.5.2
# via la-chariotte (pyproject.toml)
pytest-isort==3.1.0
# via la-chariotte (pyproject.toml)
python-bidi==0.4.2
# via xhtml2pdf
python-dateutil==2.8.2
# via icalendar
pytz==2023.3
# via icalendar
pyyaml==6.0.1
# via pyhanko
qrcode==7.4.2
# via pyhanko
reportlab==3.6.13
# via
# svglib
# xhtml2pdf
requests==2.31.0
# via
# pyhanko
# pyhanko-certvalidator
sentry-sdk==0.20.3
# via la-chariotte (pyproject.toml)
six==1.16.0
# via
# html5lib
# python-bidi
# python-dateutil
sqlparse==0.4.3
# via django
svglib==1.5.1
# via xhtml2pdf
tinycss2==1.2.1
# via
# cssselect2
# svglib
# weasyprint
toml==0.10.2
# via pytest-black
typing-extensions==4.7.1
# via qrcode
tzlocal==5.0.1
# via pyhanko
uritools==4.0.1
# via pyhanko-certvalidator
urllib3==2.0.4
# via
# requests
# sentry-sdk
weasyprint==59.0
# via django-weasyprint
webencodings==0.5.1
# via
# cssselect2
# html5lib
# tinycss2
wheel==0.40.0
# via pip-tools
xhtml2pdf==0.2.11
# via la-chariotte (pyproject.toml)
zipp==3.15.0
# via importlib-metadata
zopfli==0.2.2
# via fonttools
# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools

139
requirements.txt Normal file
View file

@ -0,0 +1,139 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --output-file=requirements.txt
#
arabic-reshaper==3.0.0
# via xhtml2pdf
asgiref==3.7.2
# via django
asn1crypto==1.5.1
# via
# oscrypto
# pyhanko
# pyhanko-certvalidator
base36==0.1.1
# via la-chariotte (pyproject.toml)
brotli==1.0.9
# via fonttools
certifi==2023.5.7
# via
# requests
# sentry-sdk
cffi==1.15.1
# via
# cryptography
# weasyprint
charset-normalizer==3.1.0
# via requests
click==8.1.3
# via pyhanko
cryptography==40.0.2
# via
# pyhanko
# pyhanko-certvalidator
cssselect2==0.7.0
# via
# svglib
# weasyprint
django==4.2.1
# via
# django-weasyprint
# la-chariotte (pyproject.toml)
django-weasyprint==1.1.0.post1
# via la-chariotte (pyproject.toml)
fonttools[woff]==4.42.1
# via weasyprint
html2text==2020.1.16
# via la-chariotte (pyproject.toml)
html5lib==1.1
# via
# weasyprint
# xhtml2pdf
icalendar==5.0.7
# via la-chariotte (pyproject.toml)
idna==3.4
# via requests
lxml==4.9.2
# via svglib
oscrypto==1.3.0
# via pyhanko-certvalidator
pillow==9.5.0
# via
# reportlab
# weasyprint
# xhtml2pdf
psycopg2-binary==2.9.6
# via la-chariotte (pyproject.toml)
pycparser==2.21
# via cffi
pydyf==0.7.0
# via weasyprint
pyhanko==0.18.1
# via xhtml2pdf
pyhanko-certvalidator==0.22.0
# via
# pyhanko
# xhtml2pdf
pypdf==3.9.0
# via xhtml2pdf
pyphen==0.14.0
# via weasyprint
pypng==0.20220715.0
# via qrcode
python-bidi==0.4.2
# via xhtml2pdf
python-dateutil==2.8.2
# via icalendar
pytz==2023.3
# via icalendar
pyyaml==6.0
# via pyhanko
qrcode==7.4.2
# via pyhanko
reportlab==3.6.13
# via
# svglib
# xhtml2pdf
requests==2.31.0
# via
# pyhanko
# pyhanko-certvalidator
sentry-sdk==0.20.3
# via la-chariotte (pyproject.toml)
six==1.16.0
# via
# html5lib
# python-bidi
# python-dateutil
sqlparse==0.4.4
# via django
svglib==1.5.1
# via xhtml2pdf
tinycss2==1.2.1
# via
# cssselect2
# svglib
# weasyprint
typing-extensions==4.6.2
# via qrcode
tzlocal==5.0.1
# via pyhanko
uritools==4.0.1
# via pyhanko-certvalidator
urllib3==2.0.2
# via
# requests
# sentry-sdk
weasyprint==52.5
# via django-weasyprint
webencodings==0.5.1
# via
# cssselect2
# html5lib
# tinycss2
xhtml2pdf==0.2.11
# via la-chariotte (pyproject.toml)
zopfli==0.2.2
# via fonttools

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long