Compare commits

...

57 commits

Author SHA1 Message Date
Ploc
6b9e3a6e96 feat: add a perfect bug report template 2025-04-09 22:31:08 +02: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
Laetitia
d354708ffe small changes on notice 2025-02-16 10:07:19 +00:00
Laetitia
2ec833fe42 add questions and yellow borders 2025-02-16 10:07:19 +00:00
Laetitia
039ee1ecb8 changer one title of notice 2025-02-16 10:07:19 +00:00
Laetitia
460fe14306 remove statics dans media from gitignore 2025-02-16 10:07:19 +00:00
Laetitia
f67b5f1de0 feat: improve notice and FAQ 2025-02-16 10:07:19 +00:00
Laetitia
8d2a3cf89f reformulation reponse faq et création liens feedback et HelloAsso dans les settings 2025-02-16 10:07:19 +00:00
Laetitia
e6be787798 ajout de la FAQ
contenu à relire, et front à amélirer
2025-02-16 10:07:19 +00:00
Laetitia
34fc3d6426 mise en page du mode d'emploi
Il faut encore écrire la fin du tuto, en copiant depuis jean cloud
2025-02-16 10:07:19 +00:00
Laetitia
b3cc0085ff Make phone not mandatory 2025-02-15 17:43:30 +01:00
xmeunier
b4311f1401 Fix pdf export layout 2025-02-15 14:05:03 +00:00
xmeunier
272df57664 Remove attributes 'target="_blank"' attributes from <a> tags 2025-01-05 13:56:18 +00:00
xmeunier
18249653f9 Prevent conflicting usage of comma in CSV files 2025-01-05 13:46:40 +01:00
xmeunier
10aed1560b Remove useless chariotte-v0-data.sql file 2024-11-05 21:21:13 +01:00
xmeunier
686a31b96a Make aggregated values computed when called instead of stored in DB
- GroupedOrder.total_price
- Order.articles_nb
- Order.price
- Item.ordered_nb
2024-11-05 11:01:02 +00:00
3d38bb56c0 Populate the changelog and bump the version to 1.3.0 2024-11-05 11:00:21 +00:00
Ploc
99db86153d refactor: change license file from text to markdown
Change license file from text to markdown as markdown adds some markup that makes it easier to read than text.
2024-11-02 15:04:01 +00:00
Ploc
8741d6d9b6 fix: license text
Copy license text from https://www.gnu.org/licenses/agpl-3.0.txt
2024-11-02 15:04:01 +00:00
xmeunier
c0c2d40286 Fix change-password and Connect buttons 2024-10-29 22:17:52 +01:00
28290f20de
Fix the password reset form
Django crispy is bow using a filter rather than a tag.

Using the `crispy` tag resulted in two imbricated forms being
generated, and a broken "send" button.
2024-10-27 23:18:58 +01:00
xmeunier
44a2c20f95 Simpler solution to prevent unitended OrderAuthor 2024-10-20 15:06:10 +02:00
mobilinux
84dadd9147 Prevent inintended creation of OrderAuthor before ordering
When a grouped_order_detail view was called with authenticated user,
when prefilling the firstName, lastName and email also inserted a new
OrderAuthor object in the database.
Now use SimpleNamespace, instead of OrderAuthor object, to carry
those values to the template.
2024-10-20 12:50:55 +00:00
xmeunier
34d2db8623 Add summed nb of ordered items of each article in a grouped order 2024-10-20 11:58:05 +00:00
Laetitia
dacafb8c70 utilisation de GITLAB_LINK dans le footer 2024-09-14 19:39:44 +00:00
Laetitia
e77c4d46d8 mise à jour du lien vers le gitlab 2024-09-14 19:39:44 +00:00
mobilinux
f378b1516a Move newsletter link to "Suivez nous" column 2024-09-14 15:05:32 +02:00
mobilinux
66aeea6ad0 Reformat html code in footer 2024-09-14 12:44:43 +00:00
mobilinux
5b95cfe843 Add link to subscribe to newsletter into footer 2024-09-14 12:44:43 +00:00
mobilinux
2a3b7d467c Revert "Add to footer the link to subscribe to Chariotte News"
This reverts commit 502555c90e3e993750708b539af634874ffa8435.
2024-09-14 12:44:43 +00:00
mobilinux
6b7bd335d0 Add to footer the link to subscribe to Chariotte News 2024-09-14 12:44:43 +00:00
mobilinux
9b6cc7604c Remove header in the CSV export of email addressese 2024-09-14 12:29:37 +00:00
mobilinux
193114e1e9 Remove order_name from groupOrder emails export
This column was quite useless, since the export grouped order email addresses is
done for a given grouped order, so the value was the same throughout the CSV.
2024-09-14 12:29:37 +00:00
Laetitia
20823580a9 feat: modification de l'affiche de la date et heure de limite de commande 2024-09-14 10:56:38 +00:00
Laetitia
b57ebafca8 feat: ajout de la date dans la page de gestion de la commande groupée 2024-09-14 10:56:38 +00:00
Laetitia
f242870d50 feat: modification du format de date et heure dans le csv 2024-09-14 10:56:38 +00:00
pauline
8b79492508 correct test unit 2024-09-14 10:56:38 +00:00
pauline
39dcaf637d add date and note in csv 2024-09-14 10:56:38 +00:00
pauline
d20cafe5db commit for rebase 2024-09-14 10:56:38 +00:00
pauline
4181588a3a save 2024-09-14 10:56:38 +00:00
Laetitia
bcfb62e740 Revert "change html classes for appearence in 'voir'"
This reverts commit 1d7412d7109493e7978140237346d43826a3aebc.
2024-09-14 10:56:38 +00:00
4fe09ea223
Bump version to 1.2.0 2024-07-12 00:10:33 +02:00
Bastien Roy
12f91b55bd 63 autocomplete personal informations 2024-06-24 17:00:03 +00:00
Laetitia
d2053c7da9 add buttons for helloAsso and feedbacks form in footer 2024-05-21 19:52:39 +02:00
a1c179f612 feat: add a /stats endpoint 2024-05-06 16:52:51 +00:00
d67bbc7d8c docs: move sendria setup to the install guide 2024-05-06 16:51:26 +00:00
91d2918586 Update the deployment instructions 2024-04-15 08:53:58 +00:00
51a6966390 chore: remove requirements.txt files 2024-04-15 08:53:25 +00:00
Laetitia
b9e66a5f2b rename mixin and files 2024-04-13 19:07:54 +02:00
Laetitia
6b3b361e07 feat 159: allow staff users to see grouped order overview pages 2024-04-13 19:07:54 +02:00
Bastien Roy
b03d37ee7c Modification de la doc pour préciser le formatage du fichier de settings 2024-04-13 16:25:08 +00:00
Bastien Roy
5b473f0cb3 Changement de settings pour les tests pour avoir les local_settings 2024-04-13 16:25:08 +00:00
5e5a462e45
chore: regroup all the tests in the test subfolder. 2024-04-13 17:15:46 +02:00
447fd88026 chore: main branch doesn't exist anymore 2024-03-20 17:27:14 +01:00
Bastien Roy
59e07f370b 158 rebase 2024-03-09 00:39:51 +01:00
e5de39e568 Rework the footer 2024-02-05 21:35:31 +01:00
74 changed files with 9509 additions and 2816 deletions

4
.gitignore vendored
View file

@ -3,10 +3,8 @@ 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,8 +26,7 @@ tests:
- pip cache purge
- pip install -e ".[dev]"
- pytest --create-db --cov --cov-report=xml
- 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
- 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
- git fetch origin develop ; diff-cover coverage.xml --fail-under=90 --compare-branch origin/develop --diff-range-notation '..'
- echo "Tests done."

View file

@ -0,0 +1,97 @@
# 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,4 +1,43 @@
# Changelog
- v1 :
- Basic functionalities for grouped orders
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.

1324
LICENSE

File diff suppressed because it is too large Load diff

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://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
@ -55,16 +55,6 @@ 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://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
@ -144,9 +144,12 @@ 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 updatedb
python manage.py collectstatic
python manage.py migrate --settings=prod_settings
python manage.py compilescss --settings=prod_settings
python manage.py collectstatic --settings=prod_settings
```
Then you'll need to restart the server from AD's interface.

View file

@ -22,22 +22,21 @@ classDiagram
name
deadline : DateTime
delivery_date : Date
delivery_slot
place
description
orga : CustomUser
total_price
}
class Item{
name
grouped_order : GroupedOrder
ordered_nb
total_price
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 git@gitlab.com:la-chariotte/la_chariotte.git
git clone https://framagit.org/la-chariotte/la-chariotte.git
```
## Virtual environment
@ -73,7 +73,7 @@ CREATE DATABASE chariotte
## Create a configuration file
Create a local configuration file:
Create a local configuration file named "local_settings.py" at the project root :
```python title="local_settings.py"
from la_chariotte.settings import *
@ -102,3 +102,13 @@ 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.1.0"
__version__ = "1.3.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>
<button class="button is-primary" href="{% url 'accounts:login'%}">Se connecter</button>
<a class="button is-primary" href="{% url 'accounts:login' %}">Se connecter</a>
</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,7 +8,8 @@
<div class="box">
<form method="POST">
<p>Entrez votre mail pour recevoir les instructions pour le réinitialiser.</p>
{% crispy form %}
{{ form | crispy }}
{% csrf_token %}
<div class="buttons">
<input class="button is-primary" type="submit" value="Envoyer">
</div>

View file

@ -4,6 +4,7 @@ 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
@ -23,6 +24,18 @@ 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)
@ -39,6 +52,19 @@ 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

@ -18,6 +18,10 @@ 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
@ -29,6 +33,7 @@ class GroupedOrderForm(forms.ModelForm):
"delivery_slot",
"place",
"description",
"is_phone_mandatory",
]
widgets = {
"name": forms.TextInput(

View file

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

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

@ -0,0 +1,20 @@
# 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,8 +25,10 @@ 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
@ -52,16 +54,18 @@ class GroupedOrder(models.Model):
)
self.code = f"{base_36_pk}{random_string}"[:code_length]
def compute_total_price(self):
@property
def total_price(self):
price = 0
for order in self.order_set.all():
price += order.price
self.total_price = price
self.save()
return price
def compute_items_ordered_nb(self):
def get_total_ordered_items(self):
total_nb = 0
for item in self.item_set.all():
item.compute_ordered_nb()
total_nb += item.ordered_nb
return total_nb
def is_open(self):
return self.deadline >= timezone.now()
@ -97,8 +101,6 @@ 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(
@ -119,26 +121,24 @@ class Order(models.Model):
GroupedOrder, on_delete=models.CASCADE, related_name="order_set"
)
author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE)
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)
created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True)
note = models.TextField(max_length=200, null=True, blank=True)
def compute_order_articles_nb(self):
@property
def 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
self.articles_nb = articles_nb
self.save()
return articles_nb
def compute_order_price(self):
@property
def price(self):
"""Computes the total price of the order"""
price = 0
for ord_item in self.ordered_items.all():
price += ord_item.get_price()
self.price = price
self.save()
return price
def __str__(self): # pragma: no cover
return (
@ -153,15 +153,13 @@ class Item(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2)
max_limit = models.PositiveSmallIntegerField(null=True, blank=True)
ordered_nb = models.IntegerField(default=0)
def compute_ordered_nb(self):
@property
def 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
self.ordered_nb = ordered_nb
self.save()
return ordered_nb
def get_total_price(self):
"""Returns the total price of all orders on this item"""

View file

@ -1,5 +1,7 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Nouvelle commande groupée{% endblock %}
{% block content %}
@ -10,8 +12,9 @@
<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.as_p }}
<form method="post" onsubmit="deadlinePassedCheck(event)">
{% csrf_token %}
{{ form | crispy }}
<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 }}
Commandes avant le {{ grouped_order.deadline|date:'d M Y' }} à {{ grouped_order.deadline|date:'H:i' }}
</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 %}">
<form id="inputs-parent" method="post" action="{% url 'order:order' grouped_order.code %}" onSubmit="document.getElementById('submit').disabled=true;">
{% csrf_token %}
<!-- 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,25 +152,27 @@
<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="{{ author.first_name }}" required></p>
value="{{ order_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="{{ author.last_name }}" required></p>
value="{{ order_author.last_name }}" required></p>
</div>
<div class="column">
<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="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="email">Adresse mail : </label>
<input id="email" type="email" placeholder="exemple@mail.fr" name="email"
value="{{ author.email }}" required></p>
value="{{ order_author.email }}" required></p>
</div>
</div>
<p><label for="note">Note à l'organisateur·ice</label>
<textarea id="note" rows=3 placeholder="(facultatif)" name="note">{{ note }}</textarea></p>
<p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label>
<textarea id="note" rows=3 name="note">{{ note }}</textarea></p>
<div class="buttons">
<button type="submit" value="Order" class="button is-primary">
<button id="submit" 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>
@ -186,7 +188,6 @@
// 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 }}
Commandes avant le {{ grouped_order.deadline|date:'d M Y' }} à {{ grouped_order.deadline|date:'H:i' }}
</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 %}" target="_blank">
<a class="button is-info" href="{% url 'order:grouped_order_sheet' grouped_order.code %}">
<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 %}" target="_blank">
<a class="button is-info" href="{% url 'order:grouped_order_csv_export' grouped_order.code %}">
<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></th>
<th>{{ total_ordered_items }}</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" target="_blank">
<a class="button is-info" href="{% url 'order:email_list' grouped_order.code %}?format=csv">
<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')" target="_blank">
<a class="button is-info" onclick="copyText('email_list')">
<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></td>
<td><a href="mailto:{{ order.author.email }}">{{ order.author.email }}</a>{% if order.author.phone %} / {{ order.author.phone }}{% endif %}</td>
<td>
<button class="button is-info is-small js-modal-trigger" data-target="order-detail-{{ order.id }}">
Voir
@ -180,43 +180,36 @@
<!-- 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">Commande de {{ order.author }}</p>
<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>
</div>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="columns is-multiline">
<div class="column is-full">
<div class="columns">
<div class="column">
{% for item in order.ordered_items.all %}
{{ item.nb }} × {{ item.item.name }}
<hr class="mb-1 mt-1">
<hr class="mb-0 mt-1">
{% endfor %}
</div>
{% if order.note %}
<div class="column is-full">
<div class="column is-4">
<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">
<div class="columns">
<div class="column is-half">
<p class="mini-title">Total à payer</p>
<p>{{ order.price }} €</p>
</div>
</div>
<p class="mini-title">Total à payer</p>
<p>{{ order.price }} €</p>
</footer>
</div>
</div>
</div>
<!-- Confirm delete order modal -->
<div id="confirm-delete-{{ order.id }}" class="modal">

View file

@ -21,26 +21,22 @@
}
{% 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 {
margin:-10px -120% ;
display:inline-block;
}
.item_name div:before{
content:'';
width:0;
padding-top:110%;
display:inline-block;
vertical-align:middle;
width:200px;
-webkit-transform: translateX(-40%);
-moz-transform: translateX(-40%);
-ms-transform: translateX(-40%);
-o-transform: translateX(-40%);
transform: translateX(-40%);
}
{% endif %}
@ -53,20 +49,19 @@
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;
}
@ -85,7 +80,7 @@
<thead>
<tr>
<th style="font-size: 0.5em; width: 2em">OK</th>
<th style="text-align: center">Nom</th>
<th style="text-align: center; width: 20%">Nom</th>
{% for item in items %}
<th class="item_name" style="font-weight: normal;">
<div>{{ item.name }}</div>

View file

@ -1,5 +1,7 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Modifier la commande groupée{% endblock %}
{% block content %}
@ -9,13 +11,14 @@
<div class="box">
<p class="title">{{ grouped_order.name }} - modifier</p>
<form method="post" onsubmit="deadlinePassedCheck(event)">{% csrf_token %}
{{ form.as_p }}
{{ form | crispy }}
<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

@ -13,6 +13,7 @@ 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):
@ -111,23 +112,36 @@ class GroupedOrderDetailView(generic.DetailView):
return get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
def get_context_data(self, **kwargs):
items = self.get_object().item_set.all()
grouped_order = self.get_object()
items = grouped_order.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(UserPassesTestMixin, generic.DetailView):
class GroupedOrderOverview(UserIsOrgaMixin, generic.DetailView):
model = GroupedOrder
template_name = "order/grouped_order_overview.html"
context_object_name = "grouped_order"
@ -136,14 +150,8 @@ class GroupedOrderOverview(UserPassesTestMixin, generic.DetailView):
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):
# 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)
# Staff can see but not edit grouped orders
return super().test_func() or self.request.user.is_staff
def get_context_data(self, **kwargs):
context = super(GroupedOrderOverview, self).get_context_data(**kwargs)
@ -155,6 +163,7 @@ class GroupedOrderOverview(UserPassesTestMixin, 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
@ -175,7 +184,7 @@ class GroupedOrderCreateView(LoginRequiredMixin, generic.CreateView):
return super().form_valid(form)
class GroupedOrderUpdateView(UserPassesTestMixin, generic.UpdateView):
class GroupedOrderUpdateView(UserIsOrgaMixin, generic.UpdateView):
model = GroupedOrder
template_name = "order/grouped_order_update.html"
context_object_name = "grouped_order"
@ -184,24 +193,16 @@ class GroupedOrderUpdateView(UserPassesTestMixin, 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(UserPassesTestMixin, generic.RedirectView):
class GroupedOrderDuplicateView(UserIsOrgaMixin, 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
@ -224,7 +225,6 @@ class GroupedOrderDuplicateView(UserPassesTestMixin, 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(UserPassesTestMixin, generic.RedirectView):
)
class GroupedOrderDeleteView(UserPassesTestMixin, generic.DeleteView):
class GroupedOrderDeleteView(UserIsOrgaMixin, generic.DeleteView):
model = GroupedOrder
template_name = "order/grouped_order_confirm_delete.html"
context_object_name = "grouped_order"
@ -249,10 +249,6 @@ class GroupedOrderDeleteView(UserPassesTestMixin, 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()
@ -284,23 +280,27 @@ class GroupedOrderAddItemsView(UserPassesTestMixin, generic.ListView):
return grouped_order.orga == self.request.user
class GroupedOrderExportView(UserPassesTestMixin, generic.DetailView):
class GroupedOrderExportView(UserIsOrgaMixin, 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 = grouped_order.item_set.filter(ordered_nb__gt=0).order_by("name")
items = [
item
for item in grouped_order.item_set.all().order_by("name")
if item.ordered_nb > 0
]
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
# Restrict access to the manager, superuser and staff
origin = get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
return origin.orga == self.request.user
return origin.orga == self.request.user or self.request.user.is_staff
def get(self, request, *args, **kwargs):
grouped_order = get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
@ -344,10 +344,9 @@ 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)
writer.writerow(["order_name", "email"])
writer = csv.writer(response, delimiter=";")
for email in email_list:
writer.writerow([grouped_order.name, email])
writer.writerow([email])
return response
else:
email_list = ";\n".join(email_list)
@ -362,10 +361,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)
writer = csv.writer(response, delimiter=";")
# write headers rows
row = ["", ""]
@ -374,6 +373,9 @@ 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 (€)"]
@ -393,6 +395,9 @@ 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,5 +29,8 @@ 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
return self.get_object().grouped_order.orga == self.request.user
# Restrict access to the manager or a superuser
return (
self.get_object().grouped_order.orga == self.request.user
or self.request.user.is_superuser
)

View file

@ -0,0 +1,13 @@
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,6 +2,7 @@ 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
@ -48,7 +49,12 @@ 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)
order = Order.objects.create(
author=author,
grouped_order=grouped_order,
note=note,
created_date=timezone.now(),
)
# add items to the order
error_message = None
@ -66,7 +72,6 @@ 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",
@ -97,8 +102,6 @@ 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
@ -117,7 +120,6 @@ 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
@ -136,5 +138,8 @@ class OrderDeleteView(UserPassesTestMixin, generic.DeleteView):
)
def test_func(self):
"""Accessible only if the requesting user is the grouped order organizer"""
return self.get_object().grouped_order.orga == self.request.user
# Restrict access to the manager or a superuser
return (
self.get_object().grouped_order.orga == self.request.user
or self.request.user.is_superuser
)

View file

@ -0,0 +1,28 @@
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,8 +7,10 @@ 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/hashbangfr/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"
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv(

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

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

@ -9,4 +9,8 @@
padding-top: $navbar-height
.formatted-text
white-space: pre-wrap
white-space: pre-wrap
img.notice-img
border: $info 3px solid
margin-bottom: 1em

View file

@ -1,6 +1,18 @@
.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
margin: auto
footer .logo
letter-spacing: 1px

View file

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

View file

@ -1,7 +1,6 @@
{% load static %}
{% load settings %}
{% load sass_tags %}
<!DOCTYPE html>
<html lang="fr">
<head>
@ -94,6 +93,9 @@
<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>
@ -121,46 +123,62 @@
{% block content %}{% endblock %}
</div>
</main>
<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>
<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>
</div>
<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>
</div>
</footer>
</body>
</html>
<script>
// For responsive menu
document.addEventListener('DOMContentLoaded', () => {
@ -183,4 +201,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" target="_blank">aller voir le code</a> si vous êtes curieux, et y contribuer si vous êtes motivé·e !
<a href="{{ gitlab }}" rel="noopener">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" target="_blank">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">ici</a>, ou par mail à <a href="mailto:{{ mail }}">{{ mail }}</a>.
</p>
</div>

View file

@ -0,0 +1,241 @@
{% 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" target="_blank">sur Gitlab</a> et ouvert à toute contribution ou suggestion.
consultable <a href="{{ gitlab }}" rel="noopener">sur Gitlab</a> et ouvert à toute contribution ou suggestion.
</p>
<p class="title">Conditions d'utilisation</p>
<p>

View file

@ -9,11 +9,126 @@
{% block content_title %}La Chariotte - mode d'emploi{% endblock %}
</p>
<div class="box">
<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>
<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>
</div>
{% endblock %}

View file

View file

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

View file

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

View file

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

View file

@ -4,16 +4,14 @@ 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.order.tests.utils import (
create_grouped_order,
order_items_in_grouped_order,
)
from la_chariotte.tests.utils import create_grouped_order, order_items_in_grouped_order
pytestmark = pytest.mark.django_db
@ -217,11 +215,32 @@ class TestJoinGroupedOrderView:
response = client.post(join_url, {"code": "123456"})
assert (
"nous ne trouvons aucune commande avec ce code" in response.content.decode()
"Désolé, 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
@ -302,7 +321,9 @@ 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, other_user):
def test_order_item__no_articles_ordered(
self, client, authenticated_user_with_name
):
"""
From the OrderDetailView, we order without having changed any item quantity.
An error is raised.
@ -312,7 +333,7 @@ class TestGroupedOrderDetailView:
days_before_delivery_date=5,
days_before_deadline=2,
name="gr order test",
orga_user=other_user,
orga_user=authenticated_user_with_name,
)
item = models.Item.objects.create(
name="test item 1", grouped_order=grouped_order, price=1
@ -338,10 +359,10 @@ class TestGroupedOrderDetailView:
order_url,
{
f"quantity_{item.pk}": [0, 0],
"first_name": "Prénom test",
"last_name": "Nom test",
"first_name": {authenticated_user_with_name.first_name},
"last_name": {authenticated_user_with_name.last_name},
"phone": "0645632569",
"email": "test@mail.fr",
"email": {authenticated_user_with_name.email},
"note": "test note",
},
)
@ -350,12 +371,14 @@ class TestGroupedOrderDetailView:
response.context["error_message"]
== "Veuillez commander au moins un produit"
)
assert response.context["author"].first_name == "Prénom test"
assert (
response.context["author"].first_name
== authenticated_user_with_name.first_name
)
content = response.content.decode()
assert "Prénom test" in content
assert "Nom test" in content
assert "0645632569" in content
assert "test@mail.fr" in content
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 "test note" in content
assert not models.Order.objects.first()
assert not models.OrderAuthor.objects.first()
@ -639,6 +662,36 @@ 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):
@ -683,6 +736,7 @@ 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
@ -737,6 +791,25 @@ 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
@ -1353,7 +1426,7 @@ class TestGroupedOrderSheetView:
response = client_log.get(generate_sheet_url)
assert response.status_code == 200
assert response.context["grouped_order"] == grouped_order
assert response.context["items"].count() == 0
assert len(response.context["items"]) == 0
assert len(response.context["orders_dict"]) == 0
# we order some items in the grouped order
@ -1361,7 +1434,7 @@ class TestGroupedOrderSheetView:
response = client_log.get(generate_sheet_url)
assert response.status_code == 200
assert response.context["grouped_order"] == grouped_order
assert response.context["items"].count() == 2
assert len(response.context["items"]) == 2
assert response.context["orders_dict"][order] == [3, 2]
assert response.context["grouped_order"].total_price == 35
@ -1479,8 +1552,7 @@ class TestExportGroupOrderEmailAdressesToDownloadView:
assert response.status_code == 200
assert response["Content-Type"] == "text/csv"
content = response.content.decode()
assert "order_name,email" in content
assert "order_name,email\r\ngr order test,test@mail.fr\r\n" in content
assert "test@mail.fr\r\n" in content
def test_export_format_default(self, client_log):
grouped_order = create_grouped_order(
@ -1587,8 +1659,10 @@ class TestExportGroupedOrderToCSVView:
assert response.status_code == 200
content = response.content.decode("utf-8")
csv_reader = csv.reader(StringIO(content))
csv_reader = csv.reader(StringIO(content), delimiter=";")
body = list(csv_reader)
created_date = f"{timezone.now().strftime('%d/%m/%Y')}"
created_time = f"{timezone.now().strftime('%H:%M')}"
assert body == [
[
"",
@ -1598,11 +1672,47 @@ 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"],
["alescargot", "bobby", "0", "1", "9,00", "bob3@escargot.fr", "'000"],
["lescargot", "bob", "3", "2", "24,00", "bob@escargot.fr", "'000"],
[
"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,
],
["", "TOTAL", "4", "3", "35,00"],
]

View file

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

View file

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

View file

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

@ -9,12 +9,20 @@ pytestmark = pytest.mark.django_db
def create_grouped_order(
days_before_delivery_date, days_before_deadline, name, orga_user
days_before_delivery_date,
days_before_deadline,
name,
orga_user,
is_phone_mandatory=True,
):
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
name=name,
orga=orga_user,
delivery_date=date,
deadline=deadline,
is_phone_mandatory=is_phone_mandatory,
)
grouped_order.create_code_from_pk()
grouped_order.save()
@ -44,11 +52,4 @@ 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

@ -25,6 +25,7 @@ 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),
@ -61,6 +62,11 @@ 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(
@ -83,4 +89,5 @@ 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://gitlab.com/la-chariotte/la_chariotte
repo_url: https://framagit.org/la-chariotte/la-chariotte
nav:
- How-tos:
- Getting started: install.md

View file

@ -15,7 +15,7 @@ dependencies = [
"icalendar>=5.0.7,<6",
"base36>=0.1.1,<1",
"django-weasyprint==1.1.0.post1", # see below
"weasyprint==52.5", # pin weasyprint to not depend on latest libpango
"weasyprint==52.5", # pin weasyprint to not depend on latest libpango
"html2text>=2020.1.16",
"django-crispy-forms>=2.0,<3",
"crispy-bulma>=0.11.0,<1",
@ -50,7 +50,7 @@ dev = [
]
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "la_chariotte.settings"
DJANGO_SETTINGS_MODULE = "local_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"]

View file

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

View file

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

7991
static/sass/style.css Normal file

File diff suppressed because it is too large Load diff

81
static/sass/style.css.map Normal file

File diff suppressed because one or more lines are too long