Compare commits

..

No commits in common. "master" and "test-menu" have entirely different histories.

84 changed files with 2362 additions and 11542 deletions

View file

@ -1,29 +1,3 @@
.git
.gitignore
.dockerignore
.env
*.pyc
__pycache__
*.sqlite3
*.db
media/
static/
# node_modules/ - keeping this to avoid npm install issues in Docker
env/
venv/
.venv/
*.log
.DS_Store
.idea/
.vscode/
*.swp
*.swo
*~
upgrade-logs/
publichealth-dev.sqlite3
CLAUDE.md
README.md
docs/
*.backup
requirements-*.txt
fix_migrations.py
media

8
.gitignore vendored
View file

@ -1,12 +1,12 @@
.env
env
/env/
/venv/
*.pyc
.sass-cache
.DS_Store
*.swp
*.sqlite3
/docker-compose.yml
/env/
/venv/
/static/
/assets/
/media/
@ -14,9 +14,7 @@ env
/.keys/
/.vagrant/
/Vagrantfile.local
/docker-compose.yml
/docs/_build/
/node_modules/
/publichealth/static/libs
publichealth.home.json
.history

View file

@ -1,8 +1,3 @@
# Change Log
Please see [Pull Request history](https://github.com/datalets/public-health-ch/pulls?q=is%3Apr+is%3Aclosed) on GitHub.
## 2.0.0 - 2022-07-12
- 10694: Introduce table block to show program details
- 10695: Add a header image to the article index template

View file

@ -1,152 +0,0 @@
FROM docker.io/python:3.12-slim AS base
ENV \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
# python:
PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libmagic1 \
libpq5 \
libjpeg62-turbo \
libfreetype6 \
zlib1g \
gettext \
procps \
curl \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
ENV VIRTUAL_ENV=/venv
# Use production configuration.
ENV DJANGO_SETTINGS_MODULE=publichealth.settings.production
ENV DJANGO_CONFIGURATION=production
## Build frontend
FROM docker.io/node:18-slim AS frontend-builder
RUN mkdir -p /build
WORKDIR /build
# Install grunt-cli
RUN npm install -g grunt-cli
# Copy package files and pre-installed node_modules
COPY package.json package-lock.json Gruntfile.js ./
# Copy node_modules if they exist locally (avoids git clone issues)
COPY node_modules ./node_modules
# Copy static assets and libs
COPY publichealth/static/ ./publichealth/static/
RUN mkdir -p publichealth/static/libs && \
cp -r node_modules/@bower_components/* publichealth/static/libs/
# Build frontend assets (skip grunt if it fails, assets will be compiled at runtime)
RUN grunt || echo "Grunt build skipped, will use django-compressor at runtime"
## Build backend
FROM base AS backend-builder
ENV \
# pip:
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
gcc \
libmagic-dev \
libpq-dev \
libjpeg-dev \
libfreetype6-dev \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# Install python build tools.
RUN pip install --upgrade pip wheel
RUN python3 -m venv $VIRTUAL_ENV
RUN mkdir -p /build
WORKDIR /build
# Install python dependencies.
COPY requirements.txt .
RUN . /venv/bin/activate && pip install -r requirements.txt
# We will run dumb-init as pid 1.
RUN . /venv/bin/activate && pip install dumb-init
# We will run our app using gunicorn.
RUN . /venv/bin/activate && pip install gunicorn
# Install django-libsass for SASS compilation
RUN . /venv/bin/activate && pip install django-libsass
# Copy our own code into to the build image.
# WARNING: use .dockerignore file to exclude unwanted files
COPY . /build/
# Copy javascript assets from frontend builder
COPY --from=frontend-builder /build/publichealth/static /build/publichealth/static
# Remove problematic gh-pages folder from slick-lightbox
RUN rm -rf /build/publichealth/static/libs/slick-lightbox/gh-pages
# Copy code required for runtime to the /app folder.
RUN mkdir /app
# Collect static data into /app/static.
RUN . /venv/bin/activate && \
SECRET_KEY=dummy \
STATIC_DIR=/app/static ./manage.py collectstatic --no-input
# Compress static files offline (required for COMPRESS_OFFLINE=True in production).
RUN . /venv/bin/activate && \
SECRET_KEY=dummy \
STATIC_DIR=/app/static \
./manage.py compress --force
# Compile messages if locale directory exists
RUN if [ -d "locale" ]; then \
. /venv/bin/activate && \
SECRET_KEY=dummy \
./manage.py compilemessages; \
fi
# Copy application code to /app
RUN cp -r publichealth feedler manage.py /app/
RUN if [ -d "locale" ]; then cp -r locale /app/; fi
RUN ls -al /app/
### runtime image
FROM base AS runtime
COPY --from=backend-builder /venv /venv
COPY --from=backend-builder /app /app
RUN groupadd --gid 1001 app \
&& useradd --home-dir /app --shell /bin/bash --gid app --uid 1001 app \
&& chown -R app:app /app
USER app
WORKDIR /app
# Ensure exectutables from virtualenv are prefered.
ENV PATH="/venv/bin:$PATH"
ADD entrypoint /entrypoint
ENTRYPOINT ["/entrypoint"]

View file

@ -10,10 +10,10 @@ tox = "*"
django-debug-toolbar = "*"
[packages]
"psycopg2-binary" = "*"
"psycopg2-binary" = "<2.9"
ConcurrentLogHandler = "*"
dj-database-url = "*"
django = "<4"
django = "<2.3"
django-anymail = "*"
django-colorful = "*"
django-crispy-forms = "*"
@ -30,7 +30,6 @@ Pillow = ">=4.0.0"
puput = ">=1.0.0"
python-dotenv = "*"
stellar = "*"
wagtail = ">=2.0,<2.16"
wagtail = ">=2.0,<2.13"
whitenoise = "*"
django-toolbelt = "*"
unidecode = "*"

760
Pipfile.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,138 +0,0 @@
#!/bin/bash
# Deployment script for publichealth on Minikube
# Usage: ./deploy-minikube.sh
set -e
echo "🚀 Starting deployment to Minikube..."
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
IMAGE_NAME="publichealth"
IMAGE_TAG="latest"
NAMESPACE="ph"
# Function to check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check prerequisites
echo "📋 Checking prerequisites..."
if ! command_exists minikube; then
echo -e "${RED}❌ Minikube is not installed${NC}"
echo "Install it from: https://minikube.sigs.k8s.io/docs/start/"
exit 1
fi
if ! command_exists kubectl; then
echo -e "${RED}❌ kubectl is not installed${NC}"
exit 1
fi
if ! command_exists docker; then
echo -e "${RED}❌ Docker is not installed${NC}"
exit 1
fi
# Start Minikube if not running
if ! minikube status | grep -q "Running"; then
echo "🔧 Starting Minikube..."
# Check if running as root and use appropriate driver
if [ "$EUID" -eq 0 ]; then
echo "Running as root, using none driver..."
minikube start --driver=none --cpus=2 --memory=4096
else
minikube start --driver=docker --cpus=2 --memory=4096
fi
else
echo -e "${GREEN}✓ Minikube is running${NC}"
fi
# Configure Docker to use Minikube's Docker daemon
echo "🔧 Configuring Docker to use Minikube's daemon..."
eval $(minikube docker-env)
# Build the Docker image
echo "🏗️ Building Docker image..."
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
echo -e "${GREEN}✓ Docker image built successfully${NC}"
# Create namespace if it doesn't exist
echo "📁 Creating namespace '${NAMESPACE}'..."
kubectl create namespace ${NAMESPACE} 2>/dev/null && echo -e "${GREEN}✓ Namespace '${NAMESPACE}' created${NC}" || echo -e "${GREEN}✓ Namespace '${NAMESPACE}' already exists${NC}"
# Apply storage configurations
echo "💾 Setting up storage in namespace '${NAMESPACE}'..."
kubectl apply -f k8s/storage.yaml -n ${NAMESPACE}
# Create secrets if they don't exist
if ! kubectl get secret publichealth-secrets -n ${NAMESPACE} >/dev/null 2>&1; then
echo -e "${YELLOW}⚠️ Creating default secrets in namespace '${NAMESPACE}' (CHANGE THESE IN PRODUCTION!)${NC}"
kubectl create secret generic publichealth-secrets -n ${NAMESPACE} \
--from-literal=django-secret-key='change-me-$(openssl rand -hex 32)' \
--from-literal=database-url='postgres://publichealth:changeme@postgres-service:5432/publichealth' \
--from-literal=postgres-password='changeme'
else
echo -e "${GREEN}✓ Secrets already exist in namespace '${NAMESPACE}'${NC}"
fi
# Deploy PostgreSQL
echo "🐘 Deploying PostgreSQL to namespace '${NAMESPACE}'..."
kubectl apply -f k8s/postgres.yaml -n ${NAMESPACE}
# Deploy Redis
echo "🔴 Deploying Redis to namespace '${NAMESPACE}'..."
kubectl apply -f k8s/redis.yaml -n ${NAMESPACE}
# Wait for database to be ready
echo "⏳ Waiting for PostgreSQL to be ready in namespace '${NAMESPACE}'..."
kubectl wait --for=condition=ready pod -l app=postgres --timeout=120s -n ${NAMESPACE}
# Deploy the application
echo "🚀 Deploying application to namespace '${NAMESPACE}'..."
kubectl apply -f k8s/deployment.yaml -n ${NAMESPACE}
# Wait for deployment to be ready
echo "⏳ Waiting for application to be ready in namespace '${NAMESPACE}'..."
kubectl wait --for=condition=available --timeout=300s deployment/publichealth -n ${NAMESPACE}
# Run migrations
echo "🔄 Running database migrations in namespace '${NAMESPACE}'..."
POD=$(kubectl get pod -l app=publichealth -n ${NAMESPACE} -o jsonpath="{.items[0].metadata.name}")
kubectl exec -it ${POD} -n ${NAMESPACE} -- python manage.py migrate
# Create superuser (optional)
echo -e "${YELLOW}Do you want to create a superuser? (y/n)${NC}"
read -r response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
kubectl exec -it ${POD} -n ${NAMESPACE} -- python manage.py createsuperuser
fi
# Get the service URL
MINIKUBE_IP=$(minikube ip)
NODE_PORT=$(kubectl get svc publichealth-service -n ${NAMESPACE} -o jsonpath='{.spec.ports[0].nodePort}')
echo ""
echo "✅ Deployment complete in namespace '${NAMESPACE}'!"
echo ""
echo "📌 Application URL: http://${MINIKUBE_IP}:${NODE_PORT}"
echo "📌 Admin URL: http://${MINIKUBE_IP}:${NODE_PORT}/admin/"
echo ""
echo "🔍 Useful commands (namespace: ${NAMESPACE}):"
echo " - View logs: kubectl logs -f deployment/publichealth -n ${NAMESPACE}"
echo " - Scale deployment: kubectl scale deployment publichealth --replicas=3 -n ${NAMESPACE}"
echo " - Access pod shell: kubectl exec -it ${POD} -n ${NAMESPACE} -- /bin/bash"
echo " - Port forward: kubectl port-forward service/publichealth-service 8000:8000 -n ${NAMESPACE}"
echo " - List resources: kubectl get all -n ${NAMESPACE}"
echo " - Delete namespace: kubectl delete namespace ${NAMESPACE}"
echo " - Stop Minikube: minikube stop"
echo ""
echo "🌐 To access from your browser, you can also run:"
echo " minikube service publichealth-service -n ${NAMESPACE}"

View file

@ -1,16 +0,0 @@
#!/bin/sh
set -e
if [ "${COLLECT_STATIC-}" ]; then
./manage.py collectstatic --no-input
fi
exec /venv/bin/dumb-init -- gunicorn \
-b [::]:8000 \
--log-file=- \
--worker-tmp-dir /dev/shm \
--workers=2 \
--threads=4 \
--worker-class=gthread \
publichealth.wsgi

View file

@ -6,9 +6,9 @@ __version__ = '0.1'
FEEDLER_APPS = (
# Wagtail apps
'wagtail',
'wagtail.core',
'wagtail.admin',
'wagtail_modeladmin', # Separate package for modeladmin in Wagtail 5+
'wagtail.contrib.modeladmin',
'wagtail.contrib.routable_page',
'wagtail.api.v2',

View file

@ -3,18 +3,17 @@
from django.db import models
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.urls import re_path # django.conf.urls.url removed in Django 4.0
from django.conf.urls import url
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext as _ # ugettext deprecated in Django 4.0
from django.utils.translation import ugettext as _
from django.shortcuts import redirect
from wagtail.admin import messages
# Wagtail 6 moved modeladmin to separate package
from wagtail_modeladmin.helpers import AdminURLHelper, ButtonHelper
from wagtail_modeladmin.options import ModelAdmin
from wagtail_modeladmin.views import IndexView
from wagtail.models import Site # wagtail.core.models moved to wagtail.models in 3.0
from wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper
from wagtail.contrib.modeladmin.options import ModelAdmin
from wagtail.contrib.modeladmin.views import IndexView
from wagtail.core.models import Site
from feedler.models import Entry
from feedler.refresh import refresh_streams
@ -41,10 +40,7 @@ class RefreshAdminURLHelper(AdminURLHelper):
This helper constructs the different urls, to overwrite the default behaviour
and append the filters to the action.
"""
def __init__(self, model, **kwargs):
super().__init__(model, **kwargs)
self.non_object_specific_actions = ('create', 'choose_parent', 'index', 'refresh')
non_object_specific_actions = ('create', 'choose_parent', 'index', 'refresh')
def get_action_url(self, action, *args, **kwargs):
query_params = kwargs.pop('query_params', None)
url_name = self.get_action_url_name(action)
@ -90,7 +86,7 @@ class EntryModelAdminMixin(object):
def get_admin_urls_for_registration(self):
urls = super().get_admin_urls_for_registration()
urls += (
re_path(
url(
self.url_helper.get_action_url_pattern('refresh'),
self.refresh_view,
name=self.url_helper.get_action_url_name('refresh')

View file

@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from django.test import Client
from wagtail.models import Page
from wagtail.core.models import Page
from wagtail.admin.views.pages import edit
# Scans for broken editors, via

View file

@ -4,7 +4,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import wagtail.fields
import wagtail.core.fields
class Migration(migrations.Migration):
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
name='FeedPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
('intro', wagtail.fields.RichTextField(blank=True, default='')),
('intro', wagtail.core.fields.RichTextField(blank=True, default='')),
('stream', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='feedler.Stream', verbose_name='Filter to stream (optional)')),
],
options={

View file

@ -1,18 +0,0 @@
# Generated by Django 3.2.13 on 2022-05-03 10:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('feedler', '0008_auto_20171117_1647'),
]
operations = [
migrations.AddField(
model_name='entry',
name='expire_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='expiry date/time'),
),
]

View file

@ -7,9 +7,7 @@ from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.core.mail import send_mail
# In Wagtail 3+, BaseSetting moved to wagtail.contrib.settings.models
from wagtail.contrib.settings.models import BaseSiteSetting as BaseSetting
from wagtail.contrib.settings.registry import register_setting
from wagtail.contrib.settings.models import BaseSetting, register_setting
from .models import Stream

View file

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
import datetime
from django.db import models
from django.utils import translation
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils.translation import gettext_lazy as _
from wagtail.models import Page, Orderable # wagtail.core moved to wagtail in v3+
from wagtail.admin.panels import FieldPanel # edit_handlers renamed to panels in Wagtail 3+
from wagtail.fields import RichTextField # wagtail.core.fields moved to wagtail.fields
from wagtail.core.models import Page, Orderable
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.core.fields import RichTextField
class Stream(models.Model):
title = models.CharField(max_length=255)
@ -40,11 +39,6 @@ class Entry(models.Model):
lang = models.CharField(max_length=2, blank=True, default='', choices=LANGUAGE_CHOICES)
content = models.TextField()
tags = models.TextField(blank=True)
expire_at = models.DateTimeField(
verbose_name=_("expiry date/time"),
blank=True,
null=True
)
stream = models.ForeignKey(Stream,
blank=False, on_delete=models.CASCADE,
@ -74,9 +68,7 @@ class FeedPage(Page):
@property
def feedentries(self):
if self.stream:
entries = Entry.objects.filter(stream=self.stream).filter(
models.Q(expire_at__isnull=True) | models.Q(expire_at__gt=datetime.datetime.now())
)
entries = Entry.objects.filter(stream=self.stream)
else:
entries = Entry.objects.all()
# Filter out by chosen language

View file

@ -1,4 +1,4 @@
from django.urls import include, re_path
from django.conf.urls import include, url
from django.conf import settings
from django.contrib import admin
from django.conf.urls.i18n import i18n_patterns
@ -6,5 +6,5 @@ from django.conf.urls.i18n import i18n_patterns
from .api import api_router
urlpatterns = [
re_path(r'^api/v2/', api_router.urls),
url(r'^api/v2/', api_router.urls),
]

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from wagtail_modeladmin.options import (
from wagtail.contrib.modeladmin.options import (
ModelAdmin, modeladmin_register)
from .admin import EntryModelAdminMixin

View file

@ -1,34 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@django-hosting.ch # Change this to your email
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@django-hosting.ch # Change this to your email
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx

View file

@ -1,93 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: publichealth
labels:
app: publichealth
spec:
replicas: 2
selector:
matchLabels:
app: publichealth
template:
metadata:
labels:
app: publichealth
spec:
containers:
- name: publichealth
image: harbor.k8s.ungleich.ch/publichealth/publichealth:3.0.1
imagePullPolicy: IfNotPresent # For local Minikube images
ports:
- containerPort: 8000
env:
- name: DJANGO_SETTINGS_MODULE
value: "publichealth.settings.production"
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: publichealth-secrets
key: django-secret-key
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: publichealth-secrets
key: database-url
- name: REDIS_URL
value: "redis://redis-service:6379/0"
- name: WAGTAILADMIN_BASE_URL
value: "http://localhost:30080"
- name: LOG_DIR
value: "/tmp"
- name: ALLOWED_HOSTS
valueFrom:
secretKeyRef:
name: publichealth-secrets
key: allowed-hosts
- name: COLLECT_STATIC
value: "1"
volumeMounts:
- name: media-storage
mountPath: /app/media
- name: static-storage
mountPath: /app/static
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /en/
port: 8000
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /en/
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: media-storage
persistentVolumeClaim:
claimName: media-pvc
- name: static-storage
persistentVolumeClaim:
claimName: static-pvc
---
apiVersion: v1
kind: Service
metadata:
name: publichealth-service
spec:
selector:
app: publichealth
ports:
- protocol: TCP
port: 8000
targetPort: 8000
nodePort: 30080
type: NodePort

View file

@ -1,38 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: ingress-nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: publichealth-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
rules:
- host: ph.django-hosting.ch
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: publichealth-service
port:
number: 8000
- host: www.ph.django-hosting.ch
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: publichealth-service
port:
number: 8000

View file

@ -1,67 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: publichealth
- name: POSTGRES_USER
value: publichealth
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: publichealth-secrets
key: postgres-password
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres-storage
subPath: postgres
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "200m"
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: postgres-service
spec:
selector:
app: postgres
ports:
- protocol: TCP
port: 5432
targetPort: 5432

View file

@ -1,38 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
name: redis-service
spec:
selector:
app: redis
ports:
- protocol: TCP
port: 6379
targetPort: 6379

View file

@ -1,15 +0,0 @@
# Example secrets file - DO NOT commit actual secrets to git!
# Create real secrets with: kubectl create secret generic publichealth-secrets \
# --from-literal=django-secret-key='your-secret-key' \
# --from-literal=database-url='postgres://user:pass@postgres-service:5432/dbname' \
# --from-literal=postgres-password='your-postgres-password'
apiVersion: v1
kind: Secret
metadata:
name: publichealth-secrets
type: Opaque
stringData:
django-secret-key: "change-me-to-a-real-secret-key"
database-url: "postgres://publichealth:changeme@postgres-service:5432/publichealth"
postgres-password: "changeme"

View file

@ -1,21 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: media-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: static-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 2Gi

7839
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,7 @@
"grunt-contrib-sass": "^1.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-sass": "^3.0.2",
"sass": "^1.45.0"
"node-sass": "^4.12.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"

View file

@ -1,8 +0,0 @@
from django.apps import AppConfig
class HomeConfig(AppConfig):
name = "publichealth.home"
def ready(self):
from . import signals # noqa: F401

View file

@ -1,8 +0,0 @@
from django import template
from html import unescape
register = template.Library()
@register.filter
def ph_html_decode(value):
return unescape(str(value))

View file

@ -1,214 +0,0 @@
"""
Find and repair Wagtail rich-text content with unbalanced inline-style or
entity tags that crashes the Draftail editor's HTML->ContentState converter
with `AssertionError: End of block reached without closing inline style /
entity elements`.
The Draftail converter is strict about tag balance; older Wagtail editors
or Word/Outlook paste sometimes left tags unclosed. The fix re-serializes
through BeautifulSoup (Python's stdlib html.parser) which auto-closes
inline tags, then verifies the output by feeding it back through the same
converter — only saves if the result is accepted.
Defaults to dry-run. Pass --apply to write changes; add --publish to
publish the new revision (otherwise it's saved as a draft for editor
review). --page-id limits to a single page.
"""
from django.core.management.base import BaseCommand
from wagtail.models import Page
from wagtail.fields import RichTextField, StreamField
from wagtail.blocks import RichTextBlock
from publichealth.home.rich_text_sanitizer import (
get_converter, is_broken, repair as _repair,
)
def _build_converter():
converter = get_converter()
# Best-effort: list the features the converter was built with for the
# mode banner. Falls back gracefully if the converter doesn't expose them.
features = sorted(getattr(converter, "features", []))
return converter, features
def _is_broken(converter, html):
return is_broken(html)
class Command(BaseCommand):
help = "Find and fix Wagtail rich-text fields with unbalanced tags."
def add_arguments(self, parser):
parser.add_argument("--apply", action="store_true",
help="Save changes (default: dry-run, report only).")
parser.add_argument("--publish", action="store_true",
help="Publish the cleaned revision (otherwise save as draft).")
parser.add_argument("--page-id", type=int, default=None,
help="Limit to a single page id.")
parser.add_argument("--diff-file", type=str, default=None,
help="Write full before/after HTML and a unified diff per block to this path.")
def handle(self, *args, **options):
import difflib
apply_changes = options["apply"]
publish = options["publish"]
page_id = options["page_id"]
diff_path = options["diff_file"]
diff_fp = open(diff_path, "w", encoding="utf-8") if diff_path else None
def emit_diff(page, field_name, sub_path, old, new):
if diff_fp is None:
return
diff_fp.write("=" * 78 + "\n")
diff_fp.write(f"Page {page.id}: {page.title}\n")
diff_fp.write(f" url_path: {page.url_path}\n")
diff_fp.write(f" field: {field_name}{sub_path}\n")
diff_fp.write("-" * 78 + "\n")
diff_fp.write("OLD:\n")
diff_fp.write(old + "\n")
diff_fp.write("-" * 78 + "\n")
diff_fp.write("NEW:\n")
diff_fp.write(new + "\n")
diff_fp.write("-" * 78 + "\n")
diff_fp.write("UNIFIED DIFF:\n")
old_lines = old.splitlines(keepends=True) or [""]
new_lines = new.splitlines(keepends=True) or [""]
for line in difflib.unified_diff(old_lines, new_lines,
fromfile="old", tofile="new", n=1):
diff_fp.write(line if line.endswith("\n") else line + "\n")
diff_fp.write("\n")
converter, features = _build_converter()
self.stdout.write(f"Detector using {len(features)} features.")
self.stdout.write(f"Mode: {'APPLY' + (' + PUBLISH' if publish else ' (draft)') if apply_changes else 'DRY RUN'}")
if diff_fp:
self.stdout.write(f"Writing full diffs to: {diff_path}")
qs = Page.objects.all()
if page_id is not None:
qs = qs.filter(id=page_id)
total = qs.count()
self.stdout.write(f"Scanning {total} page(s)...")
pages_with_issues = 0
pages_fixed = 0
pages_partial = 0
blocks_fixed = 0
blocks_unfixable = 0
for page in qs.iterator(chunk_size=50):
specific = page.specific
changes = [] # successful: (field, sub_path, old, new)
unfixable = [] # broken but repair didn't help: (field, sub_path)
stream_replacements = {} # field_name -> {index: new_html}
rich_text_field_updates = {} # field_name -> new_html
for field in specific._meta.get_fields():
if isinstance(field, RichTextField):
html = getattr(specific, field.name, "") or ""
if not _is_broken(converter, html):
continue
fixed = _repair(html)
if _is_broken(converter, fixed):
unfixable.append((field.name, ""))
continue
changes.append((field.name, "", html, fixed))
rich_text_field_updates[field.name] = fixed
elif isinstance(field, StreamField):
stream_value = getattr(specific, field.name, None)
if stream_value is None:
continue
try:
children = list(stream_value)
except Exception:
continue
field_replacements = {}
for i, child in enumerate(children):
if not isinstance(child.block, RichTextBlock):
continue
v = child.value
html = v.source if hasattr(v, "source") else str(v)
if not _is_broken(converter, html):
continue
fixed = _repair(html)
if _is_broken(converter, fixed):
unfixable.append((field.name, f"[{i}]"))
continue
changes.append((field.name, f"[{i}]({child.block_type})", html, fixed))
field_replacements[i] = fixed
if field_replacements:
stream_replacements[field.name] = (children, field_replacements)
if not changes and not unfixable:
continue
pages_with_issues += 1
if changes and not unfixable:
pages_fixed += 1
elif changes and unfixable:
pages_partial += 1
blocks_fixed += len(changes)
blocks_unfixable += len(unfixable)
self.stdout.write("")
self.stdout.write(f"Page {page.id}: {page.title!r} [{specific.__class__.__name__}]")
self.stdout.write(f" url_path: {page.url_path}")
for field_name, sub_path, old, new in changes:
self.stdout.write(f" FIX {field_name}{sub_path}")
self.stdout.write(f" old: {old[:160]!r}{'...' if len(old) > 160 else ''}")
self.stdout.write(f" new: {new[:160]!r}{'...' if len(new) > 160 else ''}")
emit_diff(page, field_name, sub_path, old, new)
for field_name, sub_path in unfixable:
self.stdout.write(f" SKIP {field_name}{sub_path} (auto-repair did not produce valid output)")
if not apply_changes:
continue
# Apply
for field_name, new_value in rich_text_field_updates.items():
setattr(specific, field_name, new_value)
for field_name, (children, replacements) in stream_replacements.items():
# Rebuild the StreamField as a list of raw {type, value} dicts.
# Using `get_prep_value` for unchanged children gives the
# JSON-serializable raw form Wagtail expects on assign;
# replacements are already raw HTML strings (correct form for
# RichTextBlock).
new_list = []
for i, child in enumerate(children):
if i in replacements:
new_list.append({"type": child.block_type, "value": replacements[i]})
else:
raw = child.block.get_prep_value(child.value)
new_list.append({"type": child.block_type, "value": raw})
setattr(specific, field_name, new_list)
try:
revision = specific.save_revision()
if publish:
revision.publish()
self.stdout.write(self.style.SUCCESS(
f" -> revision {revision.id} saved{' & published' if publish else ' (draft)'}"
))
except Exception as e:
self.stdout.write(self.style.ERROR(f" -> SAVE FAILED: {type(e).__name__}: {e}"))
self.stdout.write("")
self.stdout.write("=" * 70)
self.stdout.write(f"Pages with issues: {pages_with_issues}")
self.stdout.write(f"Pages fully fixable: {pages_fixed}")
self.stdout.write(f"Pages partial: {pages_partial} (some blocks unfixable)")
self.stdout.write(f"Blocks fixed: {blocks_fixed}")
self.stdout.write(f"Blocks unfixable: {blocks_unfixable}")
if not apply_changes:
self.stdout.write("")
self.stdout.write("DRY RUN — no changes saved. Re-run with --apply to write.")
if diff_fp:
diff_fp.close()
self.stdout.write(f"Diff log: {diff_path}")

View file

@ -5,8 +5,8 @@ from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
import wagtail.blocks
import wagtail.fields
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.images.blocks
@ -36,10 +36,10 @@ class Migration(migrations.Migration):
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
('title_fr', models.CharField(default='', max_length=255)),
('date', models.DateField(verbose_name='Date')),
('intro_de', wagtail.fields.RichTextField(default='')),
('intro_fr', wagtail.fields.RichTextField(default='')),
('body_de', wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock()), ('section', wagtail.blocks.CharBlock(classname='full title'))], blank=True, null=True)),
('body_fr', wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock()), ('section', wagtail.blocks.CharBlock(classname='full title'))], blank=True, null=True)),
('intro_de', wagtail.core.fields.RichTextField(default='')),
('intro_fr', wagtail.core.fields.RichTextField(default='')),
('body_de', wagtail.core.fields.StreamField([('paragraph', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock()), ('section', wagtail.core.blocks.CharBlock(classname='full title'))], blank=True, null=True)),
('body_fr', wagtail.core.fields.StreamField([('paragraph', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock()), ('section', wagtail.core.blocks.CharBlock(classname='full title'))], blank=True, null=True)),
('feed_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.Image')),
],
options={
@ -64,31 +64,31 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='homepage',
name='body_de',
field=wagtail.fields.RichTextField(default=''),
field=wagtail.core.fields.RichTextField(default=''),
),
migrations.AddField(
model_name='homepage',
name='body_fr',
field=wagtail.fields.RichTextField(default=''),
field=wagtail.core.fields.RichTextField(default=''),
),
migrations.AddField(
model_name='homepage',
name='infos_de',
field=wagtail.fields.StreamField([('info', wagtail.blocks.StructBlock([(b'title', wagtail.blocks.CharBlock(required=True)), (b'photo', wagtail.images.blocks.ImageChooserBlock()), (b'summary', wagtail.blocks.RichTextBlock(required=True)), (b'action', wagtail.blocks.CharBlock()), (b'url', wagtail.blocks.URLBlock())]))], blank=True, null=True),
field=wagtail.core.fields.StreamField([('info', wagtail.core.blocks.StructBlock([(b'title', wagtail.core.blocks.CharBlock(required=True)), (b'photo', wagtail.images.blocks.ImageChooserBlock()), (b'summary', wagtail.core.blocks.RichTextBlock(required=True)), (b'action', wagtail.core.blocks.CharBlock()), (b'url', wagtail.core.blocks.URLBlock())]))], blank=True, null=True),
),
migrations.AddField(
model_name='homepage',
name='infos_fr',
field=wagtail.fields.StreamField([('info', wagtail.blocks.StructBlock([(b'title', wagtail.blocks.CharBlock(required=True)), (b'photo', wagtail.images.blocks.ImageChooserBlock()), (b'summary', wagtail.blocks.RichTextBlock(required=True)), (b'action', wagtail.blocks.CharBlock()), (b'url', wagtail.blocks.URLBlock())]))], blank=True, null=True),
field=wagtail.core.fields.StreamField([('info', wagtail.core.blocks.StructBlock([(b'title', wagtail.core.blocks.CharBlock(required=True)), (b'photo', wagtail.images.blocks.ImageChooserBlock()), (b'summary', wagtail.core.blocks.RichTextBlock(required=True)), (b'action', wagtail.core.blocks.CharBlock()), (b'url', wagtail.core.blocks.URLBlock())]))], blank=True, null=True),
),
migrations.AddField(
model_name='homepage',
name='intro_de',
field=wagtail.fields.RichTextField(default=''),
field=wagtail.core.fields.RichTextField(default=''),
),
migrations.AddField(
model_name='homepage',
name='intro_fr',
field=wagtail.fields.RichTextField(default=''),
field=wagtail.core.fields.RichTextField(default=''),
),
]

View file

@ -3,7 +3,7 @@
from __future__ import unicode_literals
from django.db import migrations
import wagtail.fields
import wagtail.core.fields
class Migration(migrations.Migration):
@ -20,11 +20,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='articleindexpage',
name='intro_de',
field=wagtail.fields.RichTextField(default=''),
field=wagtail.core.fields.RichTextField(default=''),
),
migrations.AddField(
model_name='articleindexpage',
name='intro_fr',
field=wagtail.fields.RichTextField(default=''),
field=wagtail.core.fields.RichTextField(default=''),
),
]

View file

@ -3,7 +3,7 @@
from __future__ import unicode_literals
from django.db import migrations
import wagtail.fields
import wagtail.core.fields
class Migration(migrations.Migration):
@ -16,21 +16,21 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='articleindexpage',
name='intro_de',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='articleindexpage',
name='intro_fr',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='articlepage',
name='intro_de',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='articlepage',
name='intro_fr',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
]

View file

@ -3,7 +3,7 @@
from __future__ import unicode_literals
from django.db import migrations
import wagtail.fields
import wagtail.core.fields
class Migration(migrations.Migration):
@ -16,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='homepage',
name='body_de',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='homepage',
name='body_fr',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
]

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
import wagtail.fields
import wagtail.core.fields
class Migration(migrations.Migration):
@ -35,10 +35,10 @@ class Migration(migrations.Migration):
('from_address', models.CharField(blank=True, max_length=255, verbose_name='from address')),
('subject', models.CharField(blank=True, max_length=255, verbose_name='subject')),
('title_fr', models.CharField(default=b'', max_length=255)),
('intro_de', wagtail.fields.RichTextField(blank=True, default=b'')),
('intro_fr', wagtail.fields.RichTextField(blank=True, default=b'')),
('thanks_de', wagtail.fields.RichTextField(blank=True, default=b'')),
('thanks_fr', wagtail.fields.RichTextField(blank=True, default=b'')),
('intro_de', wagtail.core.fields.RichTextField(blank=True, default=b'')),
('intro_fr', wagtail.core.fields.RichTextField(blank=True, default=b'')),
('thanks_de', wagtail.core.fields.RichTextField(blank=True, default=b'')),
('thanks_fr', wagtail.core.fields.RichTextField(blank=True, default=b'')),
],
options={
'verbose_name': 'Formular',

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations, models
import wagtail.blocks
import wagtail.fields
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.images.blocks
@ -23,22 +23,22 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='contactform',
name='intro_de',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='contactform',
name='intro_fr',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='contactform',
name='thanks_de',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='contactform',
name='thanks_fr',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='contactform',
@ -48,11 +48,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='homepage',
name='infos_de',
field=wagtail.fields.StreamField((('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock()), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock()), ('url', wagtail.blocks.URLBlock())))),), blank=True, null=True),
field=wagtail.core.fields.StreamField((('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock()), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock()), ('url', wagtail.core.blocks.URLBlock())))),), blank=True, null=True),
),
migrations.AlterField(
model_name='homepage',
name='infos_fr',
field=wagtail.fields.StreamField((('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock()), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock()), ('url', wagtail.blocks.URLBlock())))),), blank=True, null=True),
field=wagtail.core.fields.StreamField((('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock()), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock()), ('url', wagtail.core.blocks.URLBlock())))),), blank=True, null=True),
),
]

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations
import wagtail.blocks
import wagtail.fields
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.images.blocks
@ -18,21 +18,21 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='articlepage',
name='body_de',
field=wagtail.fields.StreamField((('paragraph', wagtail.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock()), ('section', wagtail.blocks.CharBlock(classname='full title')), ('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False)))))), blank=True, null=True),
field=wagtail.core.fields.StreamField((('paragraph', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock()), ('section', wagtail.core.blocks.CharBlock(classname='full title')), ('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False)))))), blank=True, null=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_fr',
field=wagtail.fields.StreamField((('paragraph', wagtail.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock()), ('section', wagtail.blocks.CharBlock(classname='full title')), ('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False)))))), blank=True, null=True),
field=wagtail.core.fields.StreamField((('paragraph', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock()), ('section', wagtail.core.blocks.CharBlock(classname='full title')), ('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False)))))), blank=True, null=True),
),
migrations.AlterField(
model_name='homepage',
name='infos_de',
field=wagtail.fields.StreamField((('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))))),), blank=True, null=True),
field=wagtail.core.fields.StreamField((('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False))))),), blank=True, null=True),
),
migrations.AlterField(
model_name='homepage',
name='infos_fr',
field=wagtail.fields.StreamField((('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))))),), blank=True, null=True),
field=wagtail.core.fields.StreamField((('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False))))),), blank=True, null=True),
),
]

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations, models
import wagtail.blocks
import wagtail.fields
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.images.blocks
@ -22,17 +22,17 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='articlepage',
name='gallery',
field=wagtail.fields.StreamField((('image', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock((('image', wagtail.images.blocks.ImageChooserBlock()), ('caption', wagtail.blocks.TextBlock(required=False)))), icon='image')),), blank=True),
field=wagtail.core.fields.StreamField((('image', wagtail.core.blocks.ListBlock(wagtail.core.blocks.StructBlock((('image', wagtail.images.blocks.ImageChooserBlock()), ('caption', wagtail.core.blocks.TextBlock(required=False)))), icon='image')),), blank=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_de',
field=wagtail.fields.StreamField((('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(classname='full title')), ('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))))), ('placer', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='user'))), blank=True, null=True),
field=wagtail.core.fields.StreamField((('paragraph', wagtail.core.blocks.RichTextBlock()), ('section', wagtail.core.blocks.CharBlock(classname='full title')), ('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False))))), ('placer', wagtail.core.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='user'))), blank=True, null=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_fr',
field=wagtail.fields.StreamField((('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(classname='full title')), ('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))))), ('placer', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='user'))), blank=True, null=True),
field=wagtail.core.fields.StreamField((('paragraph', wagtail.core.blocks.RichTextBlock()), ('section', wagtail.core.blocks.CharBlock(classname='full title')), ('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False))))), ('placer', wagtail.core.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='user'))), blank=True, null=True),
),
migrations.AlterField(
model_name='contactformfield',

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations
import wagtail.blocks
import wagtail.fields
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.images.blocks
@ -18,11 +18,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='articlepage',
name='body_de',
field=wagtail.fields.StreamField((('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(classname='full title')), ('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))), icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media'))), blank=True, null=True),
field=wagtail.core.fields.StreamField((('paragraph', wagtail.core.blocks.RichTextBlock()), ('section', wagtail.core.blocks.CharBlock(classname='full title')), ('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False))), icon='help')), ('media', wagtail.core.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media'))), blank=True, null=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_fr',
field=wagtail.fields.StreamField((('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(classname='full title')), ('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))), icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media'))), blank=True, null=True),
field=wagtail.core.fields.StreamField((('paragraph', wagtail.core.blocks.RichTextBlock()), ('section', wagtail.core.blocks.CharBlock(classname='full title')), ('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False))), icon='help')), ('media', wagtail.core.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media'))), blank=True, null=True),
),
]

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations, models
import wagtail.blocks
import wagtail.fields
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.images.blocks
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='articleindexpage',
name='intro_en',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AddField(
model_name='articleindexpage',
@ -28,12 +28,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='articlepage',
name='body_en',
field=wagtail.fields.StreamField((('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(classname='full title')), ('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))), icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media'))), blank=True, null=True),
field=wagtail.core.fields.StreamField((('paragraph', wagtail.core.blocks.RichTextBlock()), ('section', wagtail.core.blocks.CharBlock(classname='full title')), ('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False))), icon='help')), ('media', wagtail.core.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media'))), blank=True, null=True),
),
migrations.AddField(
model_name='articlepage',
name='intro_en',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AddField(
model_name='articlepage',
@ -48,16 +48,16 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='homepage',
name='body_en',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
migrations.AddField(
model_name='homepage',
name='infos_en',
field=wagtail.fields.StreamField((('info', wagtail.blocks.StructBlock((('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))))),), blank=True, null=True),
field=wagtail.core.fields.StreamField((('info', wagtail.core.blocks.StructBlock((('title', wagtail.core.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.core.blocks.RichTextBlock(required=True)), ('action', wagtail.core.blocks.CharBlock(required=False)), ('url', wagtail.core.blocks.URLBlock(required=False))))),), blank=True, null=True),
),
migrations.AddField(
model_name='homepage',
name='intro_en',
field=wagtail.fields.RichTextField(blank=True, default=''),
field=wagtail.core.fields.RichTextField(blank=True, default=''),
),
]

View file

@ -1,29 +0,0 @@
# Generated by Django 3.0.14 on 2021-09-28 16:50
from django.db import migrations
import wagtail.fields
class Migration(migrations.Migration):
dependencies = [
('home', '0029_contactformfield_clean_name'),
]
operations = [
migrations.AddField(
model_name='articleindexpage',
name='subscribe_label_de',
field=wagtail.fields.RichTextField(blank=True, default=''),
),
migrations.AddField(
model_name='articleindexpage',
name='subscribe_label_en',
field=wagtail.fields.RichTextField(blank=True, default=''),
),
migrations.AddField(
model_name='articleindexpage',
name='subscribe_label_fr',
field=wagtail.fields.RichTextField(blank=True, default=''),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 3.0.14 on 2021-09-28 16:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0030_auto_20210928_1850'),
]
operations = [
migrations.AddField(
model_name='articleindexpage',
name='subscribe_action',
field=models.URLField(blank=True, default=''),
),
]

View file

@ -1,33 +0,0 @@
# Generated by Django 3.0.14 on 2021-09-28 16:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0031_articleindexpage_subscribe_action'),
]
operations = [
migrations.AlterField(
model_name='articleindexpage',
name='subscribe_action',
field=models.URLField(blank=True, default='', verbose_name='Action'),
),
migrations.AlterField(
model_name='articleindexpage',
name='subscribe_label_de',
field=models.CharField(blank=True, default='', max_length=250, verbose_name='Button Label (de)'),
),
migrations.AlterField(
model_name='articleindexpage',
name='subscribe_label_en',
field=models.CharField(blank=True, default='', max_length=250, verbose_name='Button Label (en)'),
),
migrations.AlterField(
model_name='articleindexpage',
name='subscribe_label_fr',
field=models.CharField(blank=True, default='', max_length=250, verbose_name='Button Label (fr)'),
),
]

View file

@ -1,33 +0,0 @@
# Generated by Django 3.2.11 on 2022-02-07 06:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0032_auto_20210928_1858'),
]
operations = [
migrations.AddField(
model_name='contact',
name='news_letter_text',
field=models.CharField(blank=True, default='', max_length=256),
),
migrations.AddField(
model_name='contact',
name='news_letter_text_en',
field=models.CharField(blank=True, default='', max_length=255),
),
migrations.AddField(
model_name='contact',
name='news_letter_text_fr',
field=models.CharField(blank=True, default='', max_length=255),
),
migrations.AddField(
model_name='contact',
name='news_letter_url',
field=models.URLField(blank=True, default=''),
),
]

View file

@ -1,37 +0,0 @@
# Generated by Django 3.2.13 on 2022-07-14 09:45
from django.db import migrations, models
import django.db.models.deletion
import wagtail.contrib.table_block.blocks
import wagtail.fields
class Migration(migrations.Migration):
dependencies = [
('wagtailimages', '0023_add_choose_permissions'),
('home', '0033_auto_20220207_0731'),
]
operations = [
migrations.AddField(
model_name='articleindexpage',
name='header_image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image'),
),
migrations.AddField(
model_name='articleindexpage',
name='table_de',
field=wagtail.fields.StreamField([('table_de', wagtail.contrib.table_block.blocks.TableBlock(template='home/program_table.html'))], blank=True, null=True),
),
migrations.AddField(
model_name='articleindexpage',
name='table_en',
field=wagtail.fields.StreamField([('table_en', wagtail.contrib.table_block.blocks.TableBlock(template='home/program_table.html'))], blank=True, null=True),
),
migrations.AddField(
model_name='articleindexpage',
name='table_fr',
field=wagtail.fields.StreamField([('table_fr', wagtail.contrib.table_block.blocks.TableBlock(template='home/program_table.html'))], blank=True, null=True),
),
]

View file

@ -1,32 +0,0 @@
# Generated by Django 3.2.13 on 2023-05-15 12:47
from django.db import migrations
import wagtail.blocks
import wagtail.fields
import wagtail.embeds.blocks
import wagtail.images.blocks
class Migration(migrations.Migration):
dependencies = [
('home', '0034_auto_20220714_1145'),
]
operations = [
migrations.AlterField(
model_name='articlepage',
name='body_de',
field=wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(form_classname='full title')), ('info', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))], icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media')), ('iframe', wagtail.embeds.blocks.EmbedBlock())], blank=True, null=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_en',
field=wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(form_classname='full title')), ('info', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))], icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media')), ('iframe', wagtail.embeds.blocks.EmbedBlock())], blank=True, null=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_fr',
field=wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(form_classname='full title')), ('info', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))], icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media')), ('iframe', wagtail.embeds.blocks.EmbedBlock())], blank=True, null=True),
),
]

View file

@ -1,36 +0,0 @@
# Generated by Django 3.2.13 on 2023-05-15 13:56
from django.db import migrations
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
class Migration(migrations.Migration):
dependencies = [
('home', '0035_auto_20230515_1447'),
]
operations = [
migrations.AddField(
model_name='articlepage',
name='html_content',
field=wagtail.fields.RichTextField(blank=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_de',
field=wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(form_classname='full title')), ('info', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))], icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media'))], blank=True, null=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_en',
field=wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(form_classname='full title')), ('info', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))], icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media'))], blank=True, null=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_fr',
field=wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(form_classname='full title')), ('info', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))], icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media'))], blank=True, null=True),
),
]

View file

@ -1,35 +0,0 @@
# Generated by Django 3.2.13 on 2023-05-16 10:41
from django.db import migrations
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
class Migration(migrations.Migration):
dependencies = [
('home', '0036_auto_20230515_1556'),
]
operations = [
migrations.RemoveField(
model_name='articlepage',
name='html_content',
),
migrations.AlterField(
model_name='articlepage',
name='body_de',
field=wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(form_classname='full title')), ('info', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))], icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media')), ('iframe', wagtail.blocks.RichTextBlock())], blank=True, null=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_en',
field=wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(form_classname='full title')), ('info', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))], icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media')), ('iframe', wagtail.blocks.RichTextBlock())], blank=True, null=True),
),
migrations.AlterField(
model_name='articlepage',
name='body_fr',
field=wagtail.fields.StreamField([('paragraph', wagtail.blocks.RichTextBlock()), ('section', wagtail.blocks.CharBlock(form_classname='full title')), ('info', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(required=True)), ('photo', wagtail.images.blocks.ImageChooserBlock(required=True)), ('summary', wagtail.blocks.RichTextBlock(required=True)), ('action', wagtail.blocks.CharBlock(required=False)), ('url', wagtail.blocks.URLBlock(required=False))], icon='help')), ('media', wagtail.blocks.ChoiceBlock(choices=[('gallery', 'Image gallery')], icon='media')), ('iframe', wagtail.blocks.RichTextBlock())], blank=True, null=True),
),
]

View file

@ -2,4 +2,3 @@ from .forms import *
from .models import *
from .snippets import *
from .admin import *
from .image_formats import *

View file

@ -5,13 +5,11 @@ from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.core.mail import send_mail
# In Wagtail 3+, BaseSetting moved and register_setting changed
from wagtail.contrib.settings.models import BaseSiteSetting as BaseSetting
from wagtail.contrib.settings.registry import register_setting
from wagtail.contrib.settings.models import BaseSetting, register_setting
# A simple feedback module built into the site admin
@register_setting
@register_setting(icon='', classnames='icon icon-help')
class DataletsSettings(BaseSetting):
feedback_question = models.TextField(verbose_name='Name',
help_text='Who should we reply to for questions?', blank=True)
@ -38,8 +36,8 @@ def handle_save_settings(sender, instance, *args, **kwargs):
str(instance.feedback_status),
instance.feedback_question,
instance.feedback_comment,
), "info@ungleich.ch",
[ "info@ungleich.ch" ]
), "wagtail@datalets.ch",
[ "support@datalets.ch" ]
)
instance.feedback_status = None
instance.feedback_question = ""

View file

@ -4,11 +4,11 @@ from modelcluster.fields import ParentalKey
from django.db.models import CharField
from wagtail.fields import RichTextField
from wagtail.core.fields import RichTextField
from wagtail.contrib.forms.models import (
AbstractEmailForm, AbstractFormField
)
from wagtail.admin.panels import (
from wagtail.admin.edit_handlers import (
FieldPanel, FieldRowPanel,
InlinePanel, MultiFieldPanel
)

View file

@ -1,17 +0,0 @@
# image_formats.py
from django.utils.html import format_html
from wagtail.images.formats import Format, register_image_format
class OriginalImageFormat(Format):
def image_to_html(self, image, alt_text, extra_attributes=None):
default_html = super().image_to_html(image, alt_text, extra_attributes)
return format_html("{}", default_html, alt_text)
register_image_format(
OriginalImageFormat('original_fullwidth', 'Original image', 'bodytext-image', 'original')
)

View file

@ -2,23 +2,21 @@
from __future__ import unicode_literals
import datetime
from django.db import models
from django.utils import translation
from django.conf import settings
from modelcluster.fields import ParentalKey
from wagtail.blocks import StructBlock, CharBlock, URLBlock, RichTextBlock, ListBlock, TextBlock, ChoiceBlock
from wagtail.models import Page, Orderable
from wagtail.fields import StreamField, RichTextField
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
from wagtail.core.blocks import StructBlock, CharBlock, URLBlock, RichTextBlock, ListBlock, TextBlock, ChoiceBlock
from wagtail.core.models import Page, Orderable
from wagtail.core.fields import StreamField, RichTextField
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel, InlinePanel, MultiFieldPanel
from wagtail.images.blocks import ImageChooserBlock
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index
from wagtail.contrib.table_block.blocks import TableBlock
# from puput.models import EntryPage, BlogPage # Commented: needs Wagtail 6 update
from puput.models import EntryPage, BlogPage
from feedler.models import Entry, Stream
from itertools import chain
@ -40,13 +38,6 @@ class ArticleIndexPage(Page):
'title_en',
)
header_image = models.ForeignKey(
'wagtailimages.Image',
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
intro_de = RichTextField(default='', blank=True)
intro_fr = RichTextField(default='', blank=True)
intro_en = RichTextField(default='', blank=True)
@ -56,45 +47,6 @@ class ArticleIndexPage(Page):
'intro_en',
)
table_en = StreamField(
[
('table_en', TableBlock(template='home/program_table.html'))
],
null=True,
blank=True,
)
table_de = StreamField(
[
('table_de', TableBlock(template='home/program_table.html'))
],
null=True,
blank=True,
)
table_fr = StreamField(
[
('table_fr', TableBlock(template='home/program_table.html'))
],
null=True,
blank=True,
)
trans_table = TranslatedField(
'table_de',
'table_fr',
'table_en',
)
subscribe_label_de = models.CharField("Button Label (de)", default='', blank=True, max_length=250)
subscribe_label_fr = models.CharField("Button Label (fr)", default='', blank=True, max_length=250)
subscribe_label_en = models.CharField("Button Label (en)", default='', blank=True, max_length=250)
subscribe_action = models.URLField("Action", default='', blank=True)
trans_subscribe_label = TranslatedField(
'subscribe_label_de',
'subscribe_label_fr',
'subscribe_label_en',
)
feed_image = models.ForeignKey(
'wagtailimages.Image',
null=True, blank=True,
@ -108,21 +60,7 @@ class ArticleIndexPage(Page):
FieldPanel('intro_fr'),
FieldPanel('title_en'),
FieldPanel('intro_en'),
FieldPanel('header_image'), # ImageChooserPanel no longer needed in Wagtail 3+
FieldPanel('table_en'),
FieldPanel('table_fr'),
FieldPanel('table_de'),
FieldPanel('feed_image'), # ImageChooserPanel no longer needed in Wagtail 3+
MultiFieldPanel(
[
FieldPanel('subscribe_label_de'),
FieldPanel('subscribe_label_fr'),
FieldPanel('subscribe_label_en'),
FieldPanel('subscribe_action')
],
heading="Newsletters",
classname="collapsible collapsed"
)
ImageChooserPanel('feed_image'),
]
def get_context(self, request):
@ -184,8 +122,7 @@ class ArticlePage(Page):
('info', InfoBlock(icon='help')),
('media', ChoiceBlock(choices=[
('gallery', 'Image gallery'),
], icon='media')),
('iframe', RichTextBlock())
], icon='media'))
], null=True, blank=True)
body_fr = StreamField([
('paragraph', RichTextBlock()),
@ -193,8 +130,7 @@ class ArticlePage(Page):
('info', InfoBlock(icon='help')),
('media', ChoiceBlock(choices=[
('gallery', 'Image gallery'),
], icon='media')),
('iframe', RichTextBlock())
], icon='media'))
], null=True, blank=True)
body_en = StreamField([
('paragraph', RichTextBlock()),
@ -202,8 +138,7 @@ class ArticlePage(Page):
('info', InfoBlock(icon='help')),
('media', ChoiceBlock(choices=[
('gallery', 'Image gallery'),
], icon='media')),
('iframe', RichTextBlock())
], icon='media'))
], null=True, blank=True)
trans_body = TranslatedField(
'body_de',
@ -240,23 +175,23 @@ class ArticlePage(Page):
FieldPanel('intro_de'),
], heading="Deutsch",
classname="collapsible collapsed"),
FieldPanel('body_de'), # StreamFieldPanel no longer needed in Wagtail 3+
StreamFieldPanel('body_de'),
MultiFieldPanel([
FieldPanel('title_fr'),
FieldPanel('intro_fr'),
], heading="Français",
classname="collapsible collapsed"),
FieldPanel('body_fr'), # StreamFieldPanel no longer needed in Wagtail 3+
StreamFieldPanel('body_fr'),
MultiFieldPanel([
FieldPanel('title_en'),
FieldPanel('intro_en'),
], heading="English",
classname="collapsible collapsed"),
FieldPanel('body_en'), # StreamFieldPanel no longer needed in Wagtail 3+
StreamFieldPanel('body_en'),
MultiFieldPanel([
FieldPanel('feed_image'), # ImageChooserPanel no longer needed in Wagtail 3+
ImageChooserPanel('feed_image'),
], heading="Images"),
FieldPanel('gallery'), # StreamFieldPanel no longer needed in Wagtail 3+
StreamFieldPanel('gallery'),
]
promote_panels = [
InlinePanel('related_links', label="Links"),
@ -322,19 +257,19 @@ class HomePage(Page):
MultiFieldPanel([
FieldPanel('intro_de', classname="full"),
FieldPanel('body_de', classname="full"),
FieldPanel('infos_de'), # StreamFieldPanel no longer needed in Wagtail 3+
StreamFieldPanel('infos_de'),
], heading="Deutsch",
classname="collapsible collapsed"),
MultiFieldPanel([
FieldPanel('intro_fr', classname="full"),
FieldPanel('body_fr', classname="full"),
FieldPanel('infos_fr'), # StreamFieldPanel no longer needed in Wagtail 3+
StreamFieldPanel('infos_fr'),
], heading="Français",
classname="collapsible collapsed"),
MultiFieldPanel([
FieldPanel('intro_en', classname="full"),
FieldPanel('body_en', classname="full"),
FieldPanel('infos_en'), # StreamFieldPanel no longer needed in Wagtail 3+
StreamFieldPanel('infos_en'),
], heading="English",
classname="collapsible collapsed"),
]
@ -352,23 +287,19 @@ class HomePage(Page):
@property
def blogentries(self):
# Get list of latest news
# FIXME: Temporarily disabled - puput needs update for Wagtail 6
return []
# curlang = translation.get_language()
# if not curlang in ['de', 'fr', 'en', 'it']: curlang = 'de' # Default language
# parent = BlogPage.objects.filter(slug='news-%s' % curlang)
# if not parent: return []
# posts = EntryPage.objects.live().descendant_of(parent[0])
# # Order by most recent date first
# posts = posts.order_by('-date')
# return posts[:settings.BLOG_ENTRIES_HOME_PAGE]
curlang = translation.get_language()
if not curlang in ['de', 'fr', 'en', 'it']: curlang = 'de' # Default language
parent = BlogPage.objects.filter(slug='news-%s' % curlang)
if not parent: return []
posts = EntryPage.objects.live().descendant_of(parent[0])
# Order by most recent date first
posts = posts.order_by('-date')
return posts[:settings.BLOG_ENTRIES_HOME_PAGE]
@property
def newsentries(self):
# Get the last few news entries for the home page
entries = Entry.objects.filter(
models.Q(expire_at__isnull=True) | models.Q(expire_at__gt=datetime.datetime.now())
).all().order_by('-published')
entries = Entry.objects.all().order_by('-published')
# Filter out by current language
curlang = translation.get_language()
if curlang in ['de']:

View file

@ -6,8 +6,9 @@ from django.db import models
from wagtail.snippets.models import register_snippet
from wagtail.models import Page
from wagtail.admin.panels import FieldPanel
from wagtail.core.models import Page
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.admin.edit_handlers import FieldPanel, PageChooserPanel
from .forms import ContactForm
from ..util import TranslatedField
@ -16,7 +17,6 @@ from ..util import TranslatedField
SOCIAL_NETWORK_SUPPORTED = (
('twitter', 'Twitter'),
('facebook', 'Facebook'),
('linkedin', 'LinkedIn'),
)
@register_snippet
@ -47,8 +47,6 @@ class SocialContact(models.Model):
return "https://twitter.com/%s" % self.profile
elif self.network == 'facebook':
return "https://facebook.com/%s" % self.profile
elif self.network == 'linkedin':
return "https://www.linkedin.com/company/%s" % self.profile
return "#"
def __str__(self):
return "%s" % self.network
@ -99,15 +97,6 @@ class Contact(models.Model):
on_delete=models.SET_NULL,
related_name='+',
)
news_letter_url = models.URLField(default='', blank=True)
news_letter_text = models.CharField(default='', blank=True, max_length=256)
news_letter_text_fr = models.CharField(max_length=255, blank=True, default="")
news_letter_text_en = models.CharField(max_length=255, blank=True, default="")
trans_news_letter_text = TranslatedField(
'news_letter_text',
'news_letter_text_fr',
'news_letter_text_en',
)
panels = Page.content_panels + [
FieldPanel('title_fr'),
@ -117,19 +106,12 @@ class Contact(models.Model):
FieldPanel('phone'),
FieldPanel('email'),
FieldPanel('www'),
FieldPanel('logo'), # ImageChooserPanel no longer needed in Wagtail 3+
ImageChooserPanel('logo'),
FieldPanel('color'),
FieldPanel('style'),
FieldPanel('map_url'),
FieldPanel('analytics'),
FieldPanel('contact_form'), # PageChooserPanel no longer needed in Wagtail 3+
]
panels = panels + [
FieldPanel('news_letter_url', classname="full"),
FieldPanel('news_letter_text'),
FieldPanel('news_letter_text_fr'),
FieldPanel('news_letter_text_en'),
PageChooserPanel('contact_form', 'home.ContactForm'),
]
def phone_link(self):

View file

@ -1,112 +0,0 @@
"""
Detect and repair Wagtail rich-text content with inline/entity tags wrapping
block-level elements — a pattern that crashes the Draftail editor's
HTML->ContentState converter (`AssertionError: End of block reached without
closing inline style/entity elements`).
Used by:
- the `fix_rich_text` management command, for one-off cleanup of legacy data
- the pre-save signal handlers in `signals.py`, to prevent future paste-from-Word
content from re-introducing the same patterns
"""
from bs4 import BeautifulSoup, NavigableString
from wagtail.admin.rich_text.converters.contentstate import ContentstateConverter
from wagtail.rich_text import features as feature_registry
_INLINE_TAGS = {"b", "i", "em", "strong", "code", "s", "sub", "sup", "u"}
_ENTITY_TAGS = {"a"}
_PROBLEM_WRAPPERS = list(_INLINE_TAGS | _ENTITY_TAGS)
_BLOCK_TAGS = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "hr",
"ul", "ol", "li", "blockquote", "div"]
_converter = None
def get_converter():
"""Return a ContentstateConverter loaded with every Draftail-registered feature.
Lazy-built once per process — the converter init walks the feature registry
which isn't ready at import time.
"""
global _converter
if _converter is None:
feature_registry.get_default_features()
plugins_by_editor = getattr(feature_registry, "plugins_by_editor", {})
draftail_plugins = list(plugins_by_editor.get("draftail", {}).keys())
default_features = feature_registry.get_default_features()
all_features = sorted(set(draftail_plugins) | set(default_features))
_converter = ContentstateConverter(features=all_features)
return _converter
def is_broken(html):
"""Return True if running `html` through the Draftail converter would crash."""
if not html:
return False
try:
get_converter().from_database_format(html)
except Exception:
return True
return False
def _has_significant_other_content(tag, block_child):
for c in tag.children:
if c is block_child:
continue
if isinstance(c, NavigableString):
if c.strip():
return True
else:
return True
return False
def repair(html):
"""Return repaired HTML by lifting block-wrapping inline/entity tags.
For every inline (b/i/em/strong/code/s/sub/sup/u) or entity (a) tag that
contains a block descendant:
- If it has exactly one block child and nothing else significant, invert
the nesting: `<a><p>X</p></a>` -> `<p><a>X</a></p>` (preserves link/style).
- Otherwise, unwrap the offending tag (drops the styling, keeps content).
"""
soup = BeautifulSoup(html, "html.parser")
while True:
offender = None
for tag in soup.find_all(_PROBLEM_WRAPPERS):
if tag.find(_BLOCK_TAGS):
offender = tag
break
if offender is None:
break
block_children = [c for c in offender.children if getattr(c, "name", None) in _BLOCK_TAGS]
if len(block_children) == 1 and not _has_significant_other_content(offender, block_children[0]):
block = block_children[0]
new_wrapper = soup.new_tag(offender.name, attrs=dict(offender.attrs))
for child in list(block.contents):
new_wrapper.append(child.extract())
block.append(new_wrapper)
offender.unwrap()
else:
offender.unwrap()
return str(soup)
def sanitize_if_needed(html):
"""Return repaired HTML if the input would crash the editor, else the input unchanged.
Fast path: if the converter accepts the HTML, return it untouched (no parsing).
Repair path: if the cleaned HTML still wouldn't load, return the original to
avoid making things worse.
"""
if not is_broken(html):
return html
cleaned = repair(html)
if is_broken(cleaned):
return html
return cleaned

View file

@ -4,10 +4,10 @@ from datetime import date
from django.utils.dateformat import DateFormat
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _ # ugettext_lazy deprecated in Django 4.0
from django.utils.translation import ugettext_lazy as _
from wagtail.models import Page # wagtail.core.models moved to wagtail.models in 3.0
from wagtail.contrib.search_promotions.models import Query
from wagtail.core.models import Page
from wagtail.search.models import Query
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
class HomePage(RoutablePageMixin, Page):

View file

@ -1,140 +0,0 @@
"""
Pre-save signal handlers that sanitize rich-text content before it lands
in the database.
The Draftail editor renders the *latest revision* of a page (not the page
row), so the most important hook is `Revision.pre_save` — that catches every
admin draft-save and publish flow. `Page.pre_save` is a belt-and-braces
catch for direct model writes (shell, migrations, etc).
"""
import json
from django.db.models.signals import pre_save
from django.dispatch import receiver
from wagtail.models import Page
from wagtail.fields import RichTextField, StreamField
from wagtail.blocks import RichTextBlock
from .rich_text_sanitizer import sanitize_if_needed
try:
from wagtail.models import Revision
except ImportError: # pragma: no cover — fallback for older Wagtail layouts
Revision = None
def _sanitize_stream_items(items, stream_block):
"""Sanitize a list of {type, value, id} dicts in place. Return True if any changed."""
changed = False
child_blocks = getattr(stream_block, "child_blocks", {})
for item in items:
if not isinstance(item, dict):
continue
block_def = child_blocks.get(item.get("type"))
if isinstance(block_def, RichTextBlock):
value = item.get("value")
if isinstance(value, str):
cleaned = sanitize_if_needed(value)
if cleaned != value:
item["value"] = cleaned
changed = True
return changed
@receiver(pre_save)
def sanitize_revision_content(sender, instance, **kwargs):
"""Sanitize rich-text values inside Revision.content before it's persisted."""
if Revision is None or sender is not Revision:
return
ct = getattr(instance, "content_type", None)
if ct is None:
return
model_class = ct.model_class()
if model_class is None or not issubclass(model_class, Page):
return
raw = instance.content
# Wagtail 4+ stores Revision.content as a JSONField (dict). Older Wagtail
# versions and any string-cast paths may give a JSON-encoded string.
if isinstance(raw, dict):
content_dict = raw
was_string = False
elif isinstance(raw, str):
try:
content_dict = json.loads(raw)
except (TypeError, ValueError):
return
was_string = True
else:
return
if not isinstance(content_dict, dict):
return
changed = False
for field in model_class._meta.get_fields():
if isinstance(field, RichTextField):
value = content_dict.get(field.name)
if isinstance(value, str):
cleaned = sanitize_if_needed(value)
if cleaned != value:
content_dict[field.name] = cleaned
changed = True
elif isinstance(field, StreamField):
value = content_dict.get(field.name)
stream_block = field.stream_block
if isinstance(value, list):
if _sanitize_stream_items(value, stream_block):
changed = True
elif isinstance(value, str):
try:
items = json.loads(value)
except (TypeError, ValueError):
continue
if isinstance(items, list) and _sanitize_stream_items(items, stream_block):
content_dict[field.name] = json.dumps(items)
changed = True
if changed:
instance.content = json.dumps(content_dict) if was_string else content_dict
@receiver(pre_save)
def sanitize_page_fields(sender, instance, **kwargs):
"""Sanitize rich-text fields on the Page model itself, for direct writes."""
if not isinstance(instance, Page):
return
for field in instance._meta.get_fields():
if isinstance(field, RichTextField):
value = getattr(instance, field.name, "") or ""
cleaned = sanitize_if_needed(value)
if cleaned != value:
setattr(instance, field.name, cleaned)
elif isinstance(field, StreamField):
stream_value = getattr(instance, field.name, None)
if stream_value is None:
continue
try:
children = list(stream_value)
except Exception:
continue
replacements = {}
for i, child in enumerate(children):
if not isinstance(child.block, RichTextBlock):
continue
v = child.value
html = v.source if hasattr(v, "source") else str(v)
cleaned = sanitize_if_needed(html)
if cleaned != html:
replacements[i] = cleaned
if not replacements:
continue
new_list = []
for i, child in enumerate(children):
if i in replacements:
new_list.append({"type": child.block_type, "value": replacements[i]})
else:
raw = child.block.get_prep_value(child.value)
new_list.append({"type": child.block_type, "value": raw})
setattr(instance, field.name, new_list)

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load i18n wagtailcore_tags wagtailimages_tags %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block body_class %}template-{{ self.get_verbose_name|slugify }}{% endblock %}
@ -16,25 +16,11 @@
</section>
{% else %}
{% if page.header_image %}
{% image page.header_image fill-1908x400 as img %}
<div id="carousel-banner" class="slide">
<div class="carousel-inner slick slick-initialized slick-slider" role="listbox">
<div class="slick-list draggable" tabindex="0"><div class="slick-track" style="opacity: 1; width: 1908px;"><div class="item slick-slide slick-active" data-slick-index="0" style="width: 1908px; position: relative; left: 0px; top: 0px; z-index: 900; opacity: 1;">
<img style="background-image:url({{img.url}})">
</div></div></div>
</div>
</div>
{% endif %}
<section id="article-index" class="article-index-page">
<div class="container">
<h2>{{ page.trans_title }}</h2>
<p class="lead">{{ page.trans_intro|richtext }}</p>
{% if page.subscribe_action %}
{% include 'home/subscription_form.html' %}
{% endif %}
<div class="article-body" role="main">
{% for entry in articles %}
<div class="item">
@ -45,14 +31,6 @@
</div>
{% endfor %}
</div>
<!-- Table content -->
<div class="article-table table-program" role="main">
{% for block in page.trans_table %}
{% if block.block_type == 'table_en' or block.block_type == 'table_fr' or block.block_type == 'table_de' %}
{% include_block block %}
{% endif %}
{% endfor %}
</div>
</div><!-- /container -->
</section>

View file

@ -1,4 +1,4 @@
{% load wagtailcore_tags wagtailimages_tags custom_filters %}
{% load wagtailcore_tags wagtailimages_tags %}
{% if page.feed_image %}
<div class="image">
@ -23,10 +23,6 @@
{% if block.value == 'gallery' %}
{% include 'home/photo_gallery.html' %}
{% endif %}
{% elif block.block_type == 'iframe' %}
{% autoescape off %}
{{ block.value|richtext|ph_html_decode}}
{% endautoescape %}
{% elif block.block_type != 'info' %}
<p class="block-{{ block.block_type }}">
{% include_block block %}

View file

@ -1,53 +0,0 @@
<style>
.table-program td, th {
padding: 0 10px !important;
}
</style>
<table class="table-program">
{% if table_header %}
<thead>
<tr>
{% for column in table_header %}
<th>
{% if column.strip %}
{% if html_renderer %}
{{ column.strip|safe|linebreaksbr }}
{% else %}
{{ column.strip|linebreaksbr }}
{% endif %}
{% endif %}
</th>
{% endfor %}
</tr>
</thead>
{% endif %}
<tbody>
{% for row in data %}
<tr>
{% for column in row %}
{% if first_col_is_header and forloop.first %}
<th>
{% if column.strip %}
{% if html_renderer %}
{{ column.strip|safe|linebreaksbr }}
{% else %}
{{ column.strip|linebreaksbr }}
{% endif %}
{% endif %}
</th>
{% else %}
<td>
{% if column.strip %}
{% if html_renderer %}
{{ column.strip|safe|linebreaksbr }}
{% else %}
{{ column.strip|linebreaksbr }}
{% endif %}
{% endif %}
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -1,49 +0,0 @@
<a href="#" class="btn btn-sm btn-primary pull-right" style="text-transform: uppercase;" data-toggle="modal" data-target="#subscribeModal">{{ page.trans_subscribe_label }}</a>
<div class="modal fade" id="subscribeModal" tabindex="-1" role="dialog" aria-labelledby="subscribeModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="myModalLabel">{{ page.trans_subscribe_label }}</h4>
</div>
<div class="modal-body">
<div id="mc_embed_signup">
<form action="{{page.subscribe_action}}" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
<div id="mc_embed_signup_scroll">
<div class="indicates-required"><span class="asterisk"></span> indicates required</div>
<div class="mc-field-group">
<label for="mce-EMAIL">Mailadresse/ Adresse e-mail <span class="asterisk">*</span>
</label>
<input type="email" value="" name="EMAIL" class="required email" id="mce-EMAIL">
</div>
<div class="mc-field-group">
<label for="mce-FNAME">Vorname/ Prénom <span class="asterisk">*</span>
</label>
<input type="text" value="" name="FNAME" class="required" id="mce-FNAME">
</div>
<div class="mc-field-group">
<label for="mce-LNAME">Nachname/ Nom <span class="asterisk">*</span>
</label>
<input type="text" value="" name="LNAME" class="required" id="mce-LNAME">
</div>
<div class="mc-field-group input-group">
<strong>Sprache/ Langue <span class="asterisk">*</span>
</strong>
<ul><li><input class="pull-left" type="radio" value="Deutsch" name="MMERGE9" id="mce-MMERGE9-0"><label class="pull-left" for="mce-MMERGE9-0">Deutsch</label></li>
<li class="clearfix"></li>
<li><input class="pull-left" type="radio" value="Français" name="MMERGE9" id="mce-MMERGE9-1"><label class="pull-left" for="mce-MMERGE9-1">Français</label></li>
</ul>
</div>
<div id="mce-responses" class="clear">
<div class="response" id="mce-error-response" style="display:none"></div>
<div class="response" id="mce-success-response" style="display:none"></div>
</div> <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
<div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_8368e636ee0d8404d5452a0ef_77f535b64d" tabindex="-1" value=""></div>
<div class="clear"><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
{% load wagtailcore_tags wagtailimages_tags %}
{% load wagtailcore_tags wagtailimages_tags puput_tags %}
<!-- Front page news -->
<section id="news">

View file

@ -1,5 +0,0 @@
{% if contact.trans_news_letter_text %}
<div style="text-align: center">
<a href="{{ contact.news_letter_url }}" style="border: 1px solid; padding: 15px;">{{ contact.trans_news_letter_text }}</a>
</div>
{% endif %}

View file

@ -3,6 +3,6 @@
<ul class="dropdown-menu">
<!--<li><a href="{% pageurl parent %}">{{ parent.title }}</a></li>-->
{% for child in menuitems_children %}
<li><a href="{% pageurl child %}">{{ child.trans_title }}</a></li>
<li><a href="{% pageurl child %}">{{ child.title|title }}</a></li>
{% endfor %}
</ul>

View file

@ -35,19 +35,11 @@ def get_contacts(the_site):
'socials': social_contacts
}
# Contact information (footer)
@register.inclusion_tag('tags/contact_info.html')
def contact_info(the_site):
return get_contacts(the_site)
# Newsletter (footer)
@register.inclusion_tag('tags/news_letter.html')
def news_letter(the_site):
return get_contacts(the_site)
# Contact form (footer)
@register.inclusion_tag('tags/footer_form.html')
def footer_form(the_site):

View file

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
from django import template
from django.utils import translation
from django.db.models import Case, When
from wagtail.models import Site, Page
from wagtail.core.models import Site, Page
from ..models.forms import ContactForm
register = template.Library()
@ -55,20 +54,19 @@ def top_menu(context, parent, calling_page=None):
menuitems = parent.get_children().live().in_menu().specific()
for menuitem in menuitems:
menuitem.show_dropdown = has_menu_children(menuitem)
if calling_page:
menuitem.active = (calling_page.url.startswith(menuitem.url)
if calling_page else False)
try:
menuitem.title = menuitem.trans_title
except AttributeError:
pass
menuitem.active = (calling_page.url.startswith(menuitem.url)
if calling_page else False)
try:
menuitem.title = menuitem.trans_title
except AttributeError:
pass
return {
'calling_page': calling_page,
'menuitems': menuitems,
'request': context['request'],
}
def menuitems_children(parent, context=None):
def menuitems_children(parent):
remove_mitglied = False
remove_devenez = False
if 'Qui sommes-nous' in parent.title:
@ -79,45 +77,20 @@ def menuitems_children(parent, context=None):
items_to_remove = []
for menuitem in menuitems_children:
try:
if type(menuitem) == ContactForm:
menuitem.title = menuitem.title.title()
else:
menuitem.title = menuitem.trans_title
if 'devenez' in menuitem.title.lower() and remove_devenez:
items_to_remove.append(menuitem)
elif 'mitglied werden' in menuitem.title.lower() and remove_mitglied:
items_to_remove.append(menuitem)
menuitem.title = menuitem.trans_title
except AttributeError as ae:
except AttributeError:
pass
for item in items_to_remove:
menuitems_children = menuitems_children & Page.objects.not_page(item)
if context:
request = context['request']
site = Site.find_for_request(request)
if site.hostname in ['sphc.ch', 'public-health.ch']:
# custom page here is the terms and conditions page for public-health.ch and sphc.ch (common)
if request.__dict__.get('LANGUAGE_CODE', 'de').lower() == 'en':
custom_page_id = 1020
else:
custom_page_id = 994
custom_page = Page.objects.get(id=custom_page_id)
menuitems_children = add_item_to_page_queryset(menuitems_children, custom_page)
for menuitem in menuitems_children:
try:
menuitem.title = menuitem.trans_title
except AttributeError as aee:
pass
return menuitems_children
def add_item_to_page_queryset(queryset, custom_page):
# Convert queryset to a list
pages_list = list(queryset)
# Add the custom page to the list
pages_list.append(custom_page)
# Create a new ordered queryset based on the list order
page_ids = [page.id for page in pages_list]
preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(page_ids)])
new_queryset = Page.objects.filter(id__in=page_ids).order_by(preserved_order).specific()
return new_queryset
# Retrieves the children of the top menu items for the drop downs
@register.inclusion_tag('tags/top_menu_children.html', takes_context=True)
def top_menu_children(context, parent):
@ -132,6 +105,6 @@ def top_menu_children(context, parent):
def footer_menu(context, parent, calling_page=None):
return {
'calling_page': calling_page,
'menuitems': menuitems_children(parent, context),
'menuitems': menuitems_children(parent),
'request': context['request'],
}

View file

@ -1,8 +1,8 @@
from django.shortcuts import render
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from wagtail.models import Page
from wagtail.contrib.search_promotions.models import Query
from wagtail.core.models import Page
from wagtail.search.models import Query
def search(request):

View file

@ -18,9 +18,6 @@ import os
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.dirname(PROJECT_DIR)
# Django 4.0 requires DEFAULT_AUTO_FIELD
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Project custom settings
NEWS_ENTRIES_HOME_PAGE = 2
@ -41,13 +38,12 @@ INSTALLED_APPS = [
'wagtail.contrib.routable_page',
'wagtail.contrib.sitemaps',
'wagtail_modeladmin', # Separate package for ModelAdmin in Wagtail 5+
'wagtail.contrib.modeladmin',
'wagtail.contrib.settings',
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.contrib.search_promotions',
# "wagtail.contrib.legacy.richtext", # Removed in Wagtail 3.0
'wagtail.contrib.table_block',
"wagtail.contrib.legacy.richtext",
'wagtail.embeds',
'wagtail.sites',
@ -57,8 +53,7 @@ INSTALLED_APPS = [
'wagtail.images',
'wagtail.search',
'wagtail.admin',
# 'wagtail.core' renamed to 'wagtail' in Wagtail 3.0
'wagtail',
'wagtail.core',
'wagtail.api.v2',
'rest_framework',
@ -67,7 +62,7 @@ INSTALLED_APPS = [
'modelcluster',
'compressor',
'taggit',
# 'puput', # Needs update for Wagtail 6.x compatibility
'puput',
'anymail',
'feedler',
'colorful',
@ -113,9 +108,6 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
'libraries': {
'custom_filters': 'publichealth.home.custom_filters',
},
},
},
]
@ -167,8 +159,7 @@ LANGUAGE_CODE = 'de' # default language
TIME_ZONE = 'Europe/Zurich'
USE_I18N = True
# USE_L10N is deprecated in Django 4.0 and always True
# USE_L10N = True
USE_L10N = True
USE_TZ = True
@ -185,16 +176,9 @@ STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
]
# Django 4.2 uses STORAGES setting instead of STATICFILES_STORAGE
# Whitenoise compression and caching support
# http://whitenoise.evans.io/en/stable/django.html
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
@ -214,10 +198,9 @@ COMPRESS_PRECOMPILERS = [
WAGTAIL_SITE_NAME = "Public Health Schweiz"
# Wagtail 5 uses Draftail as default editor
WAGTAILADMIN_RICH_TEXT_EDITORS = {
'default': {
'WIDGET': 'wagtail.admin.rich_text.DraftailRichTextArea'
'WIDGET': 'wagtail.admin.rich_text.HalloRichTextArea'
}
}

View file

@ -1,9 +1,6 @@
import logging
import os
import sys
import dj_database_url
from logging.handlers import RotatingFileHandler
from .base import *
# Do not set SECRET_KEY, Postgres or LDAP password or any other sensitive data here.
@ -43,13 +40,9 @@ if 'SECRET_KEY' in env:
if 'ALLOWED_HOSTS' in env:
ALLOWED_HOSTS = env['ALLOWED_HOSTS'].split(',')
# Django 4.1+ requires scheme in CSRF_TRUSTED_ORIGINS
CSRF_TRUSTED_ORIGINS = ['https://' + host for host in ALLOWED_HOSTS if host != '*']
if 'PRIMARY_HOST' in env:
BASE_URL = 'http://%s/' % env['PRIMARY_HOST']
# Also add PRIMARY_HOST to CSRF_TRUSTED_ORIGINS
CSRF_TRUSTED_ORIGINS = ['https://%s' % env['PRIMARY_HOST'], 'http://%s' % env['PRIMARY_HOST']]
if 'SERVER_EMAIL' in env:
SERVER_EMAIL = env['SERVER_EMAIL']
@ -76,16 +69,23 @@ if 'MEDIA_DIR' in env:
MEDIA_ROOT = env['MEDIA_DIR']
DATABASES = {
'default': {
'ENGINE': os.environ.get('SQL_ENGINE', 'django.db.backends.postgresql'),
'NAME': os.environ.get('POSTGRES_DB', 'app'),
'USER': os.environ.get('POSTGRES_USER', 'app'),
'PASSWORD': os.environ.get('POSTGRES_PASSWORD', ''),
'HOST': os.environ.get('POSTGRES_HOST', 'localhost'),
'PORT': os.environ.get('POSTGRES_PORT', 5432),
# Database
if 'DATABASE_URL' in os.environ:
DATABASES = {'default': dj_database_url.config()}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': env.get('PGDATABASE', APP_NAME),
'CONN_MAX_AGE': 600, # number of seconds database connections should persist for
# User, host and port can be configured by the PGUSER, PGHOST and
# PGPORT environment variables (these get picked up by libpq).
}
}
}
# Email via ESP
@ -149,46 +149,42 @@ LOGGING = {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'stream': sys.stdout,
'formatter': 'simple',
},
},
'formatters': {
'default': {
'format': '[%(asctime)s] (%(process)d/%(thread)d) %(name)s %(levelname)s: %(message)s'
},
'simple': {
'format': '[%(asctime)s] (%(process)d/%(thread)d) %(name)s %(levelname)s: %(message)s'
'verbose': '[%(asctime)s] (%(process)d/%(thread)d) %(name)s %(levelname)s: %(message)s'
}
},
'loggers': {
'feedler': {
'handlers': ['console'],
'handlers': [],
'level': 'INFO',
'propagate': False,
'formatter': 'verbose',
},
'publichealth': {
'handlers': ['console'],
'handlers': [],
'level': 'INFO',
'propagate': False,
'formatter': 'verbose',
},
'wagtail': {
'handlers': ['console'],
'handlers': [],
'level': 'WARNING',
'propagate': False,
'formatter': 'verbose',
},
'django.request': {
'handlers': ['console', 'mail_admins'],
'level': 'WARNING',
'propagate': False,
},
'django.security': {
'handlers': ['console', 'mail_admins'],
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
'formatter': 'verbose',
},
'django.security': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
'formatter': 'verbose',
},
},
}
@ -198,7 +194,7 @@ if 'LOG_DIR' in env:
# Public Health Schweiz log
LOGGING['handlers']['publichealth_file'] = {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'class': 'cloghandler.ConcurrentRotatingFileHandler',
'filename': os.path.join(env['LOG_DIR'], 'publichealth.log'),
'maxBytes': 5242880, # 5MB
'backupCount': 5
@ -208,7 +204,7 @@ if 'LOG_DIR' in env:
# Wagtail log
LOGGING['handlers']['wagtail_file'] = {
'level': 'WARNING',
'class': 'logging.handlers.RotatingFileHandler',
'class': 'cloghandler.ConcurrentRotatingFileHandler',
'filename': os.path.join(env['LOG_DIR'], 'wagtail.log'),
'maxBytes': 5242880, # 5MB
'backupCount': 5
@ -218,7 +214,7 @@ if 'LOG_DIR' in env:
# Error log
LOGGING['handlers']['errors_file'] = {
'level': 'ERROR',
'class': 'logging.handlers.RotatingFileHandler',
'class': 'cloghandler.ConcurrentRotatingFileHandler',
'filename': os.path.join(env['LOG_DIR'], 'error.log'),
'maxBytes': 5242880, # 5MB
'backupCount': 5

View file

@ -128,12 +128,5 @@ $slider-nav: 200px;
// Fix Bootstrap 3 warning
.media, .media-body { zoom: none !important; transform: scale(1); }
#mc_embed_signup input[type=radio] {
-webkit-appearance: none; padding: 0px;margin-top: 0px;
}
#mc_embed_signup {
background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif;
}
@import "subsites";

View file

@ -58,28 +58,15 @@ section {
}
// Responsive image/video classes
// Note: Wagtail 4.0+ removed the .rich-text wrapper div,
// so we target container elements directly.
.rich-text img,
.article-body img,
.lead img {
.rich-text img {
max-width: 100%;
height: auto;
}
// Wagtail 4.0+ no longer wraps embedded rich-text images in a <p> block, so
// floating them made supporter/sponsor logos collapse side by side and let
// trailing text (captions, contact blocks) wrap beside the last logo. Render
// rich-text images as stacked block elements instead: logos sit one under the
// other and any following text drops below them. The left/right alignment
// classes now position the block via auto margins rather than floating.
.richtext-image.left,
.richtext-image.right{
display:block;
float:none;
clear:both;
.richtext-image.left{
float:left;
}
.richtext-image.right{
margin-left:auto;
float:right;
}
.responsive-object {
position: relative;

View file

@ -10,7 +10,7 @@
// Language menu hack, until this is configurable
nav .language-nav a[lang='en'] { display: inline; }
//#footer, .contact-nav .link { display: none; }
#footer, .contact-nav .link { display: none; }
a.navbar-brand {
height: 60px;
@ -49,7 +49,6 @@
html[lang='fr'] .site-ngo-allianz-ebk .navbar-brand {
background-image: url('/static/org/NGO-Allianz_Logo-fr.jpg');
}
// -site-ngo-allianz-ebk
// Custom styles for gesundheitsmanifest.ch
@ -62,18 +61,3 @@ html[lang='fr'] .site-ngo-allianz-ebk .navbar-brand {
border-bottom: 1px solid #ddd;
}
}
.site-dev-pro-salutech {
.navbar-brand {
width: 240px;
height: 64px;
background-size: contain !important;
background-repeat: no-repeat;
background-image: url('/static/images/pro-salute-logo.png');
font-size: 0px !important;
img { display: none !important; }
}
#carousel-banner .carousel-caption { display: none; }
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -11,6 +11,6 @@
<h2>Sorry, there seems to be an error. Please try again soon.</h2>
<p>In case of persistent issues, write to <b>info@ungleich.ch</b></p>
<p>In case of persistent issues, write to <b>info@datalets.ch</b></p>
</body>
</html>

View file

@ -11,9 +11,6 @@
<meta name="description" content="{% block description %}{% endblock %}">
{% block extra_css %}{% endblock %}
{% if page.subscribe_action %}
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">
{% endif %}
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static 'css/main.scss' %}">
{% endcompress %}
@ -38,28 +35,7 @@
<script src="{% static 'libs/slick-carousel/slick/slick.min.js' %}"></script>
<script src="{% static 'libs/slick-lightbox/dist/slick-lightbox.js' %}"></script>
<script src="{% static 'libs/cookieconsent/build/cookieconsent.min.js' %}"></script>
{% if page.subscribe_action %}
<script type='text/javascript' src='//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js'></script>
<script type='text/javascript'>(function($) {
window.fnames = new Array();
window.ftypes = new Array();
fnames0='EMAIL';
ftypes0='email';
fnames1='FNAME';
ftypes1='text';
fnames2='LNAME';
ftypes2='text';
fnames9='MMERGE9';
ftypes9='radio';
$.extend($.validator.messages, {
required: "Dieses Feld ist ein Pflichtfeld.",
maxlength: $.validator.format("Geben Sie bitte maximal {0} Zeichen ein."),
minlength: $.validator.format("Geben Sie bitte mindestens {0} Zeichen ein."),
email: "Geben Sie bitte eine gültige E-Mail Adresse ein.",
});}(jQuery));
var $mcj = jQuery.noConflict(true);
</script>
{% endif %}
{% block extra_js %}{% endblock %}
{% compress js %}

View file

@ -24,7 +24,6 @@
</div>
<div class="col-md-4" id="contact-form">
{% footer_form the_site=the_site %}
{% news_letter the_site=the_site %}
</div>
</div><!-- /row -->
</div><!-- /container -->
@ -32,7 +31,7 @@
<div class="copyright">
{% contact_name the_site=the_site %}
&copy; 2022
&copy; 2020
&bull; <a href="https://public-health.ch/privacy/">Privacy</a>
&bull; <a href="https://public-health.ch/impressum/">Impressum</a>
</div>

View file

@ -2,9 +2,6 @@
{% get_site_root as site_root %}
{% get_site as the_site %}
<style>
</style>
<nav class="navbar-pre navbar-fixed-top">
<div class="container">
<div class="nav">
@ -30,16 +27,9 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{% pageurl site_root %}">
{% if the_site|default:""|truncatechars:"255" == "dev.pro-salute.ch"%}
<img src="{% static 'images/pro-salute-logo.png' %}" alt="Pro Salute Logo">
{% else %}
<img src="{% static 'images/public-health-logo-sign.png' %}" alt="[logo]">
{% endif %}
<span class="hidden-xs">{% contact_name the_site=the_site %}</span></a>
<a class="navbar-brand" href="{% pageurl site_root %}">
<img src="{% static 'images/public-health-logo-sign.png' %}" alt="[logo]">
<span class="hidden-xs">{% contact_name the_site=the_site %}</span></a>
</div>
{% block menu %}

View file

@ -1,30 +1,30 @@
from django.urls import include, re_path, path
from django.conf.urls import include, url
from django.conf import settings
from django.contrib import admin
from django.conf.urls.i18n import i18n_patterns
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
from wagtail import urls as wagtail_urls # wagtail.core renamed to wagtail in 3.0
from wagtail.core import urls as wagtail_urls
from wagtail.contrib.sitemaps.views import sitemap
# from puput import urls as puput_urls # Commented: needs Wagtail 6 update
from puput import urls as puput_urls
from feedler import urls as feedler_urls
from publichealth.search import views as search_views
urlpatterns = [
# path('', include(puput_urls)), # Commented: needs Wagtail 6 update
path('', include(feedler_urls)),
url(r'', include(puput_urls)),
url(r'', include(feedler_urls)),
path('django-admin/', admin.site.urls),
url(r'^django-admin/', admin.site.urls),
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
path('search/', search_views.search, name='search'),
url(r'^search/$', search_views.search, name='search'),
re_path(r'^sitemap\.xml$', sitemap),
url('^sitemap\.xml$', sitemap),
]
@ -39,13 +39,13 @@ if settings.DEBUG:
# Add views for testing 404 and 500 templates
urlpatterns += [
path('test404/', TemplateView.as_view(template_name='404.html')),
path('test500/', TemplateView.as_view(template_name='500.html')),
url(r'^test404/$', TemplateView.as_view(template_name='404.html')),
url(r'^test500/$', TemplateView.as_view(template_name='500.html')),
]
urlpatterns += i18n_patterns(
# These URLs will have /<language_code>/ appended to the beginning
path('', include(wagtail_urls)),
url(r'', include(wagtail_urls)),
)

View file

@ -1,43 +0,0 @@
# Django 4.0 Upgrade - Python 3.13 compatible versions
# Core
Django==4.0.10
wagtail==3.0.3
elasticsearch==5.5.3
# Database
psycopg2-binary
dj-database-url==1.2.0
# Content
# Note: puput may need manual fixing for Wagtail 3.0
# puput==2.0.0
guess-language-spirit==0.5.3
# Caching
django-redis==5.2.0
redis
# Frontend
django-anymail==9.0
django-colorful==1.3
django-contrib-comments==2.2.0
django-crispy-forms==1.14.0
django-el-pagination==3.3.0
django-compressor
django-libsass==0.9
django-social-share==2.3.0
libsass
Pillow
# Runtime dependencies
gunicorn
whitenoise
python-dotenv
unidecode
# Additional Django 4.0 dependencies
django-configurations
django-taggit
djangorestframework
django-modelcluster

View file

@ -1,54 +1,33 @@
# Updated: 2025-09-18 - Django 5.0 Upgrade
# Updated: 21.07.2021
# Core
Django==5.0.9 # Django 5.0 (latest stable in 5.0 series)
wagtail==6.0.6 # Wagtail 6.0 supports Django 5.0
elasticsearch==7.17.9 # Compatible with Django 5.0
wagtail==2.12.5
elasticsearch>=5.0.0,<6.0.0
# Database
psycopg2-binary==2.9.10 # Compatible with Django 5.0 and Python 3.13
dj-database-url==2.2.0 # Updated for Django 5.0
psycopg2-binary>=2.8.6,<2.9
dj-database-url==0.5.0
# Content
# Note: puput needs update or replacement for Wagtail 6.x
# puput==2.0.0
puput==1.1.1
guess-language-spirit==0.5.3
# Caching
django-redis==5.4.0 # Compatible with Django 5.0
redis==5.0.8
django-redis==5.0.0
# Frontend
django-anymail==11.0 # Updated for Django 5.0
django-anymail==8.4
django-colorful==1.3
django-contrib-comments==2.2.0 # May need testing with Django 5.0
django-crispy-forms==2.3 # Updated for Django 5.0
django-contrib-comments==2.1.0
django-crispy-forms==1.12.0
django-el-pagination==3.3.0
django-compressor==4.5.1 # Updated for Django 5.0
django-social-share==2.3.0 # May need testing with Django 5.0
libsass==0.23.0 # Updated version
Pillow==10.4.0 # Updated for Django 5.0 and Python 3.12+
django-libsass==0.8
django-social-share==2.2.1
libsass==0.21.0
Pillow==8.2.0
# Runtime dependencies
gunicorn
whitenoise
python-dotenv
#ConcurrentLogHandler
unidecode
# Additional dependencies for Django 5.0 and Wagtail 6
django-configurations
django-taggit>=5.0.0 # Required by Wagtail 6
wagtail-modeladmin==2.0.0 # Separate package for ModelAdmin in Wagtail 5+
djangorestframework>=3.15.0 # Required by Wagtail API
django-modelcluster>=6.3 # Required by Wagtail 6
django-treebeard>=4.7 # Required by Wagtail 6
django-filter>=23.3,<24 # Required by Wagtail 6.0.6 (must be <24)
Willow>=1.8.0 # Required by Wagtail for images
beautifulsoup4>=4.12.0 # Required by Wagtail
html5lib>=1.1 # Required by Wagtail
openpyxl>=3.1.0 # For Excel export support
l18n # For Wagtail localization
telepath>=0.3.1 # Required by Wagtail 6
# Django 5.0 timezone support (replaces pytz)
tzdata # For timezone support in Django 5.0
ConcurrentLogHandler

View file

@ -1,18 +0,0 @@
#!/bin/bash
cd /app
rm -rf node_modules
yarn install
mkdir -p /app/publichealth/static/libs
echo "created /app/publichealth/static/libs contents "
ls /app/publichealth/static/libs
cp -rf node_modules/@bower_components/* /app/publichealth/static/libs/
# static resources
python manage.py collectstatic --no-input -i media --settings=publichealth.settings.production
python manage.py compress --force --settings=publichealth.settings.production
if [ -d /app/static ]; then
echo "Copying"
cp -a /app/static /pod
fi

2999
yarn.lock

File diff suppressed because it is too large Load diff