Merge branch 'feat-themes' into 'develop'
feat: Support themes via CHARIOTTE_THEME settings / env variable See merge request la-chariotte/la-chariotte!136
|
@ -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
|
||||
|
|
2
.gitignore
vendored
|
@ -8,3 +8,5 @@ local_settings.py
|
|||
*venv*
|
||||
mails.sqlite
|
||||
package-lock.json
|
||||
**/sass/*.css
|
||||
/static
|
||||
|
|
|
@ -94,6 +94,8 @@ DATABASES = {
|
|||
Everything should now be ready to start the server:
|
||||
|
||||
```shell
|
||||
python manage.py compilescss
|
||||
python manage.py collectstatic
|
||||
python manage.py migrate --settings=local_settings
|
||||
python manage.py runserver --settings=local_settings
|
||||
```
|
||||
|
@ -112,3 +114,14 @@ pip install sendria
|
|||
sendria --db mails.sqlite
|
||||
$NAVIGATOR http://127.0.0.1:1080
|
||||
```
|
||||
|
||||
## Using a custom theme
|
||||
|
||||
Themes are provided in the `la_chariotte/themes` folder. The `default` theme is enabled by default, but the `CHARIOTTE_THEME` setting and environment variable allows you to change that. Another `light` theme is provided in the repository for you to try, but you can make your own.
|
||||
|
||||
After changing the setting or environment variable:
|
||||
|
||||
- delete the `static` folder at the repository root
|
||||
- run `python manage.py compilescss`
|
||||
- run `python manage.py collectstatic`
|
||||
- restart the server
|
||||
|
|
|
@ -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} €)"
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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,7 +890,8 @@ class TestGroupedOrderCreateView:
|
|||
},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert response.url.endswith("gerer-produits")
|
||||
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,7 +967,8 @@ class TestGroupedOrderUpdateView:
|
|||
},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert response.url.endswith("gerer-produits")
|
||||
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é"
|
||||
assert models.GroupedOrder.objects.first().place == "quelque part"
|
||||
|
@ -1006,7 +1009,8 @@ class TestGroupedOrderUpdateView:
|
|||
},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert response.url.endswith("gerer-produits")
|
||||
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é"
|
||||
assert models.GroupedOrder.objects.first().place == "quelque part"
|
||||
|
@ -1118,7 +1122,8 @@ class TestGroupedOrderAddItemsView:
|
|||
},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert response.url.endswith("gerer-produits")
|
||||
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
|
||||
assert response.url.endswith("gerer-produits")
|
||||
assert models.GroupedOrder.objects.count() == 1
|
||||
|
||||
# Create an item
|
||||
|
@ -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,9 +1337,11 @@ class TestGroupedOrderDuplicateView:
|
|||
|
||||
# redirection
|
||||
assert response.status_code == 302
|
||||
assert response.url == reverse(
|
||||
"order:update_grouped_order", kwargs={"code": new_grouped_order.code}
|
||||
)
|
||||
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
|
||||
assert response.url == reverse(
|
||||
"order:update_grouped_order",
|
||||
kwargs={"code": new_grouped_order.code},
|
||||
)
|
||||
|
||||
# The initial grouped order did not change
|
||||
assert grouped_order.item_set.first().ordered_nb == 4
|
||||
|
|
|
@ -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,12 +29,13 @@ class TestItemCreateView:
|
|||
create_item_view_url, {"name": "titre item", "price": 2}
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert response.url == reverse(
|
||||
"order:manage_items",
|
||||
kwargs={
|
||||
"code": grouped_order.code,
|
||||
},
|
||||
)
|
||||
if getattr(settings, "CHARIOTTE_THEME", "default") == "default":
|
||||
assert response.url == reverse(
|
||||
"order:manage_items",
|
||||
kwargs={
|
||||
"code": grouped_order.code,
|
||||
},
|
||||
)
|
||||
assert models.Item.objects.first().name == "titre item"
|
||||
response = client_log.get(response.url)
|
||||
assert "titre item" in response.content.decode()
|
||||
|
|
0
la_chariotte/themes/__init__.py
Normal file
0
la_chariotte/themes/default/__init__.py
Normal file
4
la_chariotte/themes/default/settings.py
Normal 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
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 547 KiB |
Before Width: | Height: | Size: 590 KiB After Width: | Height: | Size: 590 KiB |
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 229 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 298 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 242 KiB |
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 208 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
@ -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"
|
0
la_chariotte/themes/light/__init__.py
Normal file
31
la_chariotte/themes/light/settings.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# The theme may want to specify which URL a successful form should redirect to
|
||||
def success_url(view):
|
||||
from django.urls import reverse
|
||||
|
||||
from la_chariotte.order import views
|
||||
|
||||
if type(view) == views.grouped_order.GroupedOrderOverview:
|
||||
return reverse(
|
||||
"order:grouped_order_overview", kwargs={"code": view.object.code}
|
||||
)
|
||||
elif type(view) == views.grouped_order.GroupedOrderCreateView:
|
||||
return reverse(
|
||||
"order:grouped_order_overview", kwargs={"code": view.object.code}
|
||||
)
|
||||
elif type(view) == views.grouped_order.GroupedOrderUpdateView:
|
||||
return reverse(
|
||||
"order:grouped_order_overview", kwargs={"code": view.object.code}
|
||||
)
|
||||
elif type(view) == views.item.ItemCreateView:
|
||||
return reverse(
|
||||
"order:grouped_order_overview",
|
||||
kwargs={"code": view.object.grouped_order.code},
|
||||
)
|
||||
elif type(view) == views.item.ItemDeleteView:
|
||||
return reverse(
|
||||
"order:grouped_order_overview",
|
||||
kwargs={"code": view.object.grouped_order.code},
|
||||
)
|
||||
|
||||
# No more form success URLs to overwrite
|
||||
return None
|
1
la_chariotte/themes/light/static/fork-awesome
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../default/static/fork-awesome
|
1
la_chariotte/themes/light/static/img/contributeureuses.png
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../default/static/img/contributeureuses.png
|
1
la_chariotte/themes/light/static/img/icons
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../default/static/img/icons
|
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 19 KiB |
1
la_chariotte/themes/light/static/img/logos/logo_hashbang.png
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../../default/static/img/logos/logo_hashbang.png
|
1
la_chariotte/themes/light/static/img/logos/logo_la_chariotte.png
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../../default/static/img/logos/logo_la_chariotte.png
|
1
la_chariotte/themes/light/static/img/notice
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../default/static/img/notice
|
1
la_chariotte/themes/light/static/img/notice_1.jpg
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../default/static/img/notice_1.jpg
|
1
la_chariotte/themes/light/static/img/notice_2.jpg
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../default/static/img/notice_2.jpg
|
1
la_chariotte/themes/light/static/img/notice_3.jpg
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../default/static/img/notice_3.jpg
|
1
la_chariotte/themes/light/static/sass/base/_accordion.sass
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../../default/static/sass/base/./_accordion.sass
|
14
la_chariotte/themes/light/static/sass/base/_content.sass
Normal file
|
@ -0,0 +1,14 @@
|
|||
@media screen and (min-width: $min-desktop)
|
||||
.content
|
||||
margin: $base
|
||||
|
||||
@media screen and (max-width: $max-tablet)
|
||||
.content
|
||||
margin: $small
|
||||
|
||||
.formatted-text
|
||||
white-space: pre-wrap
|
||||
|
||||
img.notice-img
|
||||
border: $info 3px solid
|
||||
margin-bottom: 1em
|
1
la_chariotte/themes/light/static/sass/base/_footer.sass
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../../default/static/sass/base/./_footer.sass
|
1
la_chariotte/themes/light/static/sass/base/_form.sass
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../../default/static/sass/base/./_form.sass
|