Compare commits
11 Commits
e4c6e3dcef
...
codex/prod
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bfd4d789b | |||
| 7db05fea47 | |||
| 57f4c0044a | |||
| 3c8e7e923f | |||
| 0d0a2cb36c | |||
| 27db3bc536 | |||
| f093a201d1 | |||
| 644d3c0b7b | |||
| bfdf061f31 | |||
| da0798c218 | |||
| 1f05011a63 |
@@ -10,13 +10,13 @@ pipeline {
|
||||
skipDefaultCheckout(true)
|
||||
}
|
||||
environment {
|
||||
STAGING_AUDIT_PROJECT_NAME = 'mandelstudio'
|
||||
STAGING_AUDIT_HOST = 'root@49.12.204.96'
|
||||
STAGING_AUDIT_PROJECT_DIR = '/home/www-mandelstudio/mandelstudio'
|
||||
STAGING_AUDIT_MANAGE = '/var/lib/virtualenv/mandelstudio/bin/manage.py'
|
||||
STAGING_AUDIT_SSH_CREDENTIALS_ID = 'staging-root-ssh'
|
||||
}
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
agent { label 'built-in' }
|
||||
steps {
|
||||
withCredentials([sshUserPrivateKey(credentialsId: 'gitea-ssh', keyFileVariable: 'GIT_KEYFILE')]) {
|
||||
sh '''
|
||||
@@ -39,10 +39,10 @@ pipeline {
|
||||
timeout(time: 10, unit: 'MINUTES')
|
||||
}
|
||||
steps {
|
||||
checkout scm
|
||||
sh 'mkdir -p artifacts && [ -f artifacts/multilingual-audit.json ] && cp artifacts/multilingual-audit.json artifacts/previous-multilingual-audit.json || true'
|
||||
sh 'chmod +x scripts/run_remote_multilingual_audit.sh'
|
||||
withCredentials([sshUserPrivateKey(credentialsId: env.STAGING_AUDIT_SSH_CREDENTIALS_ID, keyFileVariable: 'STAGING_SSH_KEYFILE')]) {
|
||||
sh './scripts/run_remote_multilingual_audit.sh'
|
||||
}
|
||||
script {
|
||||
int status = sh(script: 'python3 scripts/multilingual_audit_ci.py --json artifacts/multilingual-audit.json --previous-json artifacts/previous-multilingual-audit.json', returnStatus: true)
|
||||
if (status == 2) {
|
||||
|
||||
@@ -55,13 +55,18 @@ The audit summary is interpreted as follows:
|
||||
|
||||
This keeps deploys safe without making warning-level cleanup a hard blocker.
|
||||
|
||||
## Jenkins requirements
|
||||
No dedicated staging SSH credential is required for the multilingual audit stage.
|
||||
## Required Jenkins credential
|
||||
Credential location:
|
||||
- `Manage Jenkins -> Credentials -> System -> Global credentials`
|
||||
|
||||
The audit runs through `/srv/apps/mandel-dashboard/bin/deploy_stg_from_jenkins.py --command`, using the same sudo-whitelisted deployment entrypoint as staging deployment.
|
||||
Credential to add:
|
||||
- `Kind`: `SSH Username with private key`
|
||||
- `ID`: `staging-root-ssh`
|
||||
- `Username`: `root`
|
||||
- `Private key`: staging SSH key
|
||||
|
||||
Current implementation uses the following environment defaults:
|
||||
- `STAGING_AUDIT_PROJECT_NAME=mandelstudio`
|
||||
- `STAGING_AUDIT_HOST=root@49.12.204.96`
|
||||
- `STAGING_AUDIT_PROJECT_DIR=/home/www-mandelstudio/mandelstudio`
|
||||
- `STAGING_AUDIT_MANAGE=/var/lib/virtualenv/mandelstudio/bin/manage.py`
|
||||
|
||||
@@ -101,7 +106,7 @@ This happens when the remote audit times out or fails, and is intentional so Jen
|
||||
## Local rerun
|
||||
To rerun the same remote audit flow locally:
|
||||
```bash
|
||||
export STAGING_AUDIT_PROJECT_NAME='mandelstudio'
|
||||
export STAGING_AUDIT_HOST='root@49.12.204.96'
|
||||
export STAGING_AUDIT_PROJECT_DIR='/home/www-mandelstudio/mandelstudio'
|
||||
export STAGING_AUDIT_MANAGE='/var/lib/virtualenv/mandelstudio/bin/manage.py'
|
||||
./scripts/run_remote_multilingual_audit.sh
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
## Devpi Release Flow
|
||||
|
||||
### Current state
|
||||
|
||||
- `mandel/testing` is the active package source for MandelBlog project builds.
|
||||
- `ocyan.plugin.template_engine==0.2.12` is published there and is the current production-safe version.
|
||||
- `mandel/stable` is not available yet.
|
||||
|
||||
This means production is intentionally running from the testing index for now, to avoid breaking installs while the stable index is not provisioned.
|
||||
|
||||
### Index roles
|
||||
|
||||
- `mandel/testing`
|
||||
- pre-production and current fallback source
|
||||
- currently also the active production source until stable exists
|
||||
- `mandel/stable`
|
||||
- intended production index
|
||||
- not yet provisioned
|
||||
|
||||
### Promotion flow
|
||||
|
||||
When `mandel/stable` exists, promote existing artifacts without rebuilding:
|
||||
|
||||
```bash
|
||||
devpi use https://pypi.mandelblog.com/mandel/testing
|
||||
devpi login mandel
|
||||
devpi push ocyan-plugin-template-engine==0.2.12 mandel/stable
|
||||
```
|
||||
|
||||
### Admin prerequisite
|
||||
|
||||
Promotion requires a devpi admin to create the production index and grant upload or push permissions.
|
||||
|
||||
Recommended admin setup:
|
||||
|
||||
```bash
|
||||
devpi index -c mandel/stable bases=root/pypi volatile=False acl_upload=mandel,Mandel-publish
|
||||
```
|
||||
|
||||
### Planned stable-first install order
|
||||
|
||||
Do not enable this until `mandel/stable` exists:
|
||||
|
||||
```bash
|
||||
PIP_INDEX_URL=https://pypi.mandelblog.com/mandel/stable/+simple/
|
||||
PIP_EXTRA_INDEX_URL=https://pypi.mandelblog.com/mandel/testing/+simple/
|
||||
```
|
||||
|
||||
### CI behavior
|
||||
|
||||
- If the stable index is missing, Jenkins logs:
|
||||
- `devpi stable index not available, using testing as production source`
|
||||
- The build does not fail because of the missing stable index.
|
||||
- Installs continue from `mandel/testing`.
|
||||
|
||||
### Validation checklist
|
||||
|
||||
After stable becomes available and promotion is done:
|
||||
|
||||
1. confirm both wheel and sdist are visible in the stable simple index
|
||||
2. switch MandelStudio to stable-first
|
||||
3. run Jenkins build and deploy
|
||||
4. verify installed version is still `0.2.12`
|
||||
5. recheck editor validation for:
|
||||
- `/contact/`
|
||||
- `/diensten/`
|
||||
- `#demo`
|
||||
- absolute URLs
|
||||
@@ -49,7 +49,6 @@ CTA_RULES = {
|
||||
r"^Service",
|
||||
r"^Dienstleistungen",
|
||||
r"^Erstgespräch",
|
||||
r"^Beratung",
|
||||
r"^Einführ",
|
||||
r"^Anpassung",
|
||||
r"^Ansichts",
|
||||
@@ -84,7 +83,6 @@ CTA_RULES = {
|
||||
r"^Descubrir",
|
||||
r"^Contactar",
|
||||
r"^Planificar",
|
||||
r"^Program",
|
||||
r"^Programe",
|
||||
r"^Concertar",
|
||||
r"^Enviar",
|
||||
@@ -143,8 +141,6 @@ def validate_cta(locale_code: str, field_path: str, normalized: str):
|
||||
last_segment = field_path.split(".")[-1]
|
||||
if last_segment not in CTA_FIELDS:
|
||||
return []
|
||||
if any(
|
||||
re.search(pattern, normalized) for pattern in CTA_RULES.get(locale_code, ())
|
||||
):
|
||||
if any(re.search(pattern, normalized) for pattern in CTA_RULES.get(locale_code, ())):
|
||||
return []
|
||||
return [make_issue("cta_language_mismatch", field_path, normalized)]
|
||||
|
||||
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
@@ -14,6 +14,7 @@
|
||||
"ocyan.plugin.oscar_partner",
|
||||
"ocyan.plugin.oscar_shipping",
|
||||
"ocyan.plugin.oscar_sequential_order_numbers",
|
||||
"ocyan.plugin.payment_mollie",
|
||||
"ocyan.plugin.roadrunner_bs5",
|
||||
"ocyan.plugin.template_engine",
|
||||
"ocyan.plugin.roadrunner_productchooser",
|
||||
@@ -30,8 +31,7 @@
|
||||
"ocyan.plugin.wagtail_content_page",
|
||||
"ocyan.plugin.wagtail_forms",
|
||||
"ocyan.plugin.wagtail_oscar_integration",
|
||||
"ocyan.plugin.roadrunner_highlight_slider",
|
||||
"ocyan.plugin.wordspinner"
|
||||
"ocyan.plugin.roadrunner_highlight_slider"
|
||||
],
|
||||
"settings": {
|
||||
"cookie_jar": {
|
||||
@@ -63,6 +63,24 @@
|
||||
"en"
|
||||
]
|
||||
},
|
||||
"payment_mollie": {
|
||||
"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": {
|
||||
"allow_anon_checkout": true,
|
||||
"cancelled_order_status": "cancelled",
|
||||
|
||||
@@ -8,6 +8,7 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.0/ref/settings/
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -26,6 +27,49 @@ INSTALLED_APPS = [
|
||||
"mandelstudio",
|
||||
] + 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.
|
||||
if "django.middleware.locale.LocaleMiddleware" not 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,40 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load agency_navigation %}
|
||||
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-lg navbar-light header-inner">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" title="{% trans 'Website logo en home pagina navigatie' %}" href="/">
|
||||
{% include "partials/brand.html" with big=True %}
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#carbasaHeaderNav" aria-controls="carbasaHeaderNav" aria-expanded="false" aria-label="{% trans 'Toggle navigation' %}">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
{% block nav %}
|
||||
<div class="collapse navbar-collapse menu-bar page-menu-bar" id="carbasaHeaderNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown agency-nav-dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="/diensten/" id="carbasaHeaderDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% trans "Our Collection" %}
|
||||
</a>
|
||||
{% agency_nav_pages as nav_pages %}
|
||||
<ul class="dropdown-menu" aria-labelledby="carbasaHeaderDropdown">
|
||||
{% for nav_page in nav_pages %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ nav_page.url }}">{{ nav_page.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block user_bar %}
|
||||
{% include "oxyan/headers/partials/carbasa-user-bar.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -1,33 +1,16 @@
|
||||
{% extends "carbasa/headers/header.html" %}
|
||||
{% load agency_navigation %}
|
||||
{% load i18n oxyan category_tags ocyan_main ocyanjson wagtailsettings_tags %}
|
||||
|
||||
{% block nav %}
|
||||
{% ocyanjson "theme" "menu_depth" 1 as menu_depth %}
|
||||
<div class="collapse navbar-collapse menu-bar page-menu-bar" id="navbarSupportedContent">
|
||||
<div class="brand-wrapper">
|
||||
{% include 'partials/brand.html' with big=True %}
|
||||
</div>
|
||||
{% agency_nav_pages as nav_pages %}
|
||||
<ul class="navbar-nav">
|
||||
{% for nav_page in nav_pages %}
|
||||
{% if nav_page.nav_children %}
|
||||
<li class="nav-item dropdown agency-nav-dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="{{ nav_page.url }}" id="agency-nav-{{ nav_page.nav_key }}" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ nav_page.title }}
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="agency-nav-{{ nav_page.nav_key }}">
|
||||
{% for child_page in nav_page.nav_children %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ child_page.url }}">{{ child_page.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item child">
|
||||
<a class="nav-link" href="{{ nav_page.url }}">{{ nav_page.title }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% rootpage_as_category as page_tree_root %}
|
||||
{% category_tree 2 page_tree_root as page_tree_items %}
|
||||
{% include "partials/dropdown.html" with menu_items=page_tree_items limit=2 %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<section class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-10 col-xl-9">
|
||||
<div class="te-richtext card border-0 shadow-sm rounded-4">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
{{ value.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.te-modern-saas .te-richtext {
|
||||
color: var(--te-color-text-base);
|
||||
background: color-mix(in srgb, var(--te-color-surface-soft) 18%, white 82%);
|
||||
}
|
||||
|
||||
.te-modern-saas .te-richtext .card-body > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.te-modern-saas .te-richtext h2,
|
||||
.te-modern-saas .te-richtext h3,
|
||||
.te-modern-saas .te-richtext h4 {
|
||||
color: var(--te-color-surface-strong);
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.te-modern-saas .te-richtext p,
|
||||
.te-modern-saas .te-richtext li {
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.te-modern-saas .te-richtext ul,
|
||||
.te-modern-saas .te-richtext ol {
|
||||
padding-left: 1.25rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.te-modern-saas .te-richtext a {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "layout.html" %}
|
||||
{% load wagtailcore_tags oxyan static string_filters i18n %}
|
||||
{% load wagtailcore_tags oxyan static string_filters mandelstudio_i18n %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
@@ -15,26 +15,13 @@
|
||||
{% include "engine/partials/tech_theme_overrides.html" %}
|
||||
{% include "engine/partials/travel_theme_overrides.html" %}
|
||||
{% include "engine/partials/saas_theme_overrides.html" %}
|
||||
<style>
|
||||
:root { --mb-site-header-height: 88px; }
|
||||
header.mega_header {
|
||||
z-index: 1200;
|
||||
}
|
||||
.te-modern-saas .te-block--saas-testimonials .saas-testimonials__header {
|
||||
top: calc(var(--mb-site-header-height) + 8px);
|
||||
z-index: 20;
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
:root { --mb-site-header-height: 72px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block layout %}
|
||||
<a class="btn btn-secondary hidelink" id="main_content_link" href="#skip_header" tabindex="2">
|
||||
{% if request.LANGUAGE_CODE == 'ru' %}Перейти к содержанию{% elif request.LANGUAGE_CODE == 'de' %}Zum Inhalt springen{% elif request.LANGUAGE_CODE == 'fr' %}Aller au contenu{% elif request.LANGUAGE_CODE == 'es' %}Ir al contenido{% elif request.LANGUAGE_CODE == 'it' %}Vai al contenuto{% elif request.LANGUAGE_CODE == 'pt' %}Ir para o conteúdo{% elif request.LANGUAGE_CODE == 'nl' %}Ga naar inhoud{% else %}Skip to content{% endif %}
|
||||
{% skip_to_content_text %}
|
||||
</a>
|
||||
{% include "carbasa/headers/header.html" %}
|
||||
{% include_header header_template|default:"engine/partials/header.html" %}
|
||||
<div id="main_content" tabindex="-1">
|
||||
<div class="te-modern-saas">
|
||||
<main>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "layout.html" %}
|
||||
{% load wagtailcore_tags oxyan static string_filters i18n %}
|
||||
{% load wagtailcore_tags oxyan static string_filters mandelstudio_i18n %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
@@ -15,26 +15,13 @@
|
||||
{% include "engine/partials/tech_theme_overrides.html" %}
|
||||
{% include "engine/partials/travel_theme_overrides.html" %}
|
||||
{% include "engine/partials/saas_theme_overrides.html" %}
|
||||
<style>
|
||||
:root { --mb-site-header-height: 88px; }
|
||||
header.mega_header {
|
||||
z-index: 1200;
|
||||
}
|
||||
.te-modern-saas .te-block--saas-testimonials .saas-testimonials__header {
|
||||
top: calc(var(--mb-site-header-height) + 8px);
|
||||
z-index: 20;
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
:root { --mb-site-header-height: 72px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block layout %}
|
||||
<a class="btn btn-secondary hidelink" id="main_content_link" href="#skip_header" tabindex="2">
|
||||
{% if request.LANGUAGE_CODE == 'ru' %}Перейти к содержанию{% elif request.LANGUAGE_CODE == 'de' %}Zum Inhalt springen{% elif request.LANGUAGE_CODE == 'fr' %}Aller au contenu{% elif request.LANGUAGE_CODE == 'es' %}Ir al contenido{% elif request.LANGUAGE_CODE == 'it' %}Vai al contenuto{% elif request.LANGUAGE_CODE == 'pt' %}Ir para o conteúdo{% elif request.LANGUAGE_CODE == 'nl' %}Ga naar inhoud{% else %}Skip to content{% endif %}
|
||||
{% skip_to_content_text %}
|
||||
</a>
|
||||
{% include "carbasa/headers/header.html" %}
|
||||
{% include_header header_template|default:"engine/partials/header.html" %}
|
||||
<div id="main_content" tabindex="-1">
|
||||
<div class="te-modern-saas">
|
||||
<main class="te-section">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "layout.html" %}
|
||||
{% load wagtailcore_tags oxyan static string_filters i18n %}
|
||||
{% load wagtailcore_tags oxyan static string_filters mandelstudio_i18n %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
@@ -15,26 +15,13 @@
|
||||
{% include "engine/partials/tech_theme_overrides.html" %}
|
||||
{% include "engine/partials/travel_theme_overrides.html" %}
|
||||
{% include "engine/partials/saas_theme_overrides.html" %}
|
||||
<style>
|
||||
:root { --mb-site-header-height: 88px; }
|
||||
header.mega_header {
|
||||
z-index: 1200;
|
||||
}
|
||||
.te-modern-saas .te-block--saas-testimonials .saas-testimonials__header {
|
||||
top: calc(var(--mb-site-header-height) + 8px);
|
||||
z-index: 20;
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
:root { --mb-site-header-height: 72px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block layout %}
|
||||
<a class="btn btn-secondary hidelink" id="main_content_link" href="#skip_header" tabindex="2">
|
||||
{% if request.LANGUAGE_CODE == 'ru' %}Перейти к содержанию{% elif request.LANGUAGE_CODE == 'de' %}Zum Inhalt springen{% elif request.LANGUAGE_CODE == 'fr' %}Aller au contenu{% elif request.LANGUAGE_CODE == 'es' %}Ir al contenido{% elif request.LANGUAGE_CODE == 'it' %}Vai al contenuto{% elif request.LANGUAGE_CODE == 'pt' %}Ir para o conteúdo{% elif request.LANGUAGE_CODE == 'nl' %}Ga naar inhoud{% else %}Skip to content{% endif %}
|
||||
{% skip_to_content_text %}
|
||||
</a>
|
||||
{% include "carbasa/headers/header.html" %}
|
||||
{% include_header header_template|default:"engine/partials/header.html" %}
|
||||
<div id="main_content" tabindex="-1">
|
||||
<div class="te-modern-saas">
|
||||
<main>
|
||||
|
||||
1
mandelstudio/templates/engine/partials/basic.html
Normal file
1
mandelstudio/templates/engine/partials/basic.html
Normal file
@@ -0,0 +1 @@
|
||||
{% include "carbasa/headers/header.html" %}
|
||||
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" %}
|
||||
1
mandelstudio/templates/engine/partials/mega.html
Normal file
1
mandelstudio/templates/engine/partials/mega.html
Normal file
@@ -0,0 +1 @@
|
||||
{% include "carbasa/headers/header.html" %}
|
||||
@@ -1,58 +0,0 @@
|
||||
{% load wagtailimages_tags %}
|
||||
<section class="saas-demo saas-demo--inline saas-demo--{{ self.background_style }}"
|
||||
data-width="{{ self.layout_width }}">
|
||||
<div class="saas-demo__container">
|
||||
<header class="saas-demo__header">
|
||||
<h2 class="saas-demo__title">{{ self.section_title }}</h2>
|
||||
{% if self.section_subtitle %}
|
||||
<div class="saas-demo__subtitle">{{ self.section_subtitle }}</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<form class="saas-demo__form" action="{% url "contact_form:contact-form-handler" %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="saas-demo__fields">
|
||||
{% for field in self.form_fields %}
|
||||
<div class="saas-demo__field">
|
||||
<label class="saas-demo__label" for="demo-{{ field.field_type }}">
|
||||
{{ field.label }}
|
||||
{% if field.required %}<span class="saas-demo__required">*</span>{% endif %}
|
||||
</label>
|
||||
{% if field.field_type == 'message' %}
|
||||
<textarea class="saas-demo__textarea"
|
||||
id="demo-{{ field.field_type }}"
|
||||
name="{% if field.field_type == "email" %}email_from{% elif field.field_type == "phone" %}phonenumber{% elif field.field_type == "text" %}name{% else %}{{ field.field_type }}{% endif %}"
|
||||
placeholder="{{ field.placeholder }}"
|
||||
{% if field.required %}required{% endif %}></textarea>
|
||||
{% elif field.field_type == 'company_size' %}
|
||||
<select class="saas-demo__select"
|
||||
id="demo-{{ field.field_type }}"
|
||||
name="{% if field.field_type == "email" %}email_from{% elif field.field_type == "phone" %}phonenumber{% elif field.field_type == "text" %}name{% else %}{{ field.field_type }}{% endif %}"
|
||||
{% if field.required %}required{% endif %}>
|
||||
<option value="">{{ field.placeholder|default:"Select company size" }}</option>
|
||||
<option value="1-10">1-10 employees</option>
|
||||
<option value="11-50">11-50 employees</option>
|
||||
<option value="51-200">51-200 employees</option>
|
||||
<option value="201-500">201-500 employees</option>
|
||||
<option value="500+">500+ employees</option>
|
||||
</select>
|
||||
{% else %}
|
||||
<input class="saas-demo__input"
|
||||
type="{% if field.field_type == 'email' %}email{% elif field.field_type == 'phone' %}tel{% else %}text{% endif %}"
|
||||
id="demo-{{ field.field_type }}"
|
||||
name="{% if field.field_type == "email" %}email_from{% elif field.field_type == "phone" %}phonenumber{% elif field.field_type == "text" %}name{% else %}{{ field.field_type }}{% endif %}"
|
||||
placeholder="{{ field.placeholder }}"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="saas-demo__submit">{{ self.submit_button_text }}</button>
|
||||
|
||||
{% if self.privacy_text %}
|
||||
<div class="saas-demo__privacy">{{ self.privacy_text }}</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,94 +0,0 @@
|
||||
{% load wagtailimages_tags %}
|
||||
<section class="saas-demo saas-demo--modal-trigger saas-demo--{{ self.background_style }}"
|
||||
data-width="{{ self.layout_width }}"
|
||||
data-demo-modal-root>
|
||||
<div class="saas-demo__container">
|
||||
<div class="saas-demo__content">
|
||||
<h2 class="saas-demo__title">{{ self.section_title }}</h2>
|
||||
{% if self.section_subtitle %}
|
||||
<div class="saas-demo__subtitle">{{ self.section_subtitle }}</div>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="saas-demo__trigger" data-demo-modal-open>
|
||||
{{ self.submit_button_text }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="saas-demo__modal" data-demo-modal hidden>
|
||||
<div class="saas-demo__modal-backdrop" data-demo-modal-close></div>
|
||||
<div class="saas-demo__modal-content">
|
||||
<button type="button" class="saas-demo__modal-close" data-demo-modal-close aria-label="Close">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
|
||||
<h3 class="saas-demo__modal-title">{{ self.section_title }}</h3>
|
||||
|
||||
<form class="saas-demo__form" action="{% url "contact_form:contact-form-handler" %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="saas-demo__fields">
|
||||
{% for field in self.form_fields %}
|
||||
<div class="saas-demo__field">
|
||||
<label class="saas-demo__label" for="modal-{{ field.field_type }}">
|
||||
{{ field.label }}
|
||||
{% if field.required %}<span class="saas-demo__required">*</span>{% endif %}
|
||||
</label>
|
||||
{% if field.field_type == 'message' %}
|
||||
<textarea class="saas-demo__textarea"
|
||||
id="modal-{{ field.field_type }}"
|
||||
name="{% if field.field_type == "email" %}email_from{% elif field.field_type == "phone" %}phonenumber{% elif field.field_type == "text" %}name{% else %}{{ field.field_type }}{% endif %}"
|
||||
placeholder="{{ field.placeholder }}"
|
||||
{% if field.required %}required{% endif %}></textarea>
|
||||
{% else %}
|
||||
<input class="saas-demo__input"
|
||||
type="{% if field.field_type == 'email' %}email{% elif field.field_type == 'phone' %}tel{% else %}text{% endif %}"
|
||||
id="modal-{{ field.field_type }}"
|
||||
name="{% if field.field_type == "email" %}email_from{% elif field.field_type == "phone" %}phonenumber{% elif field.field_type == "text" %}name{% else %}{{ field.field_type }}{% endif %}"
|
||||
placeholder="{{ field.placeholder }}"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="saas-demo__submit">{{ self.submit_button_text }}</button>
|
||||
|
||||
{% if self.privacy_text %}
|
||||
<div class="saas-demo__privacy">{{ self.privacy_text }}</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function () {
|
||||
const roots = document.querySelectorAll('[data-demo-modal-root]');
|
||||
roots.forEach((root) => {
|
||||
if (root.dataset.modalBound === "1") return;
|
||||
root.dataset.modalBound = "1";
|
||||
|
||||
const modal = root.querySelector('[data-demo-modal]');
|
||||
const openBtn = root.querySelector('[data-demo-modal-open]');
|
||||
const closeBtns = root.querySelectorAll('[data-demo-modal-close]');
|
||||
if (!modal || !openBtn) return;
|
||||
|
||||
const openModal = () => {
|
||||
modal.hidden = false;
|
||||
document.body.style.overflow = 'hidden';
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
modal.hidden = true;
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
|
||||
openBtn.addEventListener('click', openModal);
|
||||
closeBtns.forEach((btn) => btn.addEventListener('click', closeModal));
|
||||
|
||||
root.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && !modal.hidden) closeModal();
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -1,156 +0,0 @@
|
||||
{% load wagtailimages_tags %}
|
||||
<section class="saas-demo saas-demo--split saas-demo--{{ self.background_style }} mandelstudio-demo-split"
|
||||
data-width="{{ self.layout_width }}">
|
||||
<style>
|
||||
.mandelstudio-demo-split {
|
||||
position: relative;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #f3f7fc 100%);
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: clamp(1.25rem, 2.2vw, 2.25rem);
|
||||
gap: clamp(1rem, 2vw, 2rem);
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__title {
|
||||
font-size: clamp(1.9rem, 3vw, 3rem);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
margin-bottom: .8rem;
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__subtitle {
|
||||
color: #556070;
|
||||
max-width: 54ch;
|
||||
margin-bottom: .9rem;
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__benefits-list {
|
||||
margin-bottom: 1.15rem;
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__benefit {
|
||||
color: #212b3a;
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__visual {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__image {
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(39, 66, 107, .14);
|
||||
box-shadow: 0 12px 28px rgba(20, 35, 68, .12);
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__form-wrapper {
|
||||
border: 1px solid rgba(42, 72, 120, .15);
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 30px rgba(18, 38, 76, .09);
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__form {
|
||||
padding: clamp(1rem, 2vw, 1.5rem);
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__input,
|
||||
.mandelstudio-demo-split .saas-demo__select,
|
||||
.mandelstudio-demo-split .saas-demo__textarea {
|
||||
background: #fbfdff;
|
||||
border-color: #d8e1ec;
|
||||
transition: border-color .2s ease, box-shadow .2s ease;
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__input:focus,
|
||||
.mandelstudio-demo-split .saas-demo__select:focus,
|
||||
.mandelstudio-demo-split .saas-demo__textarea:focus {
|
||||
border-color: #377dff;
|
||||
box-shadow: 0 0 0 .2rem rgba(55, 125, 255, .15);
|
||||
}
|
||||
.mandelstudio-demo-split .saas-demo__submit {
|
||||
box-shadow: 0 8px 18px rgba(40, 95, 214, .3);
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.mandelstudio-demo-split .saas-demo__visual {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="saas-demo__container">
|
||||
<div class="saas-demo__content">
|
||||
<h2 class="saas-demo__title">{{ self.section_title }}</h2>
|
||||
{% if self.section_subtitle %}
|
||||
<div class="saas-demo__subtitle">{{ self.section_subtitle }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if self.benefits_title or self.benefits %}
|
||||
<div class="saas-demo__benefits">
|
||||
{% if self.benefits_title %}
|
||||
<h3 class="saas-demo__benefits-title">{{ self.benefits_title }}</h3>
|
||||
{% endif %}
|
||||
{% if self.benefits %}
|
||||
<ul class="saas-demo__benefits-list">
|
||||
{% for benefit in self.benefits %}
|
||||
<li class="saas-demo__benefit">
|
||||
<svg class="saas-demo__check" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M4 10L8 14L16 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{{ benefit }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if self.side_image %}
|
||||
<div class="saas-demo__visual">
|
||||
{% image self.side_image width-640 class="saas-demo__image" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="saas-demo__form-wrapper">
|
||||
<form class="saas-demo__form" action="{% url "contact_form:contact-form-handler" %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="saas-demo__fields">
|
||||
{% for field in self.form_fields %}
|
||||
<div class="saas-demo__field">
|
||||
<label class="saas-demo__label" for="split-{{ field.field_type }}">
|
||||
{{ field.label }}
|
||||
{% if field.required %}<span class="saas-demo__required">*</span>{% endif %}
|
||||
</label>
|
||||
{% if field.field_type == 'message' %}
|
||||
<textarea class="saas-demo__textarea"
|
||||
id="split-{{ field.field_type }}"
|
||||
name="{% if field.field_type == "email" %}email_from{% elif field.field_type == "phone" %}phonenumber{% elif field.field_type == "text" %}name{% else %}{{ field.field_type }}{% endif %}"
|
||||
placeholder="{{ field.placeholder }}"
|
||||
{% if field.required %}required{% endif %}></textarea>
|
||||
{% elif field.field_type == 'company_size' %}
|
||||
<select class="saas-demo__select"
|
||||
id="split-{{ field.field_type }}"
|
||||
name="{% if field.field_type == "email" %}email_from{% elif field.field_type == "phone" %}phonenumber{% elif field.field_type == "text" %}name{% else %}{{ field.field_type }}{% endif %}"
|
||||
{% if field.required %}required{% endif %}>
|
||||
<option value="">{{ field.placeholder|default:"Select company size" }}</option>
|
||||
<option value="1-10">1-10 employees</option>
|
||||
<option value="11-50">11-50 employees</option>
|
||||
<option value="51-200">51-200 employees</option>
|
||||
<option value="201-500">201-500 employees</option>
|
||||
<option value="500+">500+ employees</option>
|
||||
</select>
|
||||
{% else %}
|
||||
<input class="saas-demo__input"
|
||||
type="{% if field.field_type == 'email' %}email{% elif field.field_type == 'phone' %}tel{% else %}text{% endif %}"
|
||||
id="split-{{ field.field_type }}"
|
||||
name="{% if field.field_type == "email" %}email_from{% elif field.field_type == "phone" %}phonenumber{% elif field.field_type == "text" %}name{% else %}{{ field.field_type }}{% endif %}"
|
||||
placeholder="{{ field.placeholder }}"
|
||||
{% if field.required %}required{% endif %}>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="saas-demo__submit">{{ self.submit_button_text }}</button>
|
||||
|
||||
{% if self.privacy_text %}
|
||||
<div class="saas-demo__privacy">{{ self.privacy_text }}</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,38 +0,0 @@
|
||||
{% load wagtailimages_tags %}
|
||||
<section class="saas-features saas-features--grid saas-features--{{ self.background_style }}"
|
||||
data-width="{{ self.layout_width }}">
|
||||
<div class="saas-features__container">
|
||||
<header class="saas-features__header">
|
||||
<h2 class="saas-features__title">{{ self.section_title }}</h2>
|
||||
{% if self.section_subtitle %}
|
||||
<div class="saas-features__subtitle">{{ self.section_subtitle }}</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
<div class="saas-features__grid saas-features__grid--cols-{{ self.columns }}">
|
||||
{% for feature in self.features %}
|
||||
<article class="saas-features__card{% if feature.highlight == 'featured' %} saas-features__card--featured{% endif %}">
|
||||
{% if feature.highlight == 'new' %}
|
||||
<span class="saas-features__badge">{% if request.LANGUAGE_CODE == 'ru' %}Ново{% elif request.LANGUAGE_CODE == 'de' %}Neu{% elif request.LANGUAGE_CODE == 'fr' %}Nouveau{% elif request.LANGUAGE_CODE == 'es' %}Nuevo{% elif request.LANGUAGE_CODE == 'it' %}Nuovo{% elif request.LANGUAGE_CODE == 'pt' %}Novo{% else %}New{% endif %}</span>
|
||||
{% endif %}
|
||||
<div class="saas-features__icon-wrapper">
|
||||
{% if feature.icon_image %}
|
||||
{% image feature.icon_image width-64 class="saas-features__icon-img" %}
|
||||
{% elif feature.icon %}
|
||||
<i class="saas-features__icon bi bi-{{ feature.icon }}"></i>
|
||||
{% else %}
|
||||
<div class="saas-features__icon-placeholder"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h3 class="saas-features__card-title">{{ feature.title }}</h3>
|
||||
{% if feature.description %}<div class="saas-features__card-desc">{{ feature.description }}</div>{% endif %}
|
||||
{% if feature.link_text and feature.link_url %}
|
||||
<a href="{{ feature.link_url }}" class="saas-features__card-link">
|
||||
{{ feature.link_text }}
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,30 +0,0 @@
|
||||
{% load wagtailimages_tags %}
|
||||
<section class="saas-integrations saas-integrations--logo-grid saas-integrations--{{ self.background_style }}" data-width="{{ self.layout_width }}">
|
||||
<div class="saas-integrations__container">
|
||||
<header class="saas-integrations__header">
|
||||
<h2 class="saas-integrations__title">{{ self.section_title }}</h2>
|
||||
{% if self.section_subtitle %}<div class="saas-integrations__subtitle">{{ self.section_subtitle }}</div>{% endif %}
|
||||
{% if self.integration_count %}
|
||||
<span class="saas-integrations__count">{{ self.integration_count }} {% if request.LANGUAGE_CODE == 'ru' %}интеграции{% elif request.LANGUAGE_CODE == 'de' %}Integrationen{% elif request.LANGUAGE_CODE == 'fr' %}intégrations{% elif request.LANGUAGE_CODE == 'es' %}integraciones{% elif request.LANGUAGE_CODE == 'it' %}integrazioni{% elif request.LANGUAGE_CODE == 'pt' %}integrações{% elif request.LANGUAGE_CODE == 'nl' %}integraties{% else %}integrations{% endif %}</span>
|
||||
{% endif %}
|
||||
</header>
|
||||
<div class="saas-integrations__grid">
|
||||
{% for integration in self.integrations %}
|
||||
<div class="saas-integrations__item{% if integration.is_featured != 'none' %} saas-integrations__item--{{ integration.is_featured }}{% endif %}">
|
||||
{% if integration.is_featured == 'new' %}
|
||||
<span class="saas-integrations__badge">{% if request.LANGUAGE_CODE == 'ru' %}Ново{% elif request.LANGUAGE_CODE == 'de' %}Neu{% elif request.LANGUAGE_CODE == 'fr' %}Nouveau{% elif request.LANGUAGE_CODE == 'es' %}Nuevo{% elif request.LANGUAGE_CODE == 'it' %}Nuovo{% elif request.LANGUAGE_CODE == 'pt' %}Novo{% elif request.LANGUAGE_CODE == 'nl' %}Nieuw{% else %}New{% endif %}</span>
|
||||
{% elif integration.is_featured == 'popular' %}
|
||||
<span class="saas-integrations__badge saas-integrations__badge--popular">{% if request.LANGUAGE_CODE == 'ru' %}Populair{% elif request.LANGUAGE_CODE == 'de' %}Beliebt{% elif request.LANGUAGE_CODE == 'fr' %}Populaire{% elif request.LANGUAGE_CODE == 'es' %}Popular{% elif request.LANGUAGE_CODE == 'it' %}Popolare{% elif request.LANGUAGE_CODE == 'pt' %}Popular{% elif request.LANGUAGE_CODE == 'nl' %}Populair{% else %}Popular{% endif %}</span>
|
||||
{% endif %}
|
||||
{% if integration.url %}<a href="{{ integration.url }}" class="saas-integrations__link">{% endif %}
|
||||
<div class="saas-integrations__logo">{% image integration.logo width-48 class="saas-integrations__logo-img" %}</div>
|
||||
<span class="saas-integrations__name">{{ integration.name }}</span>
|
||||
{% if integration.url %}</a>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if self.cta_text and self.cta_url %}
|
||||
<div class="saas-integrations__footer"><a href="{{ self.cta_url }}" class="saas-integrations__cta">{{ self.cta_text }}<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,92 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load oxyan %}
|
||||
{% load ocyan_main %}
|
||||
{% load ocyanjson %}
|
||||
{% load static %}
|
||||
{% load wagtailcore_tags wagtailimages_tags wagtailuserbar %}
|
||||
|
||||
{% block title %}{% firstof page.seo_title self.seo_title page.title self.title shop_name %}{% endblock %}
|
||||
{% block description %}{% firstof page.search_description self.search_description "" %}{% endblock %}
|
||||
|
||||
{% block extrahead %}
|
||||
{% if cookie_jar.settings.google_tag_manager and cookie_jar.functional.is_allowed %}
|
||||
<link rel="preconnect" href="https://www.googletagmanager.com"/>
|
||||
{% endif %}
|
||||
{% if cookie_jar.settings.google_analytics and cookie_jar.functional.is_allowed %}
|
||||
<link rel="preconnect" href="https://www.google-analytics.com/">
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% if cookie_jar.needs_approval %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'cookie_jar/css/cookie_jar.css' %}">
|
||||
{% endif %}
|
||||
{% for header_snippet in cookie_jar.activated_snippet_header_templates %}
|
||||
{% include header_snippet %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block layout %}
|
||||
{% if show_basket_popup_setting %}
|
||||
{% esi_fragment "partials/added_success.html" with sessionid=True oscar_open_basket=True request=request csrf_token=csrf_token only %}
|
||||
{% endif %}
|
||||
|
||||
{% block navbar %}
|
||||
{% include "carbasa/headers/header.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_wrapper %}
|
||||
<div id="main_content" tabindex="-1">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{% include "oxyan/partials/footer.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% ocyanjson "themes" "theme-switcher" as theme_switcher %}
|
||||
{% if theme_switcher %}
|
||||
{% include "oxyan/partials/theme_switcher.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{% include "oscar/partials/extrascripts.html" %}
|
||||
{{ block.super }}
|
||||
{% if cookie_jar.needs_approval %}
|
||||
<script src="{% static 'cookie_jar/js/cookie_jar.js' %}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block onbodyload %}
|
||||
{{ block.super }}
|
||||
oxyan.layout()
|
||||
oxyan.initModalPopup()
|
||||
oxyan.initializePriceUpdate()
|
||||
oxyan.IconHoverFix()
|
||||
oxyan.lazyIconDropdown()
|
||||
oxyan.toasts()
|
||||
oxyan.commerseHeader()
|
||||
oxyan.initWCAG()
|
||||
{% ocyanjson "themes" "image_zoom" as image_zoom %}
|
||||
{% if image_zoom %}
|
||||
oxyan.initImageZoom()
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block cdn_scripts %}
|
||||
{{ block.super }}
|
||||
{% ocyanjson "wagtail" "wagtailuserbar_position" as position %}
|
||||
{% if position %}
|
||||
{% wagtailuserbar position %}
|
||||
{% endif %}
|
||||
{% for footer_snippet in cookie_jar.activated_snippet_footer_templates %}
|
||||
{% include footer_snippet %}
|
||||
{% endfor %}
|
||||
{% include "cookie_jar/cookie_banner.html" %}
|
||||
{% if cookie_jar.needs_approval %}
|
||||
{% include "cookie_jar/partials/preferences_saved_toast.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% load wagtailcore_tags %}
|
||||
{% if self.heading %}<p class="footer_header">{{ self.heading }}</p>{% endif %}
|
||||
{% if children %}
|
||||
<ul class="mb-footer-links list-unstyled m-0">
|
||||
{% for page in children %}
|
||||
<li class="mb-footer-links__item mb-2">
|
||||
<a href="{% pageurl page %}">{{ page.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
@@ -1,15 +1,19 @@
|
||||
{% load i18n %}
|
||||
<div class="header-right">
|
||||
<form action="{% url 'set_language' %}" method="post" class="language-switcher-form d-inline-flex align-items-center me-2" aria-label="Language switcher">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<label for="header-language-switcher" class="visually-hidden">{% trans "Language" %}</label>
|
||||
<select id="header-language-switcher" name="language" class="form-select form-select-sm" style="width:auto; min-width:84px;" onchange="this.form.submit()">
|
||||
{% load i18n i18n_helpers %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% for code, name in LANGUAGES %}
|
||||
<option value="{{ code }}" {% if code == LANGUAGE_CODE %}selected{% endif %}>{{ code|upper }}</option>
|
||||
{% endfor %}
|
||||
<div class="header-right">
|
||||
<form action="{% url 'set_language' %}" method="post" class="ms-lang-switcher me-2" aria-label="Language switcher">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.path|untranslated_url }}">
|
||||
<label for="header-language-switcher" class="visually-hidden">{% trans "Language" %}</label>
|
||||
<select id="header-language-switcher" name="language" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="nl" {% if LANGUAGE_CODE == 'nl' %}selected{% endif %}>NL</option>
|
||||
<option value="en" {% if LANGUAGE_CODE == 'en' %}selected{% endif %}>EN</option>
|
||||
<option value="de" {% if LANGUAGE_CODE == 'de' %}selected{% endif %}>DE</option>
|
||||
<option value="fr" {% if LANGUAGE_CODE == 'fr' %}selected{% endif %}>FR</option>
|
||||
<option value="es" {% if LANGUAGE_CODE == 'es' %}selected{% endif %}>ES</option>
|
||||
<option value="it" {% if LANGUAGE_CODE == 'it' %}selected{% endif %}>IT</option>
|
||||
<option value="pt" {% if LANGUAGE_CODE == 'pt' %}selected{% endif %}>PT</option>
|
||||
<option value="ru" {% if LANGUAGE_CODE == 'ru' %}selected{% endif %}>RU</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
@@ -17,7 +21,7 @@
|
||||
<i class="fa fa-search"></i>
|
||||
</a>
|
||||
<a href="{% url 'customer:summary' %}" aria-label="{% trans 'Customer summary' %}" class="user-button menu-circle"><i class="fa fa-user-solid"></i></a>
|
||||
{% include "oxyan/headers/partials/mini_basket.html" %}
|
||||
{% include 'oxyan/headers/partials/mini_basket.html' %}
|
||||
</div>
|
||||
|
||||
<div class="alert-messages-header" aria-live="polite">
|
||||
|
||||
@@ -1,141 +1,36 @@
|
||||
{% load staticfiles %}
|
||||
{% load wagtailcore_tags wagtailimages_tags wagtailsettings_tags cache %}
|
||||
{% 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 %}
|
||||
<style>
|
||||
.mb-footer-wrap {
|
||||
margin-top: clamp(2rem, 4vw, 3.5rem);
|
||||
position: relative;
|
||||
}
|
||||
.mb-footer {
|
||||
position: relative;
|
||||
background:
|
||||
radial-gradient(120% 120% at 0% 0%, rgba(84, 149, 230, .22) 0%, rgba(84, 149, 230, 0) 45%),
|
||||
radial-gradient(90% 120% at 100% 0%, rgba(65, 206, 186, .16) 0%, rgba(65, 206, 186, 0) 45%),
|
||||
linear-gradient(180deg, #264f72 0%, #203f5c 100%);
|
||||
border-radius: 28px 28px 0 0;
|
||||
padding: clamp(2rem, 4vw, 3rem) 0;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,.12),
|
||||
0 -10px 24px rgba(20, 43, 72, .20);
|
||||
overflow: hidden;
|
||||
}
|
||||
.mb-footer:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, rgba(255,255,255,.07) 0%, rgba(255,255,255,0) 35%, rgba(255,255,255,.06) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.mb-footer .footer_column {
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
}
|
||||
.mb-footer .mb-footer__card {
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, .055);
|
||||
border: 1px solid rgba(255, 255, 255, .14);
|
||||
border-radius: 16px;
|
||||
padding: 1.1rem 1.15rem;
|
||||
backdrop-filter: blur(1.2px);
|
||||
}
|
||||
.mb-footer .footer_header {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .01em;
|
||||
margin-bottom: .9rem;
|
||||
color: #f4f8ff;
|
||||
}
|
||||
.mb-footer .footer_column,
|
||||
.mb-footer .footer_column * {
|
||||
color: rgba(237, 244, 255, .93);
|
||||
}
|
||||
.mb-footer .footer_column a {
|
||||
color: #eef4ff;
|
||||
text-decoration: none;
|
||||
transition: color .2s ease, transform .2s ease;
|
||||
}
|
||||
.mb-footer .footer_column a:hover {
|
||||
color: #ffffff;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.mb-footer .footer_column .rich-text p {
|
||||
margin-bottom: .65rem;
|
||||
line-height: 1.65;
|
||||
max-width: 34ch;
|
||||
}
|
||||
.mb-footer .mb-footer__card .aboutus-logo {
|
||||
max-height: 52px;
|
||||
width: auto;
|
||||
}
|
||||
.mb-footer .mb-footer__card .social {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.mb-footer .mb-footer__card .social a {
|
||||
border-color: rgba(255, 255, 255, .42);
|
||||
color: #ffffff;
|
||||
background: rgba(255,255,255,.08);
|
||||
}
|
||||
.mb-footer .mb-footer__card .social a:hover {
|
||||
background: rgba(255,255,255,.18);
|
||||
}
|
||||
.mb-copyright {
|
||||
background: #1b3650;
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid rgba(255,255,255,.16);
|
||||
}
|
||||
.mb-copyright .copyright_block,
|
||||
.mb-copyright .copyright_block * {
|
||||
color: rgba(234, 241, 255, .92);
|
||||
margin: 0;
|
||||
font-size: .95rem;
|
||||
}
|
||||
.mb-copyright .copyright_block a {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.mb-copyright .copyright_block a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.mb-footer {
|
||||
border-radius: 20px 20px 0 0;
|
||||
}
|
||||
.mb-footer .mb-footer__card {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="mb-footer-wrap">
|
||||
<footer class="footer mb-footer">
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="row g-4">
|
||||
{% with footer=settings.ocyan_plugin_wagtail.OcyanSettings.footer %}
|
||||
<div class="row">
|
||||
{% with footer=localized_footer.footer|default:settings.ocyan_plugin_wagtail.OcyanSettings.footer %}
|
||||
{% for block in footer %}
|
||||
{% if block.block_type == 'page_list' and block.value.page and not block.value.page.get_children.live.public %}
|
||||
{% else %}
|
||||
<div class="{% if footer|length == 3 %}col-lg-4{% elif footer|length == 2 %}col-lg-6{% else %}col-lg-3{% endif %} col-md-6 col-sm-12 footer_column {{ block.block_type|slugify }}">
|
||||
<div class="mb-footer__card">
|
||||
<div class="col-lg-3 col-md-6 col-sm-12 footer_column {{ block.block_type|slugify }}">
|
||||
{% include_block block %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<section class="copyright_wrapper mb-copyright">
|
||||
<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>
|
||||
</div>
|
||||
{% endcache %}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 500 500" preserveAspectRatio="none" class="corner {{ class }}">
|
||||
<path class="st0" d="M0,0c166.7,0,333.3,0,500,0c-31.4-0.5-212-0.1-356.5,145C0,289.1-0.5,468.2,0,500C0,333.3,0,166.7,0,0z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 214 B |
@@ -1,24 +0,0 @@
|
||||
{% load i18n ocyan_thumbnail %}
|
||||
{% if menu_items %}
|
||||
{% for menu_item in menu_items %}
|
||||
{% with category_icon=menu_item.category.icons.first %}
|
||||
{% if menu_item.has_children %}
|
||||
<li class="nav-item has_children">
|
||||
<a class="nav-link category-label" data-name="{{ menu_item.name|safe }}" data-href="{{ menu_item.get_absolute_url }}" tabindex="-1">
|
||||
<span>{% trans "Show everything in" %}</span>{{ menu_item.name }}
|
||||
</a>
|
||||
<ul class="menu-level">
|
||||
{% else %}
|
||||
<li class="nav-item child">
|
||||
<a class="nav-link child-category" href="{{ menu_item.get_absolute_url }}" tabindex="-1">
|
||||
{{ menu_item.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for close in menu_item.num_to_close %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -1,85 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django import template
|
||||
|
||||
from wagtail.models import Locale, Page
|
||||
|
||||
from mandelstudio.management.commands._agency_content import COMMON_CTA
|
||||
|
||||
register = template.Library()
|
||||
|
||||
SOURCE_PAGE_IDS = {
|
||||
"about": 128,
|
||||
"services": 129,
|
||||
"projects": 130,
|
||||
"contact": 131,
|
||||
"process": 192,
|
||||
}
|
||||
|
||||
NAV_CHILDREN = {
|
||||
"services": [200, 201, 202, 203],
|
||||
}
|
||||
|
||||
NAV_ORDER = ["services", "projects", "process", "about", "contact"]
|
||||
|
||||
|
||||
def _resolve_locale(language_code: str | None) -> Locale | None:
|
||||
if not language_code:
|
||||
return None
|
||||
try:
|
||||
return Locale.objects.get(language_code=language_code)
|
||||
except Locale.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def _translated_page(source_id: int, language_code: str | None) -> Page | None:
|
||||
locale = _resolve_locale(language_code)
|
||||
try:
|
||||
source = Page.objects.get(id=source_id)
|
||||
except Page.DoesNotExist:
|
||||
return None
|
||||
if locale is None:
|
||||
return source.specific
|
||||
translated = (
|
||||
Page.objects.filter(translation_key=source.translation_key, locale=locale)
|
||||
.live()
|
||||
.public()
|
||||
.specific()
|
||||
.first()
|
||||
)
|
||||
return translated or source.specific
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def agency_nav_pages(context):
|
||||
request = context.get("request")
|
||||
language_code = getattr(request, "LANGUAGE_CODE", None)
|
||||
pages = []
|
||||
for key in NAV_ORDER:
|
||||
page = _translated_page(SOURCE_PAGE_IDS[key], language_code)
|
||||
if page is not None:
|
||||
page.nav_children = [
|
||||
child
|
||||
for source_id in NAV_CHILDREN.get(key, [])
|
||||
if (child := _translated_page(source_id, language_code)) is not None
|
||||
]
|
||||
page.nav_key = key
|
||||
pages.append(page)
|
||||
return pages
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def agency_page(context, key: str):
|
||||
request = context.get("request")
|
||||
language_code = getattr(request, "LANGUAGE_CODE", None)
|
||||
source_id = SOURCE_PAGE_IDS.get(key)
|
||||
if source_id is None:
|
||||
return None
|
||||
return _translated_page(source_id, language_code)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def agency_primary_cta(context):
|
||||
request = context.get("request")
|
||||
language_code = getattr(request, "LANGUAGE_CODE", None) or "nl"
|
||||
return COMMON_CTA.get(language_code, COMMON_CTA["nl"])["primary"]
|
||||
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 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 = [
|
||||
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(
|
||||
*ocyan_urlpatterns,
|
||||
prefix_default_language=False,
|
||||
)
|
||||
urlpatterns += ocyan_urlpatterns
|
||||
|
||||
@@ -1,62 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${STAGING_AUDIT_PROJECT_NAME:?STAGING_AUDIT_PROJECT_NAME is required}"
|
||||
: "${STAGING_AUDIT_HOST:?STAGING_AUDIT_HOST is required}"
|
||||
: "${STAGING_AUDIT_PROJECT_DIR:?STAGING_AUDIT_PROJECT_DIR is required}"
|
||||
: "${STAGING_AUDIT_MANAGE:?STAGING_AUDIT_MANAGE is required}"
|
||||
|
||||
mkdir -p artifacts
|
||||
SSH_OPTS=${SSH_OPTS:-"-o StrictHostKeyChecking=accept-new"}
|
||||
if [[ -n "${STAGING_SSH_KEYFILE:-}" ]]; then
|
||||
SSH_OPTS="$SSH_OPTS -i ${STAGING_SSH_KEYFILE}"
|
||||
fi
|
||||
AUDIT_TIMEOUT_SECONDS=${AUDIT_TIMEOUT_SECONDS:-300}
|
||||
ARTIFACT_DIR=${ARTIFACT_DIR:-artifacts}
|
||||
OUTPUT_JSON=${OUTPUT_JSON:-${ARTIFACT_DIR}/multilingual-audit.json}
|
||||
mkdir -p "${ARTIFACT_DIR}"
|
||||
TMP_FILE=$(mktemp)
|
||||
trap 'rm -f "$TMP_FILE"' EXIT
|
||||
OUT_FILE="artifacts/multilingual-audit.json"
|
||||
TMP_FILE="${OUT_FILE}.tmp"
|
||||
|
||||
write_failure_json() {
|
||||
python3 - <<PY > "$OUT_FILE"
|
||||
import json
|
||||
print(json.dumps({
|
||||
"run_id": None,
|
||||
"total_urls_checked": 0,
|
||||
"issues_found": 0,
|
||||
"summary": {},
|
||||
"issues": {},
|
||||
"error": ${1@Q}
|
||||
}, indent=2))
|
||||
PY
|
||||
}
|
||||
|
||||
REMOTE_CMD="cd '${STAGING_AUDIT_PROJECT_DIR}' && '${STAGING_AUDIT_MANAGE}' audit_locales --format=json"
|
||||
|
||||
set +e
|
||||
STAGING_AUDIT_PROJECT_NAME="$STAGING_AUDIT_PROJECT_NAME" REMOTE_CMD="$REMOTE_CMD" AUDIT_TIMEOUT_SECONDS="$AUDIT_TIMEOUT_SECONDS" python3 - <<'PY2' > "$TMP_FILE"
|
||||
import json
|
||||
SSH_OPTS="$SSH_OPTS" STAGING_AUDIT_HOST="$STAGING_AUDIT_HOST" REMOTE_CMD="$REMOTE_CMD" AUDIT_TIMEOUT_SECONDS="$AUDIT_TIMEOUT_SECONDS" python3 - <<'PY' > "$TMP_FILE"
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
project = os.environ["STAGING_AUDIT_PROJECT_NAME"]
|
||||
remote_cmd = os.environ["REMOTE_CMD"]
|
||||
timeout_seconds = int(os.environ["AUDIT_TIMEOUT_SECONDS"])
|
||||
cmd = [
|
||||
"sudo", "-n", "-u", "mandel", "-g", "www-data",
|
||||
"/srv/apps/mandel-dashboard/.venv/bin/python",
|
||||
"/srv/apps/mandel-dashboard/bin/deploy_stg_from_jenkins.py",
|
||||
project,
|
||||
"--command",
|
||||
remote_cmd,
|
||||
]
|
||||
ssh_opts = shlex.split(os.environ["SSH_OPTS"])
|
||||
cmd = ["ssh", *ssh_opts, os.environ["STAGING_AUDIT_HOST"], os.environ["REMOTE_CMD"]]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_seconds, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(json.dumps({
|
||||
"error": "audit_failed",
|
||||
"details": f"Audit command timed out after {timeout_seconds} seconds",
|
||||
"exit_code": 124,
|
||||
}, indent=2))
|
||||
sys.exit(2)
|
||||
stdout = result.stdout.strip()
|
||||
stderr = result.stderr.strip()
|
||||
if result.returncode != 0:
|
||||
if stdout:
|
||||
print(stdout)
|
||||
else:
|
||||
print(json.dumps({
|
||||
"error": "audit_failed",
|
||||
"details": stderr or f"Audit command failed with exit status {result.returncode}",
|
||||
"exit_code": result.returncode,
|
||||
}, indent=2))
|
||||
sys.exit(2)
|
||||
print(stdout)
|
||||
PY2
|
||||
status=$?
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=int(os.environ["AUDIT_TIMEOUT_SECONDS"]),
|
||||
)
|
||||
sys.stdout.write(proc.stdout)
|
||||
sys.stderr.write(proc.stderr)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
sys.stderr.write(exc.stderr or "")
|
||||
raise SystemExit(124)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
sys.stdout.write(exc.stdout or "")
|
||||
sys.stderr.write(exc.stderr or "")
|
||||
raise SystemExit(exc.returncode)
|
||||
PY
|
||||
rc=$?
|
||||
set -e
|
||||
cp "$TMP_FILE" "$OUTPUT_JSON"
|
||||
cat "$OUTPUT_JSON"
|
||||
exit $status
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
mv "$TMP_FILE" "$OUT_FILE"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$TMP_FILE"
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
write_failure_json "Remote multilingual audit timed out after ${AUDIT_TIMEOUT_SECONDS}s"
|
||||
else
|
||||
write_failure_json "Remote multilingual audit failed with exit status ${rc}"
|
||||
fi
|
||||
exit $rc
|
||||
|
||||
2
setup.py
2
setup.py
@@ -3,7 +3,7 @@ import json
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
install_requires: list = ["setuptools", "ocyan.main"]
|
||||
install_requires: list = ["setuptools", "ocyan.main", "elasticsearch<9"]
|
||||
|
||||
# Add frets dependencies
|
||||
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