Merge production refresh for live deploy
This commit is contained in:
60
mandelstudio/migrations/0001_initial.py
Normal file
60
mandelstudio/migrations/0001_initial.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-03-25 16:37
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
import wagtail.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("wagtailcore", "0097_alter_page_locale_alter_page_translation_key"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="LocalizedFooterContent",
|
||||||
|
fields=[
|
||||||
|
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("title", models.CharField(default="Footer content", max_length=120)),
|
||||||
|
("translation_key", models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||||
|
(
|
||||||
|
"footer",
|
||||||
|
wagtail.fields.StreamField(
|
||||||
|
[("about_us", 2), ("text", 2), ("page_list", 4), ("SubscriptionBlock", 7)],
|
||||||
|
block_lookup={
|
||||||
|
0: ("wagtail.blocks.CharBlock", (), {"help_text": "Heading of the content block.", "label": "Heading", "required": False}),
|
||||||
|
1: ("wagtail.blocks.RichTextBlock", (), {}),
|
||||||
|
2: ("wagtail.blocks.StructBlock", [[("heading", 0), ("content", 1)]], {}),
|
||||||
|
3: ("wagtail.blocks.PageChooserBlock", (), {"help_text": "List pages below this page", "label": "Page"}),
|
||||||
|
4: ("wagtail.blocks.StructBlock", [[("heading", 0), ("page", 3)]], {}),
|
||||||
|
5: ("wagtail.blocks.CharBlock", (), {"label": "Title", "required": False}),
|
||||||
|
6: ("wagtail.blocks.TextBlock", (), {"label": "Description", "required": False}),
|
||||||
|
7: ("wagtail.blocks.StructBlock", [[("title", 5), ("description", 6)]], {}),
|
||||||
|
},
|
||||||
|
default=list,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mini_footer",
|
||||||
|
wagtail.fields.StreamField(
|
||||||
|
[("text", 0)],
|
||||||
|
block_lookup={0: ("wagtail.blocks.RichTextBlock", (), {})},
|
||||||
|
default=list,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("locale", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name="+", to="wagtailcore.locale")),
|
||||||
|
("site", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="localized_footer_contents", to="wagtailcore.site")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Localized footer content",
|
||||||
|
"verbose_name_plural": "Localized footer contents",
|
||||||
|
"abstract": False,
|
||||||
|
"constraints": [models.UniqueConstraint(fields=("site", "locale"), name="unique_localized_footer_per_site_locale")],
|
||||||
|
"unique_together": {("translation_key", "locale")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
236
mandelstudio/migrations/0002_seed_localized_footer_content.py
Normal file
236
mandelstudio/migrations/0002_seed_localized_footer_content.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
CONTENT = {
|
||||||
|
"nl": {
|
||||||
|
"about": "<p>Wij bouwen snelle websites en webshops die je team zelf kan beheren. Van eerste lancering tot doorontwikkeling: helder, schaalbaar en zonder ruis.</p>",
|
||||||
|
"links_heading": "Snelle links",
|
||||||
|
"support_heading": "Help & support",
|
||||||
|
"link_labels": {
|
||||||
|
"about": "Over ons",
|
||||||
|
"services": "Diensten",
|
||||||
|
"projects": "Projecten",
|
||||||
|
"contact": "Contact",
|
||||||
|
"capabilities": "Mogelijkheden",
|
||||||
|
"ai_search": "AI Search",
|
||||||
|
"book_call": "Plan een gesprek",
|
||||||
|
},
|
||||||
|
"mini": "<p><a href=\"{contact}\">Contact</a> - <a href=\"{services}\">Diensten</a> - <a href=\"{projects}\">Projecten</a> - Copyright 2026 - MandelBlog Studio</p>",
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"about": "<p>We build fast websites and webshops your team can manage without friction. From launch to growth, the setup stays clear, scalable, and easy to extend.</p>",
|
||||||
|
"links_heading": "Quick links",
|
||||||
|
"support_heading": "Help & support",
|
||||||
|
"link_labels": {
|
||||||
|
"about": "About us",
|
||||||
|
"services": "Services",
|
||||||
|
"projects": "Projects",
|
||||||
|
"contact": "Contact",
|
||||||
|
"capabilities": "Capabilities",
|
||||||
|
"ai_search": "AI Search",
|
||||||
|
"book_call": "Book a call",
|
||||||
|
},
|
||||||
|
"mini": "<p><a href=\"{contact}\">Contact</a> - <a href=\"{services}\">Services</a> - <a href=\"{projects}\">Projects</a> - Copyright 2026 - MandelBlog Studio</p>",
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"about": "<p>Wir entwickeln schnelle Websites und Webshops, die Ihr Team selbst pflegen kann. Von der ersten Veröffentlichung bis zur Weiterentwicklung bleibt alles klar, skalierbar und wartbar.</p>",
|
||||||
|
"links_heading": "Schnellzugriff",
|
||||||
|
"support_heading": "Hilfe & Support",
|
||||||
|
"link_labels": {
|
||||||
|
"about": "Über uns",
|
||||||
|
"services": "Dienstleistungen",
|
||||||
|
"projects": "Projekte",
|
||||||
|
"contact": "Kontakt",
|
||||||
|
"capabilities": "Möglichkeiten",
|
||||||
|
"ai_search": "KI-Suche",
|
||||||
|
"book_call": "Gespräch planen",
|
||||||
|
},
|
||||||
|
"mini": "<p><a href=\"{contact}\">Kontakt</a> - <a href=\"{services}\">Dienstleistungen</a> - <a href=\"{projects}\">Projekte</a> - Copyright 2026 - MandelBlog Studio</p>",
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"about": "<p>Nous créons des sites web et des boutiques en ligne rapides que votre équipe peut gérer facilement. Du lancement à la croissance, tout reste clair, évolutif et simple à maintenir.</p>",
|
||||||
|
"links_heading": "Accès rapide",
|
||||||
|
"support_heading": "Aide & support",
|
||||||
|
"link_labels": {
|
||||||
|
"about": "À propos",
|
||||||
|
"services": "Services",
|
||||||
|
"projects": "Projets",
|
||||||
|
"contact": "Contact",
|
||||||
|
"capabilities": "Possibilités",
|
||||||
|
"ai_search": "Recherche IA",
|
||||||
|
"book_call": "Planifier un échange",
|
||||||
|
},
|
||||||
|
"mini": "<p><a href=\"{contact}\">Contact</a> - <a href=\"{services}\">Services</a> - <a href=\"{projects}\">Projets</a> - Copyright 2026 - MandelBlog Studio</p>",
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"about": "<p>Construimos sitios web y tiendas online rápidas que tu equipo puede gestionar sin complicaciones. Desde el lanzamiento hasta el crecimiento, todo se mantiene claro, escalable y fácil de ampliar.</p>",
|
||||||
|
"links_heading": "Accesos rápidos",
|
||||||
|
"support_heading": "Ayuda y soporte",
|
||||||
|
"link_labels": {
|
||||||
|
"about": "Sobre nosotros",
|
||||||
|
"services": "Servicios",
|
||||||
|
"projects": "Proyectos",
|
||||||
|
"contact": "Contacto",
|
||||||
|
"capabilities": "Posibilidades",
|
||||||
|
"ai_search": "Búsqueda con IA",
|
||||||
|
"book_call": "Planificar una llamada",
|
||||||
|
},
|
||||||
|
"mini": "<p><a href=\"{contact}\">Contacto</a> - <a href=\"{services}\">Servicios</a> - <a href=\"{projects}\">Proyectos</a> - Copyright 2026 - MandelBlog Studio</p>",
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"about": "<p>Realizziamo siti web e negozi online veloci che il tuo team può gestire in autonomia. Dal lancio alla crescita, tutto rimane chiaro, scalabile e semplice da estendere.</p>",
|
||||||
|
"links_heading": "Link rapidi",
|
||||||
|
"support_heading": "Aiuto e supporto",
|
||||||
|
"link_labels": {
|
||||||
|
"about": "Chi siamo",
|
||||||
|
"services": "Servizi",
|
||||||
|
"projects": "Progetti",
|
||||||
|
"contact": "Contatto",
|
||||||
|
"capabilities": "Possibilità",
|
||||||
|
"ai_search": "Ricerca AI",
|
||||||
|
"book_call": "Prenota una call",
|
||||||
|
},
|
||||||
|
"mini": "<p><a href=\"{contact}\">Contatto</a> - <a href=\"{services}\">Servizi</a> - <a href=\"{projects}\">Progetti</a> - Copyright 2026 - MandelBlog Studio</p>",
|
||||||
|
},
|
||||||
|
"pt": {
|
||||||
|
"about": "<p>Criamos sites e lojas online rápidos que a sua equipa consegue gerir com autonomia. Do lançamento ao crescimento, tudo permanece claro, escalável e simples de evoluir.</p>",
|
||||||
|
"links_heading": "Acesso rápido",
|
||||||
|
"support_heading": "Ajuda e suporte",
|
||||||
|
"link_labels": {
|
||||||
|
"about": "Sobre nós",
|
||||||
|
"services": "Serviços",
|
||||||
|
"projects": "Projetos",
|
||||||
|
"contact": "Contacto",
|
||||||
|
"capabilities": "Possibilidades",
|
||||||
|
"ai_search": "Pesquisa IA",
|
||||||
|
"book_call": "Marcar conversa",
|
||||||
|
},
|
||||||
|
"mini": "<p><a href=\"{contact}\">Contacto</a> - <a href=\"{services}\">Serviços</a> - <a href=\"{projects}\">Projetos</a> - Copyright 2026 - MandelBlog Studio</p>",
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"about": "<p>Мы создаём быстрые сайты и интернет-магазины, которыми ваша команда может управлять самостоятельно. От запуска до развития всё остаётся понятным, масштабируемым и удобным для роста.</p>",
|
||||||
|
"links_heading": "Быстрые ссылки",
|
||||||
|
"support_heading": "Помощь и поддержка",
|
||||||
|
"link_labels": {
|
||||||
|
"about": "О нас",
|
||||||
|
"services": "Услуги",
|
||||||
|
"projects": "Проекты",
|
||||||
|
"contact": "Контакт",
|
||||||
|
"capabilities": "Возможности",
|
||||||
|
"ai_search": "AI Search",
|
||||||
|
"book_call": "Запланировать звонок",
|
||||||
|
},
|
||||||
|
"mini": "<p><a href=\"{contact}\">Контакт</a> - <a href=\"{services}\">Услуги</a> - <a href=\"{projects}\">Проекты</a> - Copyright 2026 - MandelBlog Studio</p>",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
SOURCE_SLUGS = {
|
||||||
|
"about": "over-ons",
|
||||||
|
"services": "diensten",
|
||||||
|
"projects": "projecten",
|
||||||
|
"contact": "contact",
|
||||||
|
"capabilities": "mogelijkheden",
|
||||||
|
"ai_search": "ai-search",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_urls(Page, code):
|
||||||
|
source_pages = {
|
||||||
|
key: Page.objects.filter(locale__language_code="nl", slug=slug).first()
|
||||||
|
for key, slug in SOURCE_SLUGS.items()
|
||||||
|
}
|
||||||
|
urls = {}
|
||||||
|
for key, page in source_pages.items():
|
||||||
|
if not page:
|
||||||
|
urls[key] = "/"
|
||||||
|
continue
|
||||||
|
translated = Page.objects.filter(
|
||||||
|
translation_key=page.translation_key, locale__language_code=code
|
||||||
|
).first()
|
||||||
|
chosen = translated or page
|
||||||
|
urls[key] = getattr(chosen, "url", None) or "/"
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
def make_footer_raw(code, urls):
|
||||||
|
content = CONTENT[code]
|
||||||
|
labels = content["link_labels"]
|
||||||
|
links_html = (
|
||||||
|
f'<p><a href="{urls["about"]}">{labels["about"]}</a><br/>'
|
||||||
|
f'<a href="{urls["services"]}">{labels["services"]}</a><br/>'
|
||||||
|
f'<a href="{urls["projects"]}">{labels["projects"]}</a><br/>'
|
||||||
|
f'<a href="{urls["contact"]}">{labels["contact"]}</a></p>'
|
||||||
|
)
|
||||||
|
support_html = (
|
||||||
|
f'<p><a href="{urls["capabilities"]}">{labels["capabilities"]}</a><br/>'
|
||||||
|
f'<a href="{urls["ai_search"]}">{labels["ai_search"]}</a><br/>'
|
||||||
|
f'<a href="{urls["contact"]}">{labels["book_call"]}</a><br/>'
|
||||||
|
f'<a href="mailto:info@mandelblog.com">info@mandelblog.com</a></p>'
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"type": "about_us",
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"value": {"heading": "MandelBlog Studio", "content": content["about"]},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"value": {"heading": content["links_heading"], "content": links_html},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"value": {"heading": content["support_heading"], "content": support_html},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_mini_raw(code, urls):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"value": CONTENT[code]["mini"].format(**urls),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def seed_footer_content(apps, schema_editor):
|
||||||
|
LocalizedFooterContent = apps.get_model("mandelstudio", "LocalizedFooterContent")
|
||||||
|
Site = apps.get_model("wagtailcore", "Site")
|
||||||
|
Locale = apps.get_model("wagtailcore", "Locale")
|
||||||
|
site = Site.objects.order_by("id").first()
|
||||||
|
if site is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
from wagtail.models import Page
|
||||||
|
|
||||||
|
translation_key = uuid.uuid4()
|
||||||
|
for code in CONTENT.keys():
|
||||||
|
locale, _ = Locale.objects.get_or_create(language_code=code)
|
||||||
|
urls = build_urls(Page, code)
|
||||||
|
LocalizedFooterContent.objects.update_or_create(
|
||||||
|
site=site,
|
||||||
|
locale=locale,
|
||||||
|
defaults={
|
||||||
|
"title": f"Footer content ({code})",
|
||||||
|
"translation_key": translation_key,
|
||||||
|
"footer": make_footer_raw(code, urls),
|
||||||
|
"mini_footer": make_mini_raw(code, urls),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_seed(apps, schema_editor):
|
||||||
|
LocalizedFooterContent = apps.get_model("mandelstudio", "LocalizedFooterContent")
|
||||||
|
LocalizedFooterContent.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("mandelstudio", "0001_initial")]
|
||||||
|
operations = [migrations.RunPython(seed_footer_content, reverse_seed)]
|
||||||
51
mandelstudio/migrations/0003_locale_audit_models.py
Normal file
51
mandelstudio/migrations/0003_locale_audit_models.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("mandelstudio", "0002_seed_localized_footer_content")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="LocaleAuditRun",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("started_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("finished_at", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("locale_codes", models.JSONField(blank=True, default=list)),
|
||||||
|
("fix_enabled", models.BooleanField(default=False)),
|
||||||
|
("total_urls_checked", models.PositiveIntegerField(default=0)),
|
||||||
|
("issues_found", models.PositiveIntegerField(default=0)),
|
||||||
|
("pages_with_issues", models.PositiveIntegerField(default=0)),
|
||||||
|
("summary", models.JSONField(blank=True, default=dict)),
|
||||||
|
],
|
||||||
|
options={"ordering": ["-started_at"]},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="LocaleAuditIssue",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("locale_code", models.CharField(max_length=12)),
|
||||||
|
("object_id", models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
("object_type", models.CharField(blank=True, max_length=128)),
|
||||||
|
("url", models.TextField(blank=True)),
|
||||||
|
("title", models.CharField(blank=True, max_length=255)),
|
||||||
|
("severity", models.CharField(max_length=16)),
|
||||||
|
("issue_type", models.CharField(max_length=64)),
|
||||||
|
("field_path", models.CharField(blank=True, max_length=512)),
|
||||||
|
("bad_value", models.TextField(blank=True)),
|
||||||
|
("replacement", models.TextField(blank=True)),
|
||||||
|
("fixed", models.BooleanField(default=False)),
|
||||||
|
("extra", models.JSONField(blank=True, default=dict)),
|
||||||
|
(
|
||||||
|
"run",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="issues",
|
||||||
|
to="mandelstudio.localeauditrun",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"ordering": ["locale_code", "url", "field_path"]},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
mandelstudio/migrations/__init__.py
Normal file
0
mandelstudio/migrations/__init__.py
Normal file
@@ -2,7 +2,6 @@
|
|||||||
"ocyan_plugins": [
|
"ocyan_plugins": [
|
||||||
"ocyan.plugin.contact_form",
|
"ocyan.plugin.contact_form",
|
||||||
"ocyan.plugin.cookie_jar",
|
"ocyan.plugin.cookie_jar",
|
||||||
"ocyan.plugin.demo_data",
|
|
||||||
"ocyan.plugin.django",
|
"ocyan.plugin.django",
|
||||||
"ocyan.plugin.newsletter",
|
"ocyan.plugin.newsletter",
|
||||||
"ocyan.plugin.oscar",
|
"ocyan.plugin.oscar",
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
"ocyan.plugin.oscar_partner",
|
"ocyan.plugin.oscar_partner",
|
||||||
"ocyan.plugin.oscar_shipping",
|
"ocyan.plugin.oscar_shipping",
|
||||||
"ocyan.plugin.oscar_sequential_order_numbers",
|
"ocyan.plugin.oscar_sequential_order_numbers",
|
||||||
"ocyan.plugin.payment_dummy",
|
"ocyan.plugin.payment_mollie",
|
||||||
"ocyan.plugin.roadrunner_bs5",
|
"ocyan.plugin.roadrunner_bs5",
|
||||||
"ocyan.plugin.template_engine",
|
"ocyan.plugin.template_engine",
|
||||||
"ocyan.plugin.roadrunner_productchooser",
|
"ocyan.plugin.roadrunner_productchooser",
|
||||||
@@ -64,8 +63,23 @@
|
|||||||
"en"
|
"en"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ocyan_dummy_payment_plugin": {
|
"payment_mollie": {
|
||||||
"help_text": "Hit pay, to simulate payment."
|
"api_key": "CHANGE_ME",
|
||||||
|
"ideal": true,
|
||||||
|
"creditcard": true,
|
||||||
|
"paypal": true,
|
||||||
|
"bancontact": true,
|
||||||
|
"sofort": true,
|
||||||
|
"banktransfer": false,
|
||||||
|
"belfius": false,
|
||||||
|
"bitcoin": false,
|
||||||
|
"directdebit": false,
|
||||||
|
"eps": false,
|
||||||
|
"giftcard": false,
|
||||||
|
"giropay": false,
|
||||||
|
"inghomepay": false,
|
||||||
|
"kbc": false,
|
||||||
|
"mistercash": false
|
||||||
},
|
},
|
||||||
"oscar": {
|
"oscar": {
|
||||||
"allow_anon_checkout": true,
|
"allow_anon_checkout": true,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ For the full list of settings and their values, see
|
|||||||
https://docs.djangoproject.com/en/2.0/ref/settings/
|
https://docs.djangoproject.com/en/2.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -26,6 +27,49 @@ INSTALLED_APPS = [
|
|||||||
"mandelstudio",
|
"mandelstudio",
|
||||||
] + INSTALLED_APPS
|
] + INSTALLED_APPS
|
||||||
|
|
||||||
|
# Route through the project URL layer so MandelStudio can override
|
||||||
|
# sitemap/robots behavior while still delegating the main Ocyan routes.
|
||||||
|
ROOT_URLCONF = "mandelstudio.urls"
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_required_app(*candidates):
|
||||||
|
"""Ensure required plugin apps remain enabled when /etc/ocyan config omits them."""
|
||||||
|
if any(app in INSTALLED_APPS for app in candidates):
|
||||||
|
return
|
||||||
|
for app in candidates:
|
||||||
|
if importlib.util.find_spec(app):
|
||||||
|
INSTALLED_APPS.append(app)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
_ensure_required_app(
|
||||||
|
"ocyan.plugin.carbasa.carbasa",
|
||||||
|
"ocyan.plugin.carbasa",
|
||||||
|
)
|
||||||
|
_ensure_required_app(
|
||||||
|
"ocyan.plugin.coyote.coyote",
|
||||||
|
"ocyan.plugin.coyote",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep Carbasa/Coyote defaults stable even when plugin settings are not
|
||||||
|
# injected early enough during startup on this deployment.
|
||||||
|
OXYAN_HEADER_OPTIONS = globals().get(
|
||||||
|
"OXYAN_HEADER_OPTIONS",
|
||||||
|
[
|
||||||
|
("basic", "Basic Header"),
|
||||||
|
("big", "Big Header"),
|
||||||
|
("mega", "Mega Header"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
COMPRESS_CACHE_KEY_FUNCTION = globals().get(
|
||||||
|
"COMPRESS_CACHE_KEY_FUNCTION",
|
||||||
|
"ocyan.plugin.coyote.utils.get_compressor_cache_key",
|
||||||
|
)
|
||||||
|
OXYAN_LAZY_THEME_DEFINITIONS = globals().get(
|
||||||
|
"OXYAN_LAZY_THEME_DEFINITIONS",
|
||||||
|
"ocyan.plugin.coyote.definitions.get_coyote_definitions",
|
||||||
|
)
|
||||||
|
|
||||||
# Enable request language negotiation.
|
# Enable request language negotiation.
|
||||||
if "django.middleware.locale.LocaleMiddleware" not in MIDDLEWARE:
|
if "django.middleware.locale.LocaleMiddleware" not in MIDDLEWARE:
|
||||||
if "django.contrib.sessions.middleware.SessionMiddleware" in MIDDLEWARE:
|
if "django.contrib.sessions.middleware.SessionMiddleware" in MIDDLEWARE:
|
||||||
|
|||||||
81
mandelstudio/sitemaps.py
Normal file
81
mandelstudio/sitemaps.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from django.contrib.sitemaps.views import index as sitemap_index_view
|
||||||
|
from django.contrib.sitemaps.views import sitemap as sitemap_section_view
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from wagtail.models import Locale, Page
|
||||||
|
|
||||||
|
from ocyan.plugin.wagtail_oscar_integration.constants import CACHE_DURATION
|
||||||
|
from ocyan.plugin.wagtail_oscar_integration.sitemap import CategorySitemap
|
||||||
|
from ocyan.plugin.wagtail_oscar_integration.sitemap import ProductSitemap
|
||||||
|
from ocyan.plugin.wagtail_oscar_integration.sitemap import ShopSitemap
|
||||||
|
from ocyan.plugin.wagtail_oscar_integration.sitemap import WagtailSitemap as BaseWagtailSitemap
|
||||||
|
|
||||||
|
|
||||||
|
class WagtailSitemap(BaseWagtailSitemap):
|
||||||
|
def items(self):
|
||||||
|
page_ids = []
|
||||||
|
|
||||||
|
for locale in Locale.objects.all():
|
||||||
|
translated_root_page = self.get_wagtail_site().root_page.get_translation_or_none(
|
||||||
|
locale
|
||||||
|
)
|
||||||
|
if translated_root_page is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
locale_page_ids = (
|
||||||
|
translated_root_page.get_descendants(inclusive=True)
|
||||||
|
.live()
|
||||||
|
.public()
|
||||||
|
.order_by()
|
||||||
|
.values_list("pk", flat=True)
|
||||||
|
)
|
||||||
|
page_ids.extend(locale_page_ids)
|
||||||
|
|
||||||
|
if not page_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return (
|
||||||
|
Page.objects.filter(pk__in=page_ids)
|
||||||
|
.live()
|
||||||
|
.public()
|
||||||
|
.defer_streamfields()
|
||||||
|
.order_by("path")
|
||||||
|
.specific()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gather_sitemaps():
|
||||||
|
return {
|
||||||
|
"pages": WagtailSitemap,
|
||||||
|
"shop": ShopSitemap,
|
||||||
|
"products": ProductSitemap,
|
||||||
|
"categories": CategorySitemap,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sitemap_index(request):
|
||||||
|
return sitemap_index_view(
|
||||||
|
request,
|
||||||
|
sitemaps=gather_sitemaps(),
|
||||||
|
sitemap_url_name="sitemaps",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sitemap_section(request, section=None):
|
||||||
|
return sitemap_section_view(
|
||||||
|
request,
|
||||||
|
sitemaps=gather_sitemaps(),
|
||||||
|
section=section,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def robots_txt(request):
|
||||||
|
sitemap_url = request.build_absolute_uri("/sitemap.xml")
|
||||||
|
content = "\n".join(
|
||||||
|
[
|
||||||
|
"User-agent: *",
|
||||||
|
"Allow: /",
|
||||||
|
f"Sitemap: {sitemap_url}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return HttpResponse(content, content_type="text/plain; charset=utf-8")
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% load wagtailcore_tags oxyan static string_filters %}
|
{% load wagtailcore_tags oxyan static string_filters mandelstudio_i18n %}
|
||||||
|
|
||||||
{% block extrahead %}
|
{% block extrahead %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
{% block layout %}
|
{% block layout %}
|
||||||
<a class="btn btn-secondary hidelink" id="main_content_link" href="#skip_header" tabindex="2">
|
<a class="btn btn-secondary hidelink" id="main_content_link" href="#skip_header" tabindex="2">
|
||||||
Ga naar inhoud
|
{% skip_to_content_text %}
|
||||||
</a>
|
</a>
|
||||||
{% include_header header_template|default:"engine/partials/header.html" %}
|
{% include_header header_template|default:"engine/partials/header.html" %}
|
||||||
<div id="main_content" tabindex="-1">
|
<div id="main_content" tabindex="-1">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% load wagtailcore_tags oxyan static string_filters %}
|
{% load wagtailcore_tags oxyan static string_filters mandelstudio_i18n %}
|
||||||
|
|
||||||
{% block extrahead %}
|
{% block extrahead %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
{% block layout %}
|
{% block layout %}
|
||||||
<a class="btn btn-secondary hidelink" id="main_content_link" href="#skip_header" tabindex="2">
|
<a class="btn btn-secondary hidelink" id="main_content_link" href="#skip_header" tabindex="2">
|
||||||
Ga naar inhoud
|
{% skip_to_content_text %}
|
||||||
</a>
|
</a>
|
||||||
{% include_header header_template|default:"engine/partials/header.html" %}
|
{% include_header header_template|default:"engine/partials/header.html" %}
|
||||||
<div id="main_content" tabindex="-1">
|
<div id="main_content" tabindex="-1">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% load wagtailcore_tags oxyan static string_filters %}
|
{% load wagtailcore_tags oxyan static string_filters mandelstudio_i18n %}
|
||||||
|
|
||||||
{% block extrahead %}
|
{% block extrahead %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
{% block layout %}
|
{% block layout %}
|
||||||
<a class="btn btn-secondary hidelink" id="main_content_link" href="#skip_header" tabindex="2">
|
<a class="btn btn-secondary hidelink" id="main_content_link" href="#skip_header" tabindex="2">
|
||||||
Ga naar inhoud
|
{% skip_to_content_text %}
|
||||||
</a>
|
</a>
|
||||||
{% include_header header_template|default:"engine/partials/header.html" %}
|
{% include_header header_template|default:"engine/partials/header.html" %}
|
||||||
<div id="main_content" tabindex="-1">
|
<div id="main_content" tabindex="-1">
|
||||||
|
|||||||
1
mandelstudio/templates/engine/partials/header5.html
Normal file
1
mandelstudio/templates/engine/partials/header5.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{% include "carbasa/headers/header.html" %}
|
||||||
36
mandelstudio/templates/oxyan/partials/footer.html
Normal file
36
mandelstudio/templates/oxyan/partials/footer.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{% load staticfiles %}
|
||||||
|
{% load wagtailcore_tags wagtailimages_tags wagtailsettings_tags cache mandelstudio_footer %}
|
||||||
|
{% get_settings %}
|
||||||
|
{% localized_footer_content as localized_footer %}
|
||||||
|
|
||||||
|
{% cache 300 footer_menu LANGUAGE_CODE request.site %}
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
{% with footer=localized_footer.footer|default:settings.ocyan_plugin_wagtail.OcyanSettings.footer %}
|
||||||
|
{% for block in footer %}
|
||||||
|
<div class="col-lg-3 col-md-6 col-sm-12 footer_column {{ block.block_type|slugify }}">
|
||||||
|
{% include_block block %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<section class="copyright_wrapper">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 copyright_block">
|
||||||
|
{% if localized_footer and localized_footer.mini_footer %}
|
||||||
|
{% for block in localized_footer.mini_footer %}
|
||||||
|
{% include_block block %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% include_block settings.ocyan_plugin_wagtail.OcyanSettings.mini_footer %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endcache %}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
||||||
{% get_language_info_list for languages as languages %}
|
{% get_language_info_list for languages as languages %}
|
||||||
{% ocyanjson "i18n" "language_chooser_disabled_options" "" as disabled_languages %}
|
{% ocyanjson "i18n" "language_chooser_disabled_options" "" as disabled_languages %}
|
||||||
<form action="{% url set_language %}" method="post" class="language_form">
|
<form action="{% url 'set_language' %}" method="post" class="language_form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input name="next" type="hidden" value="{{ language_neutral_url_path|default:request.path|untranslated_url }}"/>
|
<input name="next" type="hidden" value="{{ language_neutral_url_path|default:request.path|untranslated_url }}"/>
|
||||||
{% for language in languages %}
|
{% for language in languages %}
|
||||||
|
|||||||
0
mandelstudio/templatetags/__init__.py
Normal file
0
mandelstudio/templatetags/__init__.py
Normal file
120
mandelstudio/templatetags/localized_navigation.py
Normal file
120
mandelstudio/templatetags/localized_navigation.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from wagtail.models import Page
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_language_code(language_code: str | None) -> str:
|
||||||
|
return (language_code or settings.LANGUAGE_CODE).split("-")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_locale_url(language_code: str) -> str:
|
||||||
|
default_language = _normalize_language_code(settings.LANGUAGE_CODE)
|
||||||
|
target_language = _normalize_language_code(language_code)
|
||||||
|
return "/" if target_language == default_language else f"/{target_language}/"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_translatable_page(page) -> bool:
|
||||||
|
return page is not None and hasattr(page, "translation_key") and hasattr(page, "locale")
|
||||||
|
|
||||||
|
|
||||||
|
def _translated_pages(page):
|
||||||
|
if not _is_translatable_page(page):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
_normalize_language_code(translated.locale.language_code): translated
|
||||||
|
for translated in Page.objects.filter(translation_key=page.translation_key)
|
||||||
|
.live()
|
||||||
|
.public()
|
||||||
|
.specific()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_absolute_url(request, path: str | None, page=None) -> str:
|
||||||
|
if path and request is not None:
|
||||||
|
return request.build_absolute_uri(path)
|
||||||
|
if page is not None:
|
||||||
|
return getattr(page, "full_url", "") or path or ""
|
||||||
|
return path or ""
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def page_language_options(page):
|
||||||
|
labels = {
|
||||||
|
_normalize_language_code(code): label
|
||||||
|
for code, label in settings.LANGUAGES
|
||||||
|
}
|
||||||
|
|
||||||
|
if not _is_translatable_page(page):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"code": _normalize_language_code(code),
|
||||||
|
"label": labels.get(_normalize_language_code(code), _normalize_language_code(code)),
|
||||||
|
"url": _fallback_locale_url(code),
|
||||||
|
}
|
||||||
|
for code, _label in settings.LANGUAGES
|
||||||
|
]
|
||||||
|
|
||||||
|
translations = _translated_pages(page)
|
||||||
|
options = []
|
||||||
|
for code, _label in settings.LANGUAGES:
|
||||||
|
language_code = _normalize_language_code(code)
|
||||||
|
translated_page = translations.get(language_code)
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"code": language_code,
|
||||||
|
"label": labels.get(language_code, language_code),
|
||||||
|
"url": translated_page.url if translated_page is not None else _fallback_locale_url(language_code),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def page_canonical_url(context):
|
||||||
|
request = context.get("request")
|
||||||
|
page = context.get("page") or context.get("self")
|
||||||
|
if page is not None and getattr(page, "url", None):
|
||||||
|
return _build_absolute_url(request, page.url, page)
|
||||||
|
if request is not None:
|
||||||
|
return request.build_absolute_uri()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def page_hreflang_links(context):
|
||||||
|
request = context.get("request")
|
||||||
|
page = context.get("page") or context.get("self")
|
||||||
|
if not _is_translatable_page(page):
|
||||||
|
return []
|
||||||
|
|
||||||
|
translations = _translated_pages(page)
|
||||||
|
links = []
|
||||||
|
for code, _label in settings.LANGUAGES:
|
||||||
|
language_code = _normalize_language_code(code)
|
||||||
|
translated_page = translations.get(language_code)
|
||||||
|
if translated_page is None or not getattr(translated_page, "url", None):
|
||||||
|
continue
|
||||||
|
links.append(
|
||||||
|
{
|
||||||
|
"code": language_code,
|
||||||
|
"url": _build_absolute_url(request, translated_page.url, translated_page),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
default_language = _normalize_language_code(settings.LANGUAGE_CODE)
|
||||||
|
default_page = translations.get(default_language)
|
||||||
|
if default_page is not None and getattr(default_page, "url", None):
|
||||||
|
links.append(
|
||||||
|
{
|
||||||
|
"code": "x-default",
|
||||||
|
"url": _build_absolute_url(request, default_page.url, default_page),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return links
|
||||||
26
mandelstudio/templatetags/mandelstudio_footer.py
Normal file
26
mandelstudio/templatetags/mandelstudio_footer.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from django import template
|
||||||
|
from wagtail.models import Site
|
||||||
|
|
||||||
|
from mandelstudio.models import LocalizedFooterContent
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def localized_footer_content(context):
|
||||||
|
request = context.get("request")
|
||||||
|
if request is None:
|
||||||
|
return None
|
||||||
|
site = getattr(request, "site", None) or Site.find_for_request(request)
|
||||||
|
if site is None:
|
||||||
|
return None
|
||||||
|
language_code = getattr(request, "LANGUAGE_CODE", None)
|
||||||
|
if not language_code:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
LocalizedFooterContent.objects.filter(
|
||||||
|
site=site,
|
||||||
|
locale__language_code=language_code,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
23
mandelstudio/templatetags/mandelstudio_i18n.py
Normal file
23
mandelstudio/templatetags/mandelstudio_i18n.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
SKIP_TO_CONTENT = {
|
||||||
|
"nl": "Ga naar inhoud",
|
||||||
|
"en": "Skip to content",
|
||||||
|
"de": "Zum Inhalt springen",
|
||||||
|
"fr": "Aller au contenu",
|
||||||
|
"es": "Ir al contenido",
|
||||||
|
"it": "Vai al contenuto",
|
||||||
|
"pt": "Ir para o conteúdo",
|
||||||
|
"ru": "Перейти к содержанию",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def skip_to_content_text(context) -> str:
|
||||||
|
request = context.get("request")
|
||||||
|
language_code = getattr(request, "LANGUAGE_CODE", "nl")
|
||||||
|
return SKIP_TO_CONTENT.get(language_code, SKIP_TO_CONTENT["en"])
|
||||||
@@ -1,13 +1,26 @@
|
|||||||
from django.conf.urls.i18n import i18n_patterns
|
from django.views.decorators.cache import cache_page
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from ocyan.main.urls import urlpatterns as ocyan_urlpatterns
|
from ocyan.main.urls import urlpatterns as ocyan_urlpatterns
|
||||||
|
from ocyan.plugin.wagtail_oscar_integration.constants import CACHE_DURATION
|
||||||
|
|
||||||
|
from .sitemaps import robots_txt
|
||||||
|
from .sitemaps import sitemap_index
|
||||||
|
from .sitemaps import sitemap_section
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("i18n/", include("django.conf.urls.i18n")),
|
path("i18n/", include("django.conf.urls.i18n")),
|
||||||
|
path("robots.txt", robots_txt, name="robots-txt"),
|
||||||
|
path(
|
||||||
|
"sitemap.xml",
|
||||||
|
cache_page(CACHE_DURATION)(sitemap_index),
|
||||||
|
name="sitemap-index",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"sitemap-<section>.xml",
|
||||||
|
cache_page(CACHE_DURATION)(sitemap_section),
|
||||||
|
name="sitemaps",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += i18n_patterns(
|
urlpatterns += ocyan_urlpatterns
|
||||||
*ocyan_urlpatterns,
|
|
||||||
prefix_default_language=False,
|
|
||||||
)
|
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -3,7 +3,7 @@ import json
|
|||||||
|
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
install_requires: list = ["setuptools", "ocyan.main"]
|
install_requires: list = ["setuptools", "ocyan.main", "elasticsearch<9"]
|
||||||
|
|
||||||
# Add frets dependencies
|
# Add frets dependencies
|
||||||
with open("mandelstudio/ocyan.json", encoding="utf-8") as fp:
|
with open("mandelstudio/ocyan.json", encoding="utf-8") as fp:
|
||||||
|
|||||||
36
templates/oxyan/partials/footer.html
Normal file
36
templates/oxyan/partials/footer.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{% load staticfiles %}
|
||||||
|
{% load wagtailcore_tags wagtailimages_tags wagtailsettings_tags cache mandelstudio_footer %}
|
||||||
|
{% get_settings %}
|
||||||
|
{% localized_footer_content as localized_footer %}
|
||||||
|
|
||||||
|
{% cache 300 footer_menu LANGUAGE_CODE request.site %}
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
{% with footer=localized_footer.footer|default:settings.ocyan_plugin_wagtail.OcyanSettings.footer %}
|
||||||
|
{% for block in footer %}
|
||||||
|
<div class="col-lg-3 col-md-6 col-sm-12 footer_column {{ block.block_type|slugify }}">
|
||||||
|
{% include_block block %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<section class="copyright_wrapper">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 copyright_block">
|
||||||
|
{% if localized_footer and localized_footer.mini_footer %}
|
||||||
|
{% for block in localized_footer.mini_footer %}
|
||||||
|
{% include_block block %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% include_block settings.ocyan_plugin_wagtail.OcyanSettings.mini_footer %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endcache %}
|
||||||
Reference in New Issue
Block a user