Compare commits

..

143 commits

Author SHA1 Message Date
PCoder
a398a3c20d Stack rich-text logos as blocks so trailing text sits below them
The previous clear:both fix stacked the logos but, because the images were
still float:left, trailing text (captions, contact block) wrapped beside the
last logo. Render rich-text images as block elements (display:block;float:none)
so following text drops below the logos. Right-aligned images keep their
position via margin-left:auto.
2026-06-03 18:56:54 +05:30
PCoder
460c5bcfbc Stack rich-text logos vertically after Wagtail 4.0 upgrade
Wagtail 4.0+ no longer wraps embedded rich-text images in a <p> block, so
consecutive float-aligned images (supporter/sponsor logos) collapsed next to
each other instead of stacking. Add clear:both to .richtext-image.left/.right
so each aligned image drops below the previous float, restoring the
pre-upgrade vertical stacking while still allowing text to wrap beside a
single aligned image.
2026-06-03 15:35:00 +05:30
PCoder
9b728e4e86 Sanitize rich-text on save to prevent Draftail editor 500s
Adds a pre-save signal handler on Wagtail's Revision model (and a
belt-and-braces handler on Page) that runs the same repair logic as the
fix_rich_text management command before content is persisted. Catches
inline/entity tags wrapping block-level elements — typically introduced
by Word/Outlook paste — that crash the Draftail editor on the next page
edit.

Refactors the repair core into a reusable module
(publichealth/home/rich_text_sanitizer.py) shared between the signal and
the management command. The fast path (is_broken converter check) makes
the signal a near no-op for clean content.
2026-05-07 14:09:44 +05:30
PCoder
fc955b6d67 Add fix_rich_text management command
Repairs Wagtail rich-text content with inline/entity tags wrapping
block-level elements that crashes the Draftail editor's HTML->ContentState
converter on the page edit form. Detects broken blocks by feeding them
through the same converter that fails, then either inverts the nesting
(<a><p>X</p></a> -> <p><a>X</a></p> — preserves links/styling) or
unwraps the offending tag, and verifies the result before saving as a new
page revision. Defaults to dry-run; --apply writes, --publish publishes,
--diff-file logs full before/after HTML.
2026-05-07 14:02:27 +05:30
PCoder
de2145f6a7 Fix non-responsive images after Wagtail 4.0 upgrade
Wagtail 4.0+ removed the .rich-text wrapper div from richtext output,
so the .rich-text img CSS selector no longer matched images in article
content. Add .article-body img and .lead img selectors to restore
max-width: 100% responsive behavior.
2026-03-18 23:23:27 +05:30
PCoder
50acf681ee Fix error handlers and compress issue 2026-03-07 09:53:57 +05:30
PCoder
770fc1873d Fix Static dir 2026-03-07 09:04:47 +05:30
5cb62d61f4 Merge pull request 'upgrades' (#12) from upgrades into master
Reviewed-on: #12
2026-03-05 11:31:31 +00:00
PCoder
a1f87613bb Fix permissions 2025-12-06 00:18:24 +05:30
PCoder
fbe3ad5f3c Fix SECRET_KEY & STATIC_ROOT 2025-11-03 18:53:07 +05:30
PCoder
41cc7eeaf1 Use standard POSTGRES* constants for the database 2025-11-03 17:38:01 +05:30
PCoder
8e7dcf24c0 Fix image url, tls, secret_key name 2025-09-25 00:06:20 +05:30
PCoder
e90793cb8d Fix image url 2025-09-24 23:32:26 +05:30
PCoder
dbf19fb8ae Update image version 2025-09-24 23:22:06 +05:30
PCoder
157639babd Fix LOG_DIR 2025-09-24 23:19:03 +05:30
PCoder
4394b66386 Changes for 3.0.0-7-g0c947da 2025-09-24 23:06:05 +05:30
PCoder
0c947da845 Fix image name 2025-09-24 22:17:39 +05:30
PCoder
d265e7fac0 More package changes 2025-09-24 21:20:17 +05:30
PCoder
b1a97da02c Fix package names for Debian python slim 2025-09-24 21:14:56 +05:30
PCoder
cb7a5a02be Deploy to ns 2025-09-24 20:44:06 +05:30
PCoder
7e712f9ed9 Add package-lock.json 2025-09-24 20:40:29 +05:30
PCoder
ec0b23153e Add deploy-minikube.sh 2025-09-24 20:40:10 +05:30
PCoder
8532dd43c7 Add k8s files 2025-09-24 20:39:19 +05:30
PCoder
ed65968cc6 Fixes to Dockerfile for building 2025-09-24 18:36:36 +05:30
PCoder
c80a2d3afc Don't install yarn explicitly 2025-09-24 16:55:43 +05:30
PCoder
97ed1a0a70 Update yarn.lock 2025-09-24 16:46:54 +05:30
PCoder
124db1f4f3 Add entrypoint 2025-09-24 16:46:06 +05:30
PCoder
90e1e3c6ab Updated Dockerfile 2025-09-24 16:45:45 +05:30
PCoder
56d17649ac Update .Dockerignore 2025-09-24 16:44:07 +05:30
PCoder
8d1b464d09 Remove puput_tags momentarily because it does not work with Django 5 2025-09-24 16:16:44 +05:30
PCoder
dd3f5068dd Upgrades 2025-09-24 15:50:45 +05:30
PCoder
7c0b43c82b Fix model-admin
File "/usr/lib/python3.13/threading.py", line 1043, in _bootstrap_inner
    self.run()
    ~~~~~~~~^^
  File "/usr/lib/python3.13/threading.py", line 994, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/utils/autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
    ~~^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/core/management/commands/runserver.py", line 125, in inner_run
    autoreload.raise_last_exception()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/utils/autoreload.py", line 87, in raise_last_exception
    raise _exception[1]
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 394, in execute
    autoreload.check_errors(django.setup)()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/utils/autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
    ~~^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/apps/registry.py", line 124, in populate
    app_config.ready()
    ~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/wagtail/snippets/apps.py", line 17, in ready
    register_deferred_snippets()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/wagtail/snippets/models.py", line 121, in register_deferred_snippets
    _register_snippet_immediately(registerable, viewset)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/wagtail/snippets/models.py", line 110, in _register_snippet_immediately
    viewsets.register(registerable)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/wagtail/admin/viewsets/__init__.py", line 30, in register
    viewset.on_register()
    ~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/wagtail/snippets/views/snippets.py", line 1260, in on_register
    self.register_snippet_model()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/wagtail/snippets/views/snippets.py", line 1245, in register_snippet_model
    snippet_models = get_snippet_models()
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/wagtail/snippets/models.py", line 33, in get_snippet_models
    search_for_hooks()
    ~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/wagtail/hooks.py", line 106, in search_for_hooks
    list(get_app_submodules("wagtail_hooks"))
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/wagtail/utils/apps.py", line 23, in get_app_submodules
    yield name, import_module(f"{name}.{submodule_name}")
                ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/importlib/__init__.py", line 88, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/home/pcoder/projects/n/public-health-ch/feedler/wagtail_hooks.py", line 3, in <module>
    from wagtail.contrib.modeladmin.options import (
        ModelAdmin, modeladmin_register)
ModuleNotFoundError: No module named 'wagtail.contrib.modeladmin'
2025-09-18 19:05:07 +05:30
PCoder
1f15503074 Replace PageChooserPanel with FieldPanel
File "/usr/lib/python3.13/threading.py", line 1043, in _bootstrap_inner
    self.run()
    ~~~~~~~~^^
  File "/usr/lib/python3.13/threading.py", line 994, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/utils/autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
    ~~^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/core/management/commands/runserver.py", line 125, in inner_run
    autoreload.raise_last_exception()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/utils/autoreload.py", line 87, in raise_last_exception
    raise _exception[1]
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 394, in execute
    autoreload.check_errors(django.setup)()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/utils/autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
    ~~^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/apps/registry.py", line 116, in populate
    app_config.import_models()
    ~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/apps/config.py", line 269, in import_models
    self.models_module = import_module(models_module_name)
                         ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/importlib/__init__.py", line 88, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/home/pcoder/projects/n/public-health-ch/publichealth/home/models/__init__.py", line 3, in <module>
    from .snippets import *
  File "/home/pcoder/projects/n/public-health-ch/publichealth/home/models/snippets.py", line 57, in <module>
    class Contact(models.Model):
    ...<95 lines>...
            return self.trans_title
  File "/home/pcoder/projects/n/public-health-ch/publichealth/home/models/snippets.py", line 125, in Contact
    PageChooserPanel('contact_form', 'home.ContactForm'),
    ^^^^^^^^^^^^^^^^
NameError: name 'PageChooserPanel' is not defined
2025-09-18 19:00:49 +05:30
PCoder
12301b7c5e Fix errors
Traceback (most recent call last):
  File "/usr/lib/python3.13/threading.py", line 1043, in _bootstrap_inner
    self.run()
    ~~~~~~~~^^
  File "/usr/lib/python3.13/threading.py", line 994, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/utils/autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
    ~~^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/core/management/commands/runserver.py", line 125, in inner_run
    autoreload.raise_last_exception()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/utils/autoreload.py", line 87, in raise_last_exception
    raise _exception[1]
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/core/management/__init__.py", line 394, in execute
    autoreload.check_errors(django.setup)()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/utils/autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
    ~~^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/apps/registry.py", line 116, in populate
    app_config.import_models()
    ~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/pcoder/projects/n/public-health-ch/venv/lib/python3.13/site-packages/django/apps/config.py", line 269, in import_models
    self.models_module = import_module(models_module_name)
                         ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/importlib/__init__.py", line 88, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/home/pcoder/projects/n/public-health-ch/publichealth/home/models/__init__.py", line 2, in <module>
    from .models import *
  File "/home/pcoder/projects/n/public-health-ch/publichealth/home/models/models.py", line 34, in <module>
    class ArticleIndexPage(Page):
    ...<112 lines>...
            verbose_name = "Rubrik"
  File "/home/pcoder/projects/n/public-health-ch/publichealth/home/models/models.py", line 111, in ArticleIndexPage
    ImageChooserPanel('header_image'),
    ^^^^^^^^^^^^^^^^^
NameError: name 'ImageChooserPanel' is not defined. Did you mean: 'ImageChooserBlock'?
2025-09-18 17:57:30 +05:30
PCoder
e58eaac2ca Fix psycopg2-binary version
lo64)" -DPSYCOPG_DEBUG=1 -DPG_VERSION_NUM=170005 -DHAVE_LO64=1 -DPSYCOPG_DEBUG=1 -I/home/pcoder/projects/n/public-health-ch/venv/include -I/usr/include/python3.13 -I. -I/usr/include -I/usr/include/postgresql/server -I/usr/include/libxml2 -c psycopg/utils.c -o build/temp.linux-x86_64-cpython-313/psycopg/utils.o -Wdeclaration-after-statement
      psycopg/utils.c: In function ‘psyco_is_main_interp’:
      psycopg/utils.c:397:12: error: implicit declaration of function ‘_PyInterpreterState_Get’; did you mean ‘PyInterpreterState_Get’? [-Wimplicit-function-declaration]
        397 |     return _PyInterpreterState_Get() == PyInterpreterState_Main();
            |            ^~~~~~~~~~~~~~~~~~~~~~~
            |            PyInterpreterState_Get
      psycopg/utils.c:397:38: warning: comparison between pointer and integer
        397 |     return _PyInterpreterState_Get() == PyInterpreterState_Main();
            |                                      ^~
      error: command '/usr/bin/gcc' failed with exit code 1
      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building wheel for psycopg2-binary
2025-09-18 17:43:20 +05:30
PCoder
8fd5ea553b Fix django-filter version requirements
INFO: pip is looking at multiple versions of wagtail to determine which version is compatible with other requirements. This could take a while.
ERROR: Cannot install -r requirements.txt (line 5) and django-filter>=24.2 because these package versions have conflicting dependencies.

The conflict is caused by:
    The user requested django-filter>=24.2
    wagtail 6.0.6 depends on django-filter<24 and >=23.3

To fix this you could try to:
1. loosen the range of package versions you've specified
2. remove package versions to allow pip to attempt to solve the dependency conflict

[notice] A new release of pip is available: 25.1.1 -> 25.2
[notice] To update, run: pip install --upgrade pip
ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflicts
2025-09-18 17:38:22 +05:30
PCoder
0b66f91fb7 Upgrade to Django 5.0 2025-09-18 17:30:36 +05:30
PCoder
a390a0218f Upgrades for Django 4.2 2025-09-18 17:13:25 +05:30
PCoder
69b2f3801d Add requirements-django4.txt 2025-09-18 16:41:53 +05:30
PCoder
5a41ab3cc2 Changes 2025-09-18 16:41:36 +05:30
app
20bf41652f Fix child menu translations 2025-06-10 18:03:07 +00:00
PCoder
2f8dd0c184 Update linkedin image 2025-04-18 14:15:16 +05:30
PCoder
430864fa68 Update linkedin 2025-04-18 14:06:54 +05:30
PCoder
3c38fb25e1 Add code to handle linkedin social 2025-04-18 13:34:56 +05:30
PCoder
59d1496808 Add linkedin image 2025-04-18 13:33:20 +05:30
PCoder
493626336f Translate sub menu item 2024-08-25 12:52:02 +05:30
f3ada439fa Merge pull request '12955-add-terms-link-footer' (#11) from 12955-add-terms-link-footer into master
Reviewed-on: #11
2024-07-19 14:43:53 +00:00
PCoder
b3b2a73fc9 Cleanup 2024-07-19 20:13:11 +05:30
PCoder
df467691b8 More reorganize 2024-07-19 20:09:23 +05:30
PCoder
2a6434e569 Simplify 2024-07-19 20:01:32 +05:30
PCoder
87332a9f05 Cleanup 2024-07-19 19:56:32 +05:30
PCoder
822fca63c3 Add add_item_to_page_queryset method 2024-07-19 16:21:47 +02:00
PCoder
9618ac0094 Cleanup and use correct page for EN 2024-07-19 17:50:38 +05:30
app
a7abe02784 Merging 2024-07-19 12:05:03 +00:00
app
454767b551 Fix footer menuitems translation 2024-07-19 11:58:23 +00:00
PCoder
99eb7c39fe Fix error instance 2024-07-19 16:20:19 +05:30
PCoder
780a4ce2d7 Fix bug footer menu items not translated 2024-07-19 14:04:15 +05:30
PCoder
31a0a6b060 Search hostname instead of site object 2024-07-19 14:03:53 +05:30
PCoder
a68dcb7574 Show terms and conditions link in the footer bottom 2024-07-19 13:38:15 +05:30
app
fc6f9e43a7 Merge remote-tracking branch 'origin/master' 2024-07-19 05:54:18 +00:00
PCoder
34ad67e8cd Add favicon-prosalute.ico 2024-06-17 17:37:28 +05:30
PCoder
cd99b1bc4f Check variable before accessing attribute 2024-06-04 17:07:38 +05:30
c5026ad0eb Merge pull request 'Dockerify pro-salute' (#10) from pro-salute into master
Reviewed-on: #10
2024-06-02 04:11:38 +00:00
PCoder
a3cdc6b58a Write DEBUG level logs to stdout 2024-05-31 19:09:01 +05:30
PCoder
b918903fcf Copy setup_libs.sh to /app 2024-05-31 19:08:30 +05:30
PCoder
5ba9ee52ea Create setup_libs.sh 2024-05-31 19:08:08 +05:30
PCoder
5c173f0ffb Revert back installing grunt-cli 2024-05-24 15:55:48 +05:30
PCoder
c88b94f20f Final changes 2024-05-23 17:05:21 +05:30
PCoder
c18fb883cc Revert back dev configuration 2024-05-22 22:25:54 +05:30
PCoder
d24fcd7d86 Merge remote-tracking branch 'origin/master' into pro-salute 2024-05-22 22:24:03 +05:30
PCoder
daf3a6baad Cleanup 2024-05-22 21:56:55 +05:30
PCoder
b5e786e098 Copy built static files to app 2024-05-22 21:55:41 +05:30
PCoder
c013387b03 Generate / copy staticfiles + node libraries 2024-05-22 21:55:04 +05:30
PCoder
aaf1fc1d9b Install yarn + tools 2024-05-22 21:52:23 +05:30
PCoder
1af8fe3767 Use RotatingFileHandler instead of ConcurrentLogHandler
Why?
  error: subprocess-exited-with-error

  × python setup.py egg_info did not run successfully.
  │ exit code: 1
  ╰─> [1 lines of output]
      error in ConcurrentLogHandler setup command: use_2to3 is invalid.
      [end of output]

Looks like the module is incompatible with the latest setuptools. The recommended solution on the Internet seems to be to downgrade setuptools to 57.5.0 ;; however, this still does not solve the issue for my setup
2024-05-22 21:50:56 +05:30
PCoder
0e0b464534 Add django-configurations requirement 2024-05-22 16:53:37 +05:30
PCoder
9863959d88 Add feedler module to the app folder 2024-05-17 19:06:50 +05:30
PCoder
6e835a10e6 Fix required changes 2024-05-15 11:01:32 +05:30
M.Ravi
4099b039f5 Add Dockerfile 2024-05-15 04:04:32 +00:00
M.Ravi
53ae04404b Changes on the nations server 2024-05-15 01:44:31 +00:00
app
e4a1cf5a77 Add migration 2023-05-16 11:49:32 +00:00
app
fdfebf783e Converting richtext stream to str 2023-05-16 11:49:02 +00:00
PCoder
fc03ea5927 Add richtext as a block 2023-05-16 16:11:05 +05:30
app
713f9ca9e3 Working version of adding html_content 2023-05-16 10:30:01 +00:00
app
0289586811 Add custom_filters : ph_html_decode 2023-05-16 10:28:45 +00:00
app
a7fdf769c4 Add migration home.0036_auto_20230515_1556 2023-05-15 13:56:56 +00:00
PCoder
1513c1a4d9 Merge remote-tracking branch 'ungleich/11683-add-spotify-mp3' into 11683-add-spotify-mp3 2023-05-15 19:25:08 +05:30
PCoder
5ddcf13803 New changes 2023-05-15 19:23:37 +05:30
app
9f621c9c3e Add migration home.0035_auto_20230515_1447 2023-05-15 12:48:46 +00:00
PCoder
bd2de38428 Add iframe block for article page 2023-05-15 18:14:06 +05:30
PCoder
79dbc2aa7f Remove title case in template 2022-10-28 19:45:46 +02:00
PCoder
3f46a5c25f Remove title case for menuitem title 2022-10-27 21:12:34 +05:30
d1eecee951 Merge pull request 'Introduce Original image format' (#6) from original-image-format into master
Reviewed-on: #6
2022-07-28 15:19:02 +00:00
PCoder
6818359e1a Remove unwanted code 2022-07-28 20:36:01 +05:30
PCoder
bdff34cf2b Introduce Original image format 2022-07-28 20:19:00 +05:30
app
8a87944e10 Update year in the footer 2022-07-21 14:53:51 +00:00
2c791e7fbc Merge pull request 'Remove styles to hide footer for sphc.ch' (#5) from 10751-add-footer-to-sphc into master
Reviewed-on: #5
2022-07-19 12:55:36 +00:00
d04a6fed60 Merge branch 'master' into 10751-add-footer-to-sphc 2022-07-19 12:54:48 +00:00
PCoder
e056b08609 Remove styles to hide footer for sphc.ch 2022-07-19 14:53:30 +02:00
app
ed9648d81d Add migration 2022-07-14 09:47:21 +00:00
PCoder
df9448e745 Update Changelog 2022-07-12 12:21:49 +05:30
9eaf497430 Merge pull request 'WIP 10694-table-block' (#3) from 10694-table-block into master
Reviewed-on: #3
2022-07-12 06:45:54 +00:00
PCoder
ae3a7d5ca9 Revert back changes to main.scss 2022-07-12 12:14:03 +05:30
PCoder
673ad6b924 Remove unwanted code 2022-07-12 12:11:17 +05:30
PCoder
5f2eedeac9 Merge branch 'master' into 10694-table-block 2022-07-12 12:06:53 +05:30
3946fff314 Merge pull request '10695-header-image' (#4) from 10695-header-image into master
Reviewed-on: #4
2022-07-12 06:32:58 +00:00
PCoder
8927460dd6 Fix accessing the header image's url 2022-07-11 15:04:51 +02:00
PCoder
3a1fa6d724 Use proper field for image 2022-07-11 18:18:55 +05:30
PCoder
849d539478 Add header_image to Article Index Page 2022-07-11 18:12:57 +05:30
PCoder
ae12f220ec Use program_table template for all language tables 2022-07-11 17:35:12 +05:30
PCoder
86242dbfb1 Add missing comma 2022-07-11 17:19:11 +05:30
PCoder
08ca710b99 Put styles into template for the moment 2022-07-11 17:16:20 +05:30
PCoder
78c3548e23 Use correct template path 2022-07-11 13:36:14 +02:00
PCoder
59356a783f Add program_table.html template 2022-07-11 16:51:55 +05:30
PCoder
f1aa2ad418 Use custom table template 2022-07-11 16:51:25 +05:30
PCoder
0691476d99 Cleanup unused code + format 2022-07-11 16:45:46 +05:30
PCoder
a6a00a33f3 Attempt classname for table 2022-07-11 16:21:11 +05:30
PCoder
c70b86a54b Revert back change in page_content 2022-07-11 16:16:57 +05:30
PCoder
6fa462dcf6 Minimal table style 2022-07-11 16:09:50 +05:30
PCoder
3ff5f1eb50 Working TableBlock (but it would be lot easier using a RawHTMLBlock) 2022-07-11 09:32:15 +02:00
PCoder
57e30e6217 WIP: dirty hack to add table to content (not working) 2022-07-07 11:27:38 +02:00
1abbc707f3 Merge pull request '10472/expiry-date-to-entry' (#2) from 10472/expiry-date-to-entry into master
Reviewed-on: #2
2022-05-04 08:32:29 +00:00
PCoder
7d11a440c6 Also filter entries by expire_at in the news section of the home page 2022-05-04 10:28:34 +05:30
PCoder
2d287dff32 Add migration 2022-05-03 15:43:49 +05:30
PCoder
4847b02c90 Add expire_at field to Entry model 2022-05-03 15:32:34 +05:30
PCoder
d1a0c930eb Fix style for newsletter 2022-02-07 13:09:43 +05:30
PCoder
2599af0d75 Merge remote-tracking branch 'ungleich/master' 2022-02-07 12:48:13 +05:30
PCoder
705ad68500 Add migration file 2022-02-07 12:44:04 +05:30
7e2c94fe52 Merge pull request 'Add functionality to show news_letter' (#1) from allow-newsletter into master
Reviewed-on: #1
2022-02-07 07:07:42 +00:00
PCoder
048faf479c Add functionality to show news_letter 2022-02-07 12:19:14 +05:30
PCoder
57503d1cfa Replace emails with info@ungleich.ch 2022-01-27 22:25:12 +05:30
PCoder
96679c0ab2 Merge remote-tracking branch 'upstream/master' into loleg-wagtail-2.15 2021-12-13 05:00:39 +01:00
60dbbff97d Merge branch 'master' into wagtail-2.15 2021-12-11 22:55:41 +01:00
5de116506d Merge branch 'master' of github.com:dataletsch/public-health-ch 2021-12-11 22:54:46 +01:00
68b13e06a8 Wagtail 2.15.1
psycopg2 2.9.2
puput 1.1.3
redis 5.1
crispy-forms 1.13
libsass 0.9
Pillow 8.4
2021-12-11 22:53:06 +01:00
67eefbd560 node-sass replaced with Dart Sass 2021-12-11 22:39:34 +01:00
f4afbda1a6
Merge pull request #48 from pcoder/enable-legacy-rich-text-wrapper
Enable legacy rich-text wrapper
2021-11-14 21:43:45 +01:00
65097a9841 Merge branch 'newsletters' into 'master'
Task #9790 Add newsletter signup button

See merge request ungleich-public/public-health-ch!2
2021-09-29 10:45:20 +02:00
0e3b52abc5 Task #9790 Add newsletter signup button 2021-09-29 10:40:34 +02:00
e847172599 Bumped charset, dotenv 2021-07-25 00:24:38 +02:00
b9b88e8235 Minor dependency update 2021-07-15 08:48:37 +02:00
Oleg Lavrovsky
adfeb8c7aa Minor library update 2021-07-01 08:54:57 +02:00
Oleg Lavrovsky
175dd9fd15 Added unidecode 2021-07-01 08:36:57 +02:00
84 changed files with 11542 additions and 2362 deletions

View file

@ -1,3 +1,29 @@
.git
.gitignore
.dockerignore
media
.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

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,7 +14,9 @@
/.keys/
/.vagrant/
/Vagrantfile.local
/docker-compose.yml
/docs/_build/
/node_modules/
/publichealth/static/libs
publichealth.home.json
.history

View file

@ -1,3 +1,8 @@
# 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

152
Dockerfile Normal file
View file

@ -0,0 +1,152 @@
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" = "<2.9"
"psycopg2-binary" = "*"
ConcurrentLogHandler = "*"
dj-database-url = "*"
django = "<2.3"
django = "<4"
django-anymail = "*"
django-colorful = "*"
django-crispy-forms = "*"
@ -30,6 +30,7 @@ Pillow = ">=4.0.0"
puput = ">=1.0.0"
python-dotenv = "*"
stellar = "*"
wagtail = ">=2.0,<2.13"
wagtail = ">=2.0,<2.16"
whitenoise = "*"
django-toolbelt = "*"
unidecode = "*"

760
Pipfile.lock generated

File diff suppressed because it is too large Load diff

138
deploy-minikube.sh Executable file
View file

@ -0,0 +1,138 @@
#!/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}"

16
entrypoint Executable file
View file

@ -0,0 +1,16 @@
#!/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.core',
'wagtail',
'wagtail.admin',
'wagtail.contrib.modeladmin',
'wagtail_modeladmin', # Separate package for modeladmin in Wagtail 5+
'wagtail.contrib.routable_page',
'wagtail.api.v2',

View file

@ -3,17 +3,18 @@
from django.db import models
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.conf.urls import url
from django.urls import re_path # django.conf.urls.url removed in Django 4.0
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _ # ugettext deprecated in Django 4.0
from django.shortcuts import redirect
from wagtail.admin import messages
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
# 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 feedler.models import Entry
from feedler.refresh import refresh_streams
@ -40,7 +41,10 @@ class RefreshAdminURLHelper(AdminURLHelper):
This helper constructs the different urls, to overwrite the default behaviour
and append the filters to the action.
"""
non_object_specific_actions = ('create', 'choose_parent', 'index', 'refresh')
def __init__(self, model, **kwargs):
super().__init__(model, **kwargs)
self.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)
@ -86,7 +90,7 @@ class EntryModelAdminMixin(object):
def get_admin_urls_for_registration(self):
urls = super().get_admin_urls_for_registration()
urls += (
url(
re_path(
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.core.models import Page
from wagtail.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.core.fields
import wagtail.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.core.fields.RichTextField(blank=True, default='')),
('intro', wagtail.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

@ -0,0 +1,18 @@
# 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,7 +7,9 @@ from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.core.mail import send_mail
from wagtail.contrib.settings.models import BaseSetting, register_setting
# 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 .models import Stream

View file

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
import datetime
from django.db import models
from django.utils import translation
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from wagtail.core.models import Page, Orderable
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.core.fields import RichTextField
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
class Stream(models.Model):
title = models.CharField(max_length=255)
@ -39,6 +40,11 @@ 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,
@ -68,7 +74,9 @@ class FeedPage(Page):
@property
def feedentries(self):
if self.stream:
entries = Entry.objects.filter(stream=self.stream)
entries = Entry.objects.filter(stream=self.stream).filter(
models.Q(expire_at__isnull=True) | models.Q(expire_at__gt=datetime.datetime.now())
)
else:
entries = Entry.objects.all()
# Filter out by chosen language

View file

@ -1,4 +1,4 @@
from django.conf.urls import include, url
from django.urls import include, re_path
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 = [
url(r'^api/v2/', api_router.urls),
re_path(r'^api/v2/', api_router.urls),
]

View file

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

34
k8s/cert-manager.yaml Normal file
View file

@ -0,0 +1,34 @@
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

93
k8s/deployment.yaml Normal file
View file

@ -0,0 +1,93 @@
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

38
k8s/ingress-nginx.yaml Normal file
View file

@ -0,0 +1,38 @@
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

67
k8s/postgres.yaml Normal file
View file

@ -0,0 +1,67 @@
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

38
k8s/redis.yaml Normal file
View file

@ -0,0 +1,38 @@
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

15
k8s/secrets-example.yaml Normal file
View file

@ -0,0 +1,15 @@
# 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"

21
k8s/storage.yaml Normal file
View file

@ -0,0 +1,21 @@
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 Normal file

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",
"node-sass": "^4.12.0"
"sass": "^1.45.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"

View file

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

View file

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

View file

View file

@ -0,0 +1,214 @@
"""
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.core.blocks
import wagtail.core.fields
import wagtail.blocks
import wagtail.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.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)),
('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)),
('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.core.fields.RichTextField(default=''),
field=wagtail.fields.RichTextField(default=''),
),
migrations.AddField(
model_name='homepage',
name='body_fr',
field=wagtail.core.fields.RichTextField(default=''),
field=wagtail.fields.RichTextField(default=''),
),
migrations.AddField(
model_name='homepage',
name='infos_de',
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),
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),
),
migrations.AddField(
model_name='homepage',
name='infos_fr',
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),
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),
),
migrations.AddField(
model_name='homepage',
name='intro_de',
field=wagtail.core.fields.RichTextField(default=''),
field=wagtail.fields.RichTextField(default=''),
),
migrations.AddField(
model_name='homepage',
name='intro_fr',
field=wagtail.core.fields.RichTextField(default=''),
field=wagtail.fields.RichTextField(default=''),
),
]

View file

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

View file

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

View file

@ -3,7 +3,7 @@
from __future__ import unicode_literals
from django.db import migrations
import wagtail.core.fields
import wagtail.fields
class Migration(migrations.Migration):
@ -16,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='homepage',
name='body_de',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
field=wagtail.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='homepage',
name='body_fr',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
field=wagtail.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.core.fields
import wagtail.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.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'')),
('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'')),
],
options={
'verbose_name': 'Formular',

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations, models
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
@ -23,22 +23,22 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='contactform',
name='intro_de',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
field=wagtail.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='contactform',
name='intro_fr',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
field=wagtail.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='contactform',
name='thanks_de',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
field=wagtail.fields.RichTextField(blank=True, default=''),
),
migrations.AlterField(
model_name='contactform',
name='thanks_fr',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
field=wagtail.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.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),
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),
),
migrations.AlterField(
model_name='homepage',
name='infos_fr',
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),
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),
),
]

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
@ -18,21 +18,21 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='articlepage',
name='body_de',
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),
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),
),
migrations.AlterField(
model_name='articlepage',
name='body_fr',
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),
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),
),
migrations.AlterField(
model_name='homepage',
name='infos_de',
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),
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),
),
migrations.AlterField(
model_name='homepage',
name='infos_fr',
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),
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),
),
]

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations, models
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
@ -22,17 +22,17 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='articlepage',
name='gallery',
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),
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),
),
migrations.AlterField(
model_name='articlepage',
name='body_de',
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),
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),
),
migrations.AlterField(
model_name='articlepage',
name='body_fr',
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),
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),
),
migrations.AlterField(
model_name='contactformfield',

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
@ -18,11 +18,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='articlepage',
name='body_de',
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),
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),
),
migrations.AlterField(
model_name='articlepage',
name='body_fr',
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),
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),
),
]

View file

@ -3,8 +3,8 @@
from __future__ import unicode_literals
from django.db import migrations, models
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='articleindexpage',
name='intro_en',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
field=wagtail.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.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),
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),
),
migrations.AddField(
model_name='articlepage',
name='intro_en',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
field=wagtail.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.core.fields.RichTextField(blank=True, default=''),
field=wagtail.fields.RichTextField(blank=True, default=''),
),
migrations.AddField(
model_name='homepage',
name='infos_en',
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),
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),
),
migrations.AddField(
model_name='homepage',
name='intro_en',
field=wagtail.core.fields.RichTextField(blank=True, default=''),
field=wagtail.fields.RichTextField(blank=True, default=''),
),
]

View file

@ -0,0 +1,29 @@
# 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

@ -0,0 +1,18 @@
# 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

@ -0,0 +1,33 @@
# 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

@ -0,0 +1,33 @@
# 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

@ -0,0 +1,37 @@
# 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

@ -0,0 +1,32 @@
# 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

@ -0,0 +1,36 @@
# 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

@ -0,0 +1,35 @@
# 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,3 +2,4 @@ from .forms import *
from .models import *
from .snippets import *
from .admin import *
from .image_formats import *

View file

@ -5,11 +5,13 @@ from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.core.mail import send_mail
from wagtail.contrib.settings.models import BaseSetting, register_setting
# 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
# A simple feedback module built into the site admin
@register_setting(icon='', classnames='icon icon-help')
@register_setting
class DataletsSettings(BaseSetting):
feedback_question = models.TextField(verbose_name='Name',
help_text='Who should we reply to for questions?', blank=True)
@ -36,8 +38,8 @@ def handle_save_settings(sender, instance, *args, **kwargs):
str(instance.feedback_status),
instance.feedback_question,
instance.feedback_comment,
), "wagtail@datalets.ch",
[ "support@datalets.ch" ]
), "info@ungleich.ch",
[ "info@ungleich.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.core.fields import RichTextField
from wagtail.fields import RichTextField
from wagtail.contrib.forms.models import (
AbstractEmailForm, AbstractFormField
)
from wagtail.admin.edit_handlers import (
from wagtail.admin.panels import (
FieldPanel, FieldRowPanel,
InlinePanel, MultiFieldPanel
)

View file

@ -0,0 +1,17 @@
# 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,21 +2,23 @@
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.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.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.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
# from puput.models import EntryPage, BlogPage # Commented: needs Wagtail 6 update
from feedler.models import Entry, Stream
from itertools import chain
@ -38,6 +40,13 @@ 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)
@ -47,6 +56,45 @@ 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,
@ -60,7 +108,21 @@ class ArticleIndexPage(Page):
FieldPanel('intro_fr'),
FieldPanel('title_en'),
FieldPanel('intro_en'),
ImageChooserPanel('feed_image'),
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"
)
]
def get_context(self, request):
@ -122,7 +184,8 @@ class ArticlePage(Page):
('info', InfoBlock(icon='help')),
('media', ChoiceBlock(choices=[
('gallery', 'Image gallery'),
], icon='media'))
], icon='media')),
('iframe', RichTextBlock())
], null=True, blank=True)
body_fr = StreamField([
('paragraph', RichTextBlock()),
@ -130,7 +193,8 @@ class ArticlePage(Page):
('info', InfoBlock(icon='help')),
('media', ChoiceBlock(choices=[
('gallery', 'Image gallery'),
], icon='media'))
], icon='media')),
('iframe', RichTextBlock())
], null=True, blank=True)
body_en = StreamField([
('paragraph', RichTextBlock()),
@ -138,7 +202,8 @@ class ArticlePage(Page):
('info', InfoBlock(icon='help')),
('media', ChoiceBlock(choices=[
('gallery', 'Image gallery'),
], icon='media'))
], icon='media')),
('iframe', RichTextBlock())
], null=True, blank=True)
trans_body = TranslatedField(
'body_de',
@ -175,23 +240,23 @@ class ArticlePage(Page):
FieldPanel('intro_de'),
], heading="Deutsch",
classname="collapsible collapsed"),
StreamFieldPanel('body_de'),
FieldPanel('body_de'), # StreamFieldPanel no longer needed in Wagtail 3+
MultiFieldPanel([
FieldPanel('title_fr'),
FieldPanel('intro_fr'),
], heading="Français",
classname="collapsible collapsed"),
StreamFieldPanel('body_fr'),
FieldPanel('body_fr'), # StreamFieldPanel no longer needed in Wagtail 3+
MultiFieldPanel([
FieldPanel('title_en'),
FieldPanel('intro_en'),
], heading="English",
classname="collapsible collapsed"),
StreamFieldPanel('body_en'),
FieldPanel('body_en'), # StreamFieldPanel no longer needed in Wagtail 3+
MultiFieldPanel([
ImageChooserPanel('feed_image'),
FieldPanel('feed_image'), # ImageChooserPanel no longer needed in Wagtail 3+
], heading="Images"),
StreamFieldPanel('gallery'),
FieldPanel('gallery'), # StreamFieldPanel no longer needed in Wagtail 3+
]
promote_panels = [
InlinePanel('related_links', label="Links"),
@ -257,19 +322,19 @@ class HomePage(Page):
MultiFieldPanel([
FieldPanel('intro_de', classname="full"),
FieldPanel('body_de', classname="full"),
StreamFieldPanel('infos_de'),
FieldPanel('infos_de'), # StreamFieldPanel no longer needed in Wagtail 3+
], heading="Deutsch",
classname="collapsible collapsed"),
MultiFieldPanel([
FieldPanel('intro_fr', classname="full"),
FieldPanel('body_fr', classname="full"),
StreamFieldPanel('infos_fr'),
FieldPanel('infos_fr'), # StreamFieldPanel no longer needed in Wagtail 3+
], heading="Français",
classname="collapsible collapsed"),
MultiFieldPanel([
FieldPanel('intro_en', classname="full"),
FieldPanel('body_en', classname="full"),
StreamFieldPanel('infos_en'),
FieldPanel('infos_en'), # StreamFieldPanel no longer needed in Wagtail 3+
], heading="English",
classname="collapsible collapsed"),
]
@ -287,19 +352,23 @@ class HomePage(Page):
@property
def blogentries(self):
# Get list of latest news
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]
# 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]
@property
def newsentries(self):
# Get the last few news entries for the home page
entries = Entry.objects.all().order_by('-published')
entries = Entry.objects.filter(
models.Q(expire_at__isnull=True) | models.Q(expire_at__gt=datetime.datetime.now())
).all().order_by('-published')
# Filter out by current language
curlang = translation.get_language()
if curlang in ['de']:

View file

@ -6,9 +6,8 @@ from django.db import models
from wagtail.snippets.models import register_snippet
from wagtail.core.models import Page
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.admin.edit_handlers import FieldPanel, PageChooserPanel
from wagtail.models import Page
from wagtail.admin.panels import FieldPanel
from .forms import ContactForm
from ..util import TranslatedField
@ -17,6 +16,7 @@ from ..util import TranslatedField
SOCIAL_NETWORK_SUPPORTED = (
('twitter', 'Twitter'),
('facebook', 'Facebook'),
('linkedin', 'LinkedIn'),
)
@register_snippet
@ -47,6 +47,8 @@ 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
@ -97,6 +99,15 @@ 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'),
@ -106,12 +117,19 @@ class Contact(models.Model):
FieldPanel('phone'),
FieldPanel('email'),
FieldPanel('www'),
ImageChooserPanel('logo'),
FieldPanel('logo'), # ImageChooserPanel no longer needed in Wagtail 3+
FieldPanel('color'),
FieldPanel('style'),
FieldPanel('map_url'),
FieldPanel('analytics'),
PageChooserPanel('contact_form', 'home.ContactForm'),
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'),
]
def phone_link(self):

View file

@ -0,0 +1,112 @@
"""
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 ugettext_lazy as _
from django.utils.translation import gettext_lazy as _ # ugettext_lazy deprecated in Django 4.0
from wagtail.core.models import Page
from wagtail.search.models import Query
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.contrib.routable_page.models import RoutablePageMixin, route
class HomePage(RoutablePageMixin, Page):

View file

@ -0,0 +1,140 @@
"""
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 wagtailcore_tags wagtailimages_tags %}
{% load i18n wagtailcore_tags wagtailimages_tags %}
{% block body_class %}template-{{ self.get_verbose_name|slugify }}{% endblock %}
@ -16,11 +16,25 @@
</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">
@ -31,6 +45,14 @@
</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 %}
{% load wagtailcore_tags wagtailimages_tags custom_filters %}
{% if page.feed_image %}
<div class="image">
@ -23,6 +23,10 @@
{% 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

@ -0,0 +1,53 @@
<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

@ -0,0 +1,49 @@
<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 puput_tags %}
{% load wagtailcore_tags wagtailimages_tags %}
<!-- Front page news -->
<section id="news">

View file

@ -0,0 +1,5 @@
{% 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.title|title }}</a></li>
<li><a href="{% pageurl child %}">{{ child.trans_title }}</a></li>
{% endfor %}
</ul>

View file

@ -35,11 +35,19 @@ 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,8 +1,9 @@
# -*- coding: utf-8 -*-
from django import template
from django.utils import translation
from django.db.models import Case, When
from wagtail.core.models import Site, Page
from wagtail.models import Site, Page
from ..models.forms import ContactForm
register = template.Library()
@ -54,19 +55,20 @@ 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)
menuitem.active = (calling_page.url.startswith(menuitem.url)
if calling_page else False)
try:
menuitem.title = menuitem.trans_title
except AttributeError:
pass
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
return {
'calling_page': calling_page,
'menuitems': menuitems,
'request': context['request'],
}
def menuitems_children(parent):
def menuitems_children(parent, context=None):
remove_mitglied = False
remove_devenez = False
if 'Qui sommes-nous' in parent.title:
@ -77,20 +79,45 @@ def menuitems_children(parent):
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)
except AttributeError:
menuitem.title = menuitem.trans_title
except AttributeError as ae:
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):
@ -105,6 +132,6 @@ def top_menu_children(context, parent):
def footer_menu(context, parent, calling_page=None):
return {
'calling_page': calling_page,
'menuitems': menuitems_children(parent),
'menuitems': menuitems_children(parent, context),
'request': context['request'],
}

View file

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

View file

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

View file

@ -1,6 +1,9 @@
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.
@ -40,9 +43,13 @@ 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']
@ -69,23 +76,16 @@ if 'MEDIA_DIR' in env:
MEDIA_ROOT = env['MEDIA_DIR']
# 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).
}
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),
}
}
# Email via ESP
@ -149,42 +149,46 @@ LOGGING = {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'stream': sys.stdout,
'formatter': 'simple',
},
},
'formatters': {
'default': {
'verbose': '[%(asctime)s] (%(process)d/%(thread)d) %(name)s %(levelname)s: %(message)s'
'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'
}
},
'loggers': {
'feedler': {
'handlers': [],
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
'formatter': 'verbose',
},
'publichealth': {
'handlers': [],
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
'formatter': 'verbose',
},
'wagtail': {
'handlers': [],
'handlers': ['console'],
'level': 'WARNING',
'propagate': False,
'formatter': 'verbose',
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'handlers': ['console', 'mail_admins'],
'level': 'WARNING',
'propagate': False,
'formatter': 'verbose',
},
'django.security': {
'handlers': ['mail_admins'],
'handlers': ['console', 'mail_admins'],
'level': 'ERROR',
'propagate': False,
'formatter': 'verbose',
},
},
}
@ -194,7 +198,7 @@ if 'LOG_DIR' in env:
# Public Health Schweiz log
LOGGING['handlers']['publichealth_file'] = {
'level': 'INFO',
'class': 'cloghandler.ConcurrentRotatingFileHandler',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(env['LOG_DIR'], 'publichealth.log'),
'maxBytes': 5242880, # 5MB
'backupCount': 5
@ -204,7 +208,7 @@ if 'LOG_DIR' in env:
# Wagtail log
LOGGING['handlers']['wagtail_file'] = {
'level': 'WARNING',
'class': 'cloghandler.ConcurrentRotatingFileHandler',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(env['LOG_DIR'], 'wagtail.log'),
'maxBytes': 5242880, # 5MB
'backupCount': 5
@ -214,7 +218,7 @@ if 'LOG_DIR' in env:
# Error log
LOGGING['handlers']['errors_file'] = {
'level': 'ERROR',
'class': 'cloghandler.ConcurrentRotatingFileHandler',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(env['LOG_DIR'], 'error.log'),
'maxBytes': 5242880, # 5MB
'backupCount': 5

View file

@ -128,5 +128,12 @@ $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,15 +58,28 @@ section {
}
// Responsive image/video classes
.rich-text img {
// Note: Wagtail 4.0+ removed the .rich-text wrapper div,
// so we target container elements directly.
.rich-text img,
.article-body img,
.lead img {
max-width: 100%;
height: auto;
}
.richtext-image.left{
float:left;
// 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.right{
float:right;
margin-left:auto;
}
.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,6 +49,7 @@
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
@ -61,3 +62,18 @@ 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.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

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@datalets.ch</b></p>
<p>In case of persistent issues, write to <b>info@ungleich.ch</b></p>
</body>
</html>

View file

@ -11,6 +11,9 @@
<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 %}
@ -35,7 +38,28 @@
<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,6 +24,7 @@
</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 -->
@ -31,7 +32,7 @@
<div class="copyright">
{% contact_name the_site=the_site %}
&copy; 2020
&copy; 2022
&bull; <a href="https://public-health.ch/privacy/">Privacy</a>
&bull; <a href="https://public-health.ch/impressum/">Impressum</a>
</div>

View file

@ -2,6 +2,9 @@
{% 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">
@ -27,9 +30,16 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<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>
<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>
</div>
{% block menu %}

View file

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

43
requirements-django4.txt Normal file
View file

@ -0,0 +1,43 @@
# 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,33 +1,54 @@
# Updated: 21.07.2021
# Updated: 2025-09-18 - Django 5.0 Upgrade
# Core
wagtail==2.12.5
elasticsearch>=5.0.0,<6.0.0
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
# Database
psycopg2-binary>=2.8.6,<2.9
dj-database-url==0.5.0
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
# Content
puput==1.1.1
# Note: puput needs update or replacement for Wagtail 6.x
# puput==2.0.0
guess-language-spirit==0.5.3
# Caching
django-redis==5.0.0
django-redis==5.4.0 # Compatible with Django 5.0
redis==5.0.8
# Frontend
django-anymail==8.4
django-anymail==11.0 # Updated for Django 5.0
django-colorful==1.3
django-contrib-comments==2.1.0
django-crispy-forms==1.12.0
django-contrib-comments==2.2.0 # May need testing with Django 5.0
django-crispy-forms==2.3 # Updated for Django 5.0
django-el-pagination==3.3.0
django-libsass==0.8
django-social-share==2.2.1
libsass==0.21.0
Pillow==8.2.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+
# Runtime dependencies
gunicorn
whitenoise
python-dotenv
ConcurrentLogHandler
#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

18
setup_libs.sh Executable file
View file

@ -0,0 +1,18 @@
#!/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

3001
yarn.lock

File diff suppressed because it is too large Load diff