feat: Support themes via CHARIOTTE_THEME settings / env variable

This commit is contained in:
selfhoster1312 2025-03-07 17:44:47 +01:00
parent 749ca45148
commit d75002ae28
81 changed files with 93 additions and 26 deletions

View file

@ -8,3 +8,7 @@ omit =
la_chariotte/settings.py
la_chariotte/asgi.py
la_chariotte/wsgi.py
# We only test the default theme
# Also, code depending on theme_settings in the codebase is marked
# `# pragma: no cover` to avoid false positives.
la_chariotte/themes/*/settings.py

View file

@ -73,9 +73,6 @@ class GroupedOrder(models.Model):
def is_to_be_delivered(self):
return self.delivery_date >= timezone.now().date()
def get_absolute_url(self):
return reverse("order:manage_items", kwargs={"code": self.code})
def clean_fields(self, exclude=None):
super().clean_fields(exclude=exclude)
# Ensure that new grouped orders use a delivery_date in the future.
@ -172,9 +169,6 @@ class Item(models.Model):
else:
return None
def get_absolute_url(self):
return reverse("order:manage_items", kwargs={"code": self.grouped_order.code})
def __str__(self): # pragma: no cover
return f"{self.name} ({self.price} €)"

View file

@ -11,6 +11,7 @@ from django.views import generic
from django_weasyprint import WeasyTemplateResponseMixin
from icalendar import Calendar, Event, vCalAddress, vText
from ...settings import theme_settings
from ..forms import GroupedOrderForm, Item, JoinGroupedOrderForm
from ..models import GroupedOrder, OrderAuthor
from .mixins import UserIsOrgaMixin
@ -183,6 +184,14 @@ class GroupedOrderCreateView(LoginRequiredMixin, generic.CreateView):
self.object.create_code_from_pk()
return super().form_valid(form)
def get_success_url(self):
# If the theme has specified a custom redirect, use it as success URL
theme_redirect = theme_settings.success_url(self)
if theme_redirect: # pragma: no cover
return theme_redirect
else:
return reverse("order:manage_items", kwargs={"code": self.object.code})
class GroupedOrderUpdateView(UserIsOrgaMixin, generic.UpdateView):
model = GroupedOrder
@ -198,6 +207,14 @@ class GroupedOrderUpdateView(UserIsOrgaMixin, generic.UpdateView):
kwargs["user"] = self.request.user
return kwargs
def get_success_url(self):
# If the theme has specified a custom redirect, use it as success URL
theme_redirect = theme_settings.success_url(self)
if theme_redirect: # pragma: no cover
return theme_redirect
else:
return reverse("order:manage_items", kwargs={"code": self.object.code})
class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
def get_object(self, queryset=None):

View file

@ -3,6 +3,7 @@ from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.views import generic
from ...settings import theme_settings
from ..forms import ItemCreateForm
from ..models import GroupedOrder, Item
@ -21,12 +22,29 @@ class ItemCreateView(UserPassesTestMixin, generic.CreateView):
grouped_order = get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
return grouped_order.orga == self.request.user
def get_success_url(self):
# If the theme has specified a custom redirect, use it as success URL
theme_redirect = theme_settings.success_url(self)
if theme_redirect: # pragma: no cover
return theme_redirect
else:
return reverse_lazy(
"order:manage_items", kwargs={"code": self.object.grouped_order.code}
)
class ItemDeleteView(UserPassesTestMixin, generic.DeleteView):
model = Item
def get_success_url(self):
return reverse_lazy("order:manage_items", args=[self.object.grouped_order.code])
# If the theme has specified a custom redirect, use it as success URL
theme_redirect = theme_settings.success_url(self)
if theme_redirect: # pragma: no cover
return theme_redirect
else:
return reverse_lazy(
"order:manage_items", kwargs={"code": self.object.grouped_order.code}
)
def test_func(self):
# Restrict access to the manager or a superuser

View file

@ -57,10 +57,14 @@ LOGIN_URL = "accounts:login"
LOGIN_REDIRECT_URL = "order:index"
LOGOUT_REDIRECT_URL = "dashboard"
CHARIOTTE_THEME = os.getenv("CHARIOTTE_THEME", "default")
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "la_chariotte" / "templates"],
"DIRS": (
[BASE_DIR / "la_chariotte" / "themes" / CHARIOTTE_THEME / "templates"]
),
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
@ -134,7 +138,7 @@ USE_TZ = True
# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_URL = "static/"
STATICFILES_DIRS = ["la_chariotte/static"]
STATICFILES_DIRS = [BASE_DIR / "la_chariotte" / "themes" / CHARIOTTE_THEME / "static"]
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
@ -191,3 +195,12 @@ if DEBUG:
EMAIL_HOST_PASSWORD = ""
EMAIL_PORT = 1025
EMAIL_USE_TLS = False
# Load theme settings as la_chariotte.settings.theme_settings
from importlib import import_module
theme_settings = import_module(
"la_chariotte.themes.%s.settings" % CHARIOTTE_THEME
if CHARIOTTE_THEME
else "default"
)

View file

@ -10,6 +10,7 @@ from django.urls import reverse
from django.utils import timezone
from icalendar import Calendar, vText
from la_chariotte import settings
from la_chariotte.order import models
from la_chariotte.tests.utils import create_grouped_order, order_items_in_grouped_order
@ -889,6 +890,7 @@ class TestGroupedOrderCreateView:
},
)
assert response.status_code == 302
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().code != ""
@ -965,6 +967,7 @@ class TestGroupedOrderUpdateView:
},
)
assert response.status_code == 302
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "titre test modifié"
@ -1006,6 +1009,7 @@ class TestGroupedOrderUpdateView:
},
)
assert response.status_code == 302
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "titre test modifié"
@ -1118,6 +1122,7 @@ class TestGroupedOrderAddItemsView:
},
)
assert response.status_code == 302
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1
@ -1126,7 +1131,11 @@ class TestGroupedOrderAddItemsView:
create_item_url = reverse("order:item_create", args=[grouped_order.code])
response = client_log.post(create_item_url, {"name": "Pain test", "price": "2"})
response.status_code == 302
response.url == reverse("order:manage_items", args=[grouped_order.code])
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
assert response.url == reverse(
"order:manage_items",
args=[grouped_order.code],
)
assert grouped_order.item_set.count() == 1
# Delete the item
@ -1134,7 +1143,11 @@ class TestGroupedOrderAddItemsView:
delete_item_url = reverse("order:item_delete", args=[grouped_order.id, item.id])
response = client_log.post(delete_item_url)
assert response.status_code == 302
assert response.url == reverse("order:manage_items", args=[grouped_order.code])
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
assert response.url == reverse(
"order:manage_items",
args=[grouped_order.code],
)
assert grouped_order.item_set.count() == 0
def test_create_or_delete_item__not_orga(self, client_log, other_user):
@ -1324,8 +1337,10 @@ class TestGroupedOrderDuplicateView:
# redirection
assert response.status_code == 302
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
assert response.url == reverse(
"order:update_grouped_order", kwargs={"code": new_grouped_order.code}
"order:update_grouped_order",
kwargs={"code": new_grouped_order.code},
)
# The initial grouped order did not change

View file

@ -2,6 +2,7 @@ import pytest
from django.contrib import auth
from django.urls import reverse
from la_chariotte import settings
from la_chariotte.order import models
from .utils import create_grouped_order
@ -28,6 +29,7 @@ class TestItemCreateView:
create_item_view_url, {"name": "titre item", "price": 2}
)
assert response.status_code == 302
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
assert response.url == reverse(
"order:manage_items",
kwargs={

View file

View file

View file

@ -0,0 +1,4 @@
# The theme may want to specify which URL a successful form should redirect to
def success_url(view):
# No more form success URLs to overwrite
return None

View file

Before

Width:  |  Height:  |  Size: 547 KiB

After

Width:  |  Height:  |  Size: 547 KiB

View file

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 590 KiB

View file

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 229 KiB

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View file

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View file

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View file

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View file

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 298 KiB

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 242 KiB

View file

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 208 KiB

View file

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View file

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View file

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View file

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View file

@ -1,12 +1,12 @@
// 1. Import the initial variables
@import "../../../node_modules/bulma/sass/utilities/initial-variables"
@import "../../../../../node_modules/bulma/sass/utilities/initial-variables"
// 2. Set your own initial variables
@import "./base/variables"
// @import "./base/fonts"
// 3. Import the rest of Bulma
@import "../../../node_modules/bulma/bulma"
@import "../../../../../node_modules/bulma/bulma"
// 4. Import your stuff here
@import "./base/global"