mirror of
https://framagit.org/la-chariotte/la-chariotte.git
synced 2025-05-07 14:21:51 +02:00
Compare commits
5 commits
8590a295a8
...
6a0ed9b60b
Author | SHA1 | Date | |
---|---|---|---|
6a0ed9b60b | |||
![]() |
10aed1560b | ||
![]() |
686a31b96a | ||
9f8be6d52d | |||
5a4260ed42 |
10 changed files with 147 additions and 1269 deletions
87
Dockerfile
87
Dockerfile
|
@ -1,16 +1,81 @@
|
||||||
FROM python:3.11-slim
|
# Stage 1: General debian environment
|
||||||
|
FROM debian:stable-slim AS linux-base
|
||||||
|
|
||||||
WORKDIR /srv/app
|
# Assure UTF-8 encoding is used.
|
||||||
|
ENV LC_CTYPE=C.utf8
|
||||||
|
# Location of the virtual environment
|
||||||
|
ENV UV_PROJECT_ENVIRONMENT="/venv"
|
||||||
|
# Location of the python installation via uv
|
||||||
|
ENV UV_PYTHON_INSTALL_DIR="/python"
|
||||||
|
# Byte compile the python files on installation
|
||||||
|
ENV UV_COMPILE_BYTECODE=1
|
||||||
|
# Python verision to use
|
||||||
|
ENV UV_PYTHON=python3.12
|
||||||
|
# Tweaking the PATH variable for easier use
|
||||||
|
ENV PATH="$UV_PROJECT_ENVIRONMENT/bin:$PATH"
|
||||||
|
|
||||||
RUN apt update && \
|
# Update debian
|
||||||
apt install --no-install-recommends -y libpq-dev build-essential curl gcc sassc libpango-1.0-0 libpangoft2-1.0-0 libjpeg-dev libopenjp2-7-dev libffi-dev libpangoft2-1.0-0 libcairo2 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 shared-mime-info gettext &&\
|
RUN apt-get update
|
||||||
rm -rf /var/lib/apt/lists/*
|
RUN apt-get upgrade -y
|
||||||
|
|
||||||
COPY --chown=www-data:www-data . .
|
# Install general required dependencies
|
||||||
|
RUN apt-get install --no-install-recommends -y tzdata \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libpangoft2-1.0-0 \
|
||||||
|
libjpeg-dev \
|
||||||
|
libopenjp2-7-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libpangoft2-1.0-0 \
|
||||||
|
libcairo2 \
|
||||||
|
libpangocairo-1.0-0 \
|
||||||
|
libgdk-pixbuf2.0-0 \
|
||||||
|
shared-mime-info \
|
||||||
|
gettext \
|
||||||
|
tini
|
||||||
|
|
||||||
run python -m pip install --upgrade pip \
|
# Stage 2: Python environment
|
||||||
&& python -m pip install uwsgi \
|
FROM linux-base AS python-base
|
||||||
&& python -m pip install -r requirements.txt \
|
|
||||||
&& python -m pip install .
|
|
||||||
|
|
||||||
ENTRYPOINT [ "uwsgi", "-i", "uwsgi.ini" ]
|
# Install debian dependencies
|
||||||
|
RUN apt-get install --no-install-recommends -y \
|
||||||
|
libpq-dev \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
gcc \
|
||||||
|
sassc
|
||||||
|
|
||||||
|
# Install uv
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||||
|
|
||||||
|
# Create virtual environment and install dependencies
|
||||||
|
COPY pyproject.toml ./
|
||||||
|
COPY uv.lock ./
|
||||||
|
RUN uv sync --frozen --no-dev --no-install-project
|
||||||
|
RUN uv pip install granian
|
||||||
|
|
||||||
|
# Stage 3: Building environment
|
||||||
|
FROM python-base AS builder-base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# Build static files
|
||||||
|
RUN python manage.py compilescss
|
||||||
|
RUN python manage.py collectstatic --no-input
|
||||||
|
|
||||||
|
# Compile translation files
|
||||||
|
# RUN python manage.py compilemessages
|
||||||
|
|
||||||
|
|
||||||
|
# Stage 4: Webapp environment
|
||||||
|
FROM linux-base AS webapp
|
||||||
|
|
||||||
|
# Copy python, virtual env and static assets
|
||||||
|
COPY --from=builder-base $UV_PYTHON_INSTALL_DIR $UV_PYTHON_INSTALL_DIR
|
||||||
|
COPY --from=builder-base $UV_PROJECT_ENVIRONMENT $UV_PROJECT_ENVIRONMENT
|
||||||
|
COPY --from=builder-base /app /app
|
||||||
|
|
||||||
|
# Start the application server
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["docker/entrypoint.sh"]
|
||||||
|
|
File diff suppressed because it is too large
Load diff
14
docker/entrypoint.sh
Executable file
14
docker/entrypoint.sh
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
echo "Migrate database..."
|
||||||
|
python manage.py migrate --settings local_settings
|
||||||
|
|
||||||
|
echo "Start server..."
|
||||||
|
|
||||||
|
granian la_chariotte.wsgi:application \
|
||||||
|
--host 127.0.0.1 \
|
||||||
|
--port 8000 \
|
||||||
|
--interface wsgi \
|
||||||
|
--no-ws \
|
||||||
|
--loop uvloop
|
|
@ -22,22 +22,21 @@ classDiagram
|
||||||
name
|
name
|
||||||
deadline : DateTime
|
deadline : DateTime
|
||||||
delivery_date : Date
|
delivery_date : Date
|
||||||
|
delivery_slot
|
||||||
place
|
place
|
||||||
description
|
description
|
||||||
orga : CustomUser
|
orga : CustomUser
|
||||||
total_price
|
|
||||||
}
|
}
|
||||||
class Item{
|
class Item{
|
||||||
name
|
name
|
||||||
grouped_order : GroupedOrder
|
grouped_order : GroupedOrder
|
||||||
ordered_nb
|
ordered_nb
|
||||||
total_price
|
price
|
||||||
max_limit
|
max_limit
|
||||||
}
|
}
|
||||||
class Order{
|
class Order{
|
||||||
grouped_order : GroupedOrder
|
grouped_order : GroupedOrder
|
||||||
author : OrderAuthor
|
author : OrderAuthor
|
||||||
price
|
|
||||||
created_date
|
created_date
|
||||||
note
|
note
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -25,7 +25,6 @@ class GroupedOrder(models.Model):
|
||||||
max_length=100, null=True, blank=True, verbose_name="Lieu de livraison"
|
max_length=100, null=True, blank=True, verbose_name="Lieu de livraison"
|
||||||
)
|
)
|
||||||
description = models.TextField("Description", null=True, blank=True)
|
description = models.TextField("Description", null=True, blank=True)
|
||||||
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
|
||||||
code = models.CharField(auto_created=True)
|
code = models.CharField(auto_created=True)
|
||||||
|
|
||||||
def create_code_from_pk(self):
|
def create_code_from_pk(self):
|
||||||
|
@ -52,16 +51,12 @@ class GroupedOrder(models.Model):
|
||||||
)
|
)
|
||||||
self.code = f"{base_36_pk}{random_string}"[:code_length]
|
self.code = f"{base_36_pk}{random_string}"[:code_length]
|
||||||
|
|
||||||
def compute_total_price(self):
|
@property
|
||||||
|
def total_price(self):
|
||||||
price = 0
|
price = 0
|
||||||
for order in self.order_set.all():
|
for order in self.order_set.all():
|
||||||
price += order.price
|
price += order.price
|
||||||
self.total_price = price
|
return price
|
||||||
self.save()
|
|
||||||
|
|
||||||
def compute_items_ordered_nb(self):
|
|
||||||
for item in self.item_set.all():
|
|
||||||
item.compute_ordered_nb()
|
|
||||||
|
|
||||||
def get_total_ordered_items(self):
|
def get_total_ordered_items(self):
|
||||||
total_nb = 0
|
total_nb = 0
|
||||||
|
@ -123,26 +118,24 @@ class Order(models.Model):
|
||||||
GroupedOrder, on_delete=models.CASCADE, related_name="order_set"
|
GroupedOrder, on_delete=models.CASCADE, related_name="order_set"
|
||||||
)
|
)
|
||||||
author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE)
|
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 et heure de commande", auto_now_add=True)
|
created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True)
|
||||||
note = models.TextField(max_length=200, null=True, blank=True)
|
note = models.TextField(max_length=200, null=True, blank=True)
|
||||||
|
|
||||||
def compute_order_articles_nb(self):
|
@property
|
||||||
|
def articles_nb(self):
|
||||||
"""Computes the number of articles in this order"""
|
"""Computes the number of articles in this order"""
|
||||||
articles_nb = 0
|
articles_nb = 0
|
||||||
for ord_item in self.ordered_items.all():
|
for ord_item in self.ordered_items.all():
|
||||||
articles_nb += ord_item.nb
|
articles_nb += ord_item.nb
|
||||||
self.articles_nb = articles_nb
|
return articles_nb
|
||||||
self.save()
|
|
||||||
|
|
||||||
def compute_order_price(self):
|
@property
|
||||||
|
def price(self):
|
||||||
"""Computes the total price of the order"""
|
"""Computes the total price of the order"""
|
||||||
price = 0
|
price = 0
|
||||||
for ord_item in self.ordered_items.all():
|
for ord_item in self.ordered_items.all():
|
||||||
price += ord_item.get_price()
|
price += ord_item.get_price()
|
||||||
self.price = price
|
return price
|
||||||
self.save()
|
|
||||||
|
|
||||||
def __str__(self): # pragma: no cover
|
def __str__(self): # pragma: no cover
|
||||||
return (
|
return (
|
||||||
|
@ -157,15 +150,13 @@ class Item(models.Model):
|
||||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
max_limit = models.PositiveSmallIntegerField(null=True, blank=True)
|
max_limit = models.PositiveSmallIntegerField(null=True, blank=True)
|
||||||
|
|
||||||
ordered_nb = models.IntegerField(default=0)
|
@property
|
||||||
|
def ordered_nb(self):
|
||||||
def compute_ordered_nb(self):
|
|
||||||
"""Computes the number of times this item has been ordered"""
|
"""Computes the number of times this item has been ordered"""
|
||||||
ordered_nb = 0
|
ordered_nb = 0
|
||||||
for order in self.orders.all():
|
for order in self.orders.all():
|
||||||
ordered_nb += order.nb
|
ordered_nb += order.nb
|
||||||
self.ordered_nb = ordered_nb
|
return ordered_nb
|
||||||
self.save()
|
|
||||||
|
|
||||||
def get_total_price(self):
|
def get_total_price(self):
|
||||||
"""Returns the total price of all orders on this item"""
|
"""Returns the total price of all orders on this item"""
|
||||||
|
|
|
@ -150,12 +150,6 @@ class GroupedOrderOverview(UserIsOrgaMixin, generic.DetailView):
|
||||||
# Staff can see but not edit grouped orders
|
# Staff can see but not edit grouped orders
|
||||||
return super().test_func() or self.request.user.is_staff
|
return super().test_func() or self.request.user.is_staff
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
# Compute grouped order total price before display
|
|
||||||
self.get_object().compute_total_price()
|
|
||||||
self.get_object().compute_items_ordered_nb()
|
|
||||||
return super().get(self, request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(GroupedOrderOverview, self).get_context_data(**kwargs)
|
context = super(GroupedOrderOverview, self).get_context_data(**kwargs)
|
||||||
# Add share link to context
|
# Add share link to context
|
||||||
|
@ -228,7 +222,6 @@ class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
|
||||||
# duplicate each item and add it to new_grouped_order
|
# duplicate each item and add it to new_grouped_order
|
||||||
for item in initial_grouped_order.item_set.all():
|
for item in initial_grouped_order.item_set.all():
|
||||||
item.pk = None
|
item.pk = None
|
||||||
item.ordered_nb = 0
|
|
||||||
item.save()
|
item.save()
|
||||||
new_grouped_order.item_set.add(item)
|
new_grouped_order.item_set.add(item)
|
||||||
|
|
||||||
|
@ -300,7 +293,11 @@ class GroupedOrderExportView(UserIsOrgaMixin, generic.DetailView):
|
||||||
context = super(GroupedOrderExportView, self).get_context_data(**kwargs)
|
context = super(GroupedOrderExportView, self).get_context_data(**kwargs)
|
||||||
grouped_order = self.get_object()
|
grouped_order = self.get_object()
|
||||||
|
|
||||||
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(
|
orders = grouped_order.order_set.all().order_by(
|
||||||
"author__last_name", "author__first_name"
|
"author__last_name", "author__first_name"
|
||||||
)
|
)
|
||||||
|
|
|
@ -72,7 +72,6 @@ def place_order(request, code):
|
||||||
if error_message:
|
if error_message:
|
||||||
order.delete()
|
order.delete()
|
||||||
author.delete()
|
author.delete()
|
||||||
grouped_order.compute_items_ordered_nb()
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"order/grouped_order_detail.html",
|
"order/grouped_order_detail.html",
|
||||||
|
@ -103,8 +102,6 @@ def place_order(request, code):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send confirmation mail and redirect to confirmation page
|
# Send confirmation mail and redirect to confirmation page
|
||||||
order.compute_order_price()
|
|
||||||
grouped_order.compute_items_ordered_nb()
|
|
||||||
send_order_confirmation_mail(order)
|
send_order_confirmation_mail(order)
|
||||||
|
|
||||||
# Redirect to prevent data from being posted twice when the user hits the Back
|
# Redirect to prevent data from being posted twice when the user hits the Back
|
||||||
|
@ -123,7 +120,6 @@ def validate_item_ordered_nb(item, ordered_nb):
|
||||||
|
|
||||||
def validate_articles_ordered_nb(order):
|
def validate_articles_ordered_nb(order):
|
||||||
"""Return an error if no items are ordered"""
|
"""Return an error if no items are ordered"""
|
||||||
order.compute_order_articles_nb()
|
|
||||||
if order.articles_nb == 0:
|
if order.articles_nb == 0:
|
||||||
return "Veuillez commander au moins un produit"
|
return "Veuillez commander au moins un produit"
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -1396,7 +1396,7 @@ class TestGroupedOrderSheetView:
|
||||||
response = client_log.get(generate_sheet_url)
|
response = client_log.get(generate_sheet_url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.context["grouped_order"] == grouped_order
|
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
|
assert len(response.context["orders_dict"]) == 0
|
||||||
|
|
||||||
# we order some items in the grouped order
|
# we order some items in the grouped order
|
||||||
|
@ -1404,7 +1404,7 @@ class TestGroupedOrderSheetView:
|
||||||
response = client_log.get(generate_sheet_url)
|
response = client_log.get(generate_sheet_url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.context["grouped_order"] == grouped_order
|
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["orders_dict"][order] == [3, 2]
|
||||||
assert response.context["grouped_order"].total_price == 35
|
assert response.context["grouped_order"].total_price == 35
|
||||||
|
|
||||||
|
|
|
@ -44,11 +44,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, item=item_2, nb=2)
|
||||||
models.OrderedItem.objects.create(order=order_2, item=item_1, nb=1)
|
models.OrderedItem.objects.create(order=order_2, item=item_1, nb=1)
|
||||||
models.OrderedItem.objects.create(order=order_3, item=item_2, 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
|
return order
|
||||||
|
|
Loading…
Reference in a new issue