mirror of
https://framagit.org/la-chariotte/la-chariotte.git
synced 2025-05-01 11:22:24 +02:00
Merge branch '186/mettre-le-lien-du-gitlab-dans-le-footer' of gitlab.com:la-chariotte/la_chariotte into 186/mettre-le-lien-du-gitlab-dans-le-footer
This commit is contained in:
commit
b03e67e737
25 changed files with 208 additions and 97 deletions
|
@ -7,6 +7,5 @@ La Chariotte is a web application to handle grouped orders.
|
|||
- [Public server](https://chariotte.fr)
|
||||
- [Online documentation](https://docs.chariotte.fr)
|
||||
|
||||
This project has been initially developped and published by [Hashbang](https://
|
||||
hashbang.fr/) under an [Affero GPLv3](LICENSE) license, and is now maintained
|
||||
and developed by a team of volunteers.
|
||||
This project has been initially developed and published by [Hashbang](https://hashbang.fr/)
|
||||
under an [Affero GPLv3](LICENSE) license, and is now maintained and developed by a team of volunteers.
|
||||
|
|
|
@ -60,3 +60,7 @@ build it locally, just run:
|
|||
```bash
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
## Merging rules
|
||||
|
||||
In order to be merged, your code needs to be reviewed by two maintainers. Don't hesitate to ping us if needed.
|
||||
|
|
|
@ -50,6 +50,61 @@ en/latest/), managed by AlwaysData.
|
|||
The production settings are stored in `~/ la_chariotte/prod_settings.py`, and
|
||||
the secrets are defined in the admin console.
|
||||
|
||||
Here are the settings, with some comments that might be useful.
|
||||
|
||||
```python title="prod_settings.py"
|
||||
SECRET_KEY = "YOUR SECRET KEY HERE, used to hash the passwords. CHANGE IT."
|
||||
|
||||
# We're connecting to a psql server, AD manages the access and the backups.
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": "chariotte_prod",
|
||||
"USER": "chariotte_prod",
|
||||
"PASSWORD": "",
|
||||
"HOST": "host",
|
||||
}
|
||||
}
|
||||
|
||||
ALLOWED_HOSTS = ["chariotte.fr",]
|
||||
DEBUG = False
|
||||
|
||||
# We're sending mails using AD infrastructure
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_FROM = 'notification@chariotte.fr'
|
||||
EMAIL_HOST = 'smtp-chariotte.alwaysdata.net'
|
||||
EMAIL_PORT = 587
|
||||
EMAIL_HOST_USER = 'notification@chariotte.fr'
|
||||
EMAIL_HOST_PASSWORD = "XXX"
|
||||
EMAIL_USE_TLS = True
|
||||
|
||||
DEFAULT_FROM_EMAIL = os.getenv(
|
||||
"DJANGO_DEFAULT_FROM_EMAIL", "La Chariotte <notification@chariotte.fr>"
|
||||
)
|
||||
|
||||
CONTACT_MAIL = "contact@chariotte.fr"
|
||||
|
||||
# We're collecting the static files on this specific folder.
|
||||
STATIC_ROOT = "/home/chariotte/static/"
|
||||
```
|
||||
|
||||
We're using sentry (sentry.io) to be alerted when an error happens on the server
|
||||
|
||||
```python title="prod_settings.py"
|
||||
import sentry_sdk
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn="PUT YOUR DSN HERE",
|
||||
# Set traces_sample_rate to 1.0 to capture 100%
|
||||
# of transactions for performance monitoring.
|
||||
traces_sample_rate=1.0,
|
||||
# Set profiles_sample_rate to 1.0 to profile 100%
|
||||
# of sampled transactions.
|
||||
# We recommend adjusting this value in production.
|
||||
profiles_sample_rate=1.0,
|
||||
)
|
||||
```
|
||||
|
||||
### The different sites
|
||||
|
||||
In the AD console, here are the defined sites:
|
||||
|
|
|
@ -3,6 +3,8 @@ from django.db import models
|
|||
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
EMAIL_FIELD = "username"
|
||||
|
||||
username = models.EmailField(
|
||||
"Email",
|
||||
unique=True,
|
||||
|
|
|
@ -2,7 +2,6 @@ import pytest
|
|||
from django.urls import reverse
|
||||
|
||||
from la_chariotte import settings
|
||||
from la_chariotte.order.models import GroupedOrder
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import html2text
|
|||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
|
||||
def send_order_confirmation_mail(order):
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
import datetime
|
||||
from typing import Any, Optional, Sequence, Type, Union
|
||||
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
||||
from django.db import IntegrityError
|
||||
from django.forms.utils import to_current_timezone
|
||||
from django.forms.widgets import Widget
|
||||
from django.utils import timezone
|
||||
|
||||
from la_chariotte import settings
|
||||
from la_chariotte.order.models import GroupedOrder, Item
|
||||
|
||||
|
||||
|
|
|
@ -5,27 +5,52 @@ import django.db.models.deletion
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Grouped_order',
|
||||
name="Grouped_order",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('orga', models.CharField(max_length=100)),
|
||||
('date', models.DateField(verbose_name='Date de livraison')),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("orga", models.CharField(max_length=100)),
|
||||
("date", models.DateField(verbose_name="Date de livraison")),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
name="Order",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('author', models.CharField(max_length=100, verbose_name='Personne qui passe la commande')),
|
||||
('grouped_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='order.grouped_order')),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"author",
|
||||
models.CharField(
|
||||
max_length=100, verbose_name="Personne qui passe la commande"
|
||||
),
|
||||
),
|
||||
(
|
||||
"grouped_order",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="order.grouped_order",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -4,15 +4,14 @@ from django.db import migrations, models
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0001_initial'),
|
||||
("order", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='grouped_order',
|
||||
name='name',
|
||||
model_name="grouped_order",
|
||||
name="name",
|
||||
field=models.CharField(max_length=100, null=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -5,18 +5,31 @@ import django.db.models.deletion
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0002_grouped_order_name'),
|
||||
("order", "0002_grouped_order_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Item',
|
||||
name="Item",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('grouped_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='order.grouped_order')),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100)),
|
||||
(
|
||||
"grouped_order",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="order.grouped_order",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -4,15 +4,14 @@ from django.db import migrations, models
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0003_item'),
|
||||
("order", "0003_item"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='ordered_nb',
|
||||
model_name="item",
|
||||
name="ordered_nb",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -5,28 +5,53 @@ import django.db.models.deletion
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0004_item_ordered_nb'),
|
||||
("order", "0004_item_ordered_nb"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='item',
|
||||
name='ordered_nb',
|
||||
model_name="item",
|
||||
name="ordered_nb",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='grouped_order',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_set', to='order.grouped_order'),
|
||||
model_name="order",
|
||||
name="grouped_order",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="order_set",
|
||||
to="order.grouped_order",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrderedItem',
|
||||
name="OrderedItem",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nb', models.PositiveSmallIntegerField(default=0)),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ordered_items', to='order.item')),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='order.order')),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("nb", models.PositiveSmallIntegerField(default=0)),
|
||||
(
|
||||
"item",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="ordered_items",
|
||||
to="order.item",
|
||||
),
|
||||
),
|
||||
(
|
||||
"order",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="orders",
|
||||
to="order.order",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -4,15 +4,14 @@ from django.db import migrations, models
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0005_remove_item_ordered_nb_alter_order_grouped_order_and_more'),
|
||||
("order", "0005_remove_item_ordered_nb_alter_order_grouped_order_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='ordered_nb',
|
||||
model_name="item",
|
||||
name="ordered_nb",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -5,20 +5,27 @@ import django.db.models.deletion
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0006_item_ordered_nb'),
|
||||
("order", "0006_item_ordered_nb"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ordereditem',
|
||||
name='item',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='order.item'),
|
||||
model_name="ordereditem",
|
||||
name="item",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="orders",
|
||||
to="order.item",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ordereditem',
|
||||
name='order',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ordered_items', to='order.order'),
|
||||
model_name="ordereditem",
|
||||
name="order",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="ordered_items",
|
||||
to="order.order",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -5,16 +5,20 @@ from django.db import migrations, models
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0007_alter_ordereditem_item_alter_ordereditem_order'),
|
||||
("order", "0007_alter_ordereditem_item_alter_ordereditem_order"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='grouped_order',
|
||||
name='deadline',
|
||||
field=models.DateTimeField(default=datetime.datetime(2023, 3, 23, 14, 38, 17, 365192, tzinfo=datetime.timezone.utc), verbose_name='Date limite de commande'),
|
||||
model_name="grouped_order",
|
||||
name="deadline",
|
||||
field=models.DateTimeField(
|
||||
default=datetime.datetime(
|
||||
2023, 3, 23, 14, 38, 17, 365192, tzinfo=datetime.timezone.utc
|
||||
),
|
||||
verbose_name="Date limite de commande",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -4,15 +4,14 @@ from django.db import migrations
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0008_grouped_order_deadline'),
|
||||
("order", "0008_grouped_order_deadline"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='grouped_order',
|
||||
old_name='date',
|
||||
new_name='delivery_date',
|
||||
model_name="grouped_order",
|
||||
old_name="date",
|
||||
new_name="delivery_date",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -7,30 +7,34 @@ import random
|
|||
|
||||
def create_code_from_pk(pk):
|
||||
"""When a grouped order is created, we compute a unique code that will be used in url path
|
||||
How we generate this code :
|
||||
How we generate this code :
|
||||
1. The instance pk, written in base36 (max 5 digits for now - we assume that there will not be more than 60466175 grouped orders)
|
||||
2. A random int written in base36 (5 digits long)
|
||||
3. Only the 6 first digits of this string
|
||||
The use of pk in the beginning of the string guarantees the uniqueness, and the random part makes that we cannot guess the url path.
|
||||
The use of pk in the beginning of the string guarantees the uniqueness, and the random part makes that we cannot guess the url path.
|
||||
"""
|
||||
base_36_pk = base36.dumps(pk)
|
||||
random_string = base36.dumps(random.randint(1727605,60466175)) # generates a 5 digits long string
|
||||
random_string = base36.dumps(
|
||||
random.randint(1727605, 60466175)
|
||||
) # generates a 5 digits long string
|
||||
return f"{base_36_pk}{random_string}"
|
||||
|
||||
|
||||
def set_code_default(apps, schema_editor):
|
||||
"""Provides a default code to existing grouped orders during migration"""
|
||||
GroupedOrder = apps.get_model("order","GroupedOrder")
|
||||
GroupedOrder = apps.get_model("order", "GroupedOrder")
|
||||
for grouped_order in GroupedOrder.objects.all().iterator():
|
||||
grouped_order.code = create_code_from_pk(grouped_order.pk)
|
||||
grouped_order.save()
|
||||
|
||||
|
||||
def reverse_set_code_default(apps, schema_editor):
|
||||
"""Reverse the set_code default function"""
|
||||
GroupedOrder = apps.get_model("order","GroupedOrder")
|
||||
GroupedOrder = apps.get_model("order", "GroupedOrder")
|
||||
for grouped_order in GroupedOrder.objects.all().iterator():
|
||||
grouped_order.code = ''
|
||||
grouped_order.code = ""
|
||||
grouped_order.save()
|
||||
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
|
|
@ -83,7 +83,8 @@
|
|||
{% if items %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<tr>
|
||||
<th style="font-size: 0.5em; width: 2em">OK</th>
|
||||
<th style="text-align: center">Nom</th>
|
||||
{% for item in items %}
|
||||
<th class="item_name" style="font-weight: normal;">
|
||||
|
@ -91,11 +92,11 @@
|
|||
</th>
|
||||
{% endfor %}
|
||||
<th style="width: 2cm">Prix</th>
|
||||
<th style="font-size: 0.5em; width: 2em">OK</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="background-color: #bababa">
|
||||
<td></td>
|
||||
<td style="text-align: left">Prix unitaire</td>
|
||||
{% for item in items %}
|
||||
<td>
|
||||
|
@ -103,9 +104,9 @@
|
|||
</td>
|
||||
{% endfor %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr style="background-color: #bababa">
|
||||
<th></th>
|
||||
<th style="text-align: left">TOTAL</th>
|
||||
{% for item in items %}
|
||||
<th>
|
||||
|
@ -113,10 +114,10 @@
|
|||
</th>
|
||||
{% endfor %}
|
||||
<th>{{ grouped_order.total_price }} €</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for order, ordered_items in orders_dict.items %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
{{ order.author.last_name|upper }} {{ order.author.first_name }}
|
||||
</td>
|
||||
|
@ -130,7 +131,6 @@
|
|||
<td>
|
||||
{{ order.price }} €
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from la_chariotte.order import models
|
||||
from la_chariotte.order.tests.utils import create_grouped_order
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from la_chariotte.order import models
|
||||
from la_chariotte.order.tests.utils import (
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from la_chariotte.order import models
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.urls import path
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from . import views
|
||||
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
import csv
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from io import BytesIO
|
||||
|
||||
from django import http
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.views import generic
|
||||
from django_weasyprint import WeasyTemplateResponseMixin
|
||||
from django_weasyprint.views import WeasyTemplateResponse, WeasyTemplateView
|
||||
from icalendar import Calendar, Event, vCalAddress, vText
|
||||
from xhtml2pdf import pisa
|
||||
|
||||
from ..forms import GroupedOrderForm, Item, JoinGroupedOrderForm
|
||||
from ..models import GroupedOrder, OrderAuthor
|
||||
|
|
|
@ -19,7 +19,6 @@ from django.contrib.auth.views import (
|
|||
PasswordResetCompleteView,
|
||||
PasswordResetConfirmView,
|
||||
PasswordResetDoneView,
|
||||
PasswordResetView,
|
||||
)
|
||||
from django.urls import include, path
|
||||
from django.views.generic.base import TemplateView
|
||||
|
|
BIN
mails.sqlite
BIN
mails.sqlite
Binary file not shown.
Loading…
Reference in a new issue