From cc6a7899093236060f9d7574f31009971e1507ed Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 12 Jun 2026 21:07:27 +0200 Subject: [PATCH 1/6] feat: Support options for djangocms command in django CMS 5.1 --- .github/workflows/test_startcmsproject.yml | 36 ++++ djangocms_install_rules.json | 71 ++++++++ djangocms_install_rules.schema.v1.json | 196 +++++++++++++++++++++ project_name/settings.py-tpl | 25 ++- project_name/urls.py-tpl | 4 +- requirements.in | 7 +- 6 files changed, 331 insertions(+), 8 deletions(-) create mode 100644 djangocms_install_rules.json create mode 100644 djangocms_install_rules.schema.v1.json diff --git a/.github/workflows/test_startcmsproject.yml b/.github/workflows/test_startcmsproject.yml index 5652695..950e07f 100644 --- a/.github/workflows/test_startcmsproject.yml +++ b/.github/workflows/test_startcmsproject.yml @@ -7,6 +7,42 @@ concurrency: cancel-in-progress: true jobs: + validate-install-rules: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.13' + - name: Validate djangocms_install_rules.json against its schema + run: | + python -m pip install jsonschema + python << 'EOF' + import json + import os + import sys + import jsonschema + + with open("djangocms_install_rules.json") as f: + doc = json.load(f) + + schema_file = os.path.basename(doc.get("$schema", "")) + if not schema_file: + sys.exit("djangocms_install_rules.json has no $schema reference") + if not os.path.exists(schema_file): + sys.exit(f"Referenced schema {schema_file} not found in repository") + + with open(schema_file) as f: + schema = json.load(f) + + jsonschema.Draft7Validator.check_schema(schema) + errors = list(jsonschema.Draft7Validator(schema).iter_errors(doc)) + for error in errors: + print(f"ERROR at {list(error.absolute_path)}: {error.message}") + sys.exit(1 if errors else 0) + EOF + create-project: runs-on: ${{ matrix.os }} strategy: diff --git a/djangocms_install_rules.json b/djangocms_install_rules.json new file mode 100644 index 0000000..8863672 --- /dev/null +++ b/djangocms_install_rules.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://raw.githubusercontent.com/django-cms/cms-template/main/djangocms_install_rules.schema.v1.json", + "comment": "Rules used by `djangocms .` to add django CMS to an existing project. Fetched from the cms-template repository (branch matching the installed django CMS major.minor), with this file as the bundled fallback. Each `when` condition may contain a `flag` (truthy command option) and/or a `mode` (list of matching --mode values). For installed_apps and middleware a rule may also carry a `before` or `after` anchor (an existing entry) for positional insertion; otherwise items are appended.", + "installed_apps": [ + {"items": ["djangocms_simple_admin_style"], "before": "django.contrib.admin"}, + { + "items": [ + "django.contrib.sites", + "cms", + "menus", + "treebeard", + "sekizai", + "filer", + "easy_thumbnails", + "djangocms_frontend", + "djangocms_text", + "djangocms_link" + ] + }, + {"items": ["djangocms_versioning"], "when": {"flag": "versioning"}}, + {"items": ["djangocms_moderation"], "when": {"flag": "moderation"}}, + {"items": ["djangocms_alias"], "when": {"flag": "alias"}}, + {"items": ["djangocms_stories", "taggit", "taggit_autosuggest"], "when": {"flag": "stories"}}, + {"items": ["rest_framework", "djangocms_rest"], "when": {"mode": ["headless", "hybrid"]}} + ], + "middleware": [ + { + "items": ["django.middleware.locale.LocaleMiddleware"], + "after": "django.contrib.sessions.middleware.SessionMiddleware" + }, + { + "items": [ + "cms.middleware.user.CurrentUserMiddleware", + "cms.middleware.page.CurrentPageMiddleware", + "cms.middleware.toolbar.ToolbarMiddleware", + "cms.middleware.language.LanguageCookieMiddleware" + ] + } + ], + "context_processors": [ + { + "items": [ + "django.template.context_processors.i18n", + "sekizai.context_processors.sekizai", + "cms.context_processors.cms_settings" + ] + } + ], + "settings": [ + {"name": "SITE_ID", "snippet": "SITE_ID = 1"}, + {"name": "X_FRAME_OPTIONS", "snippet": "X_FRAME_OPTIONS = \"SAMEORIGIN\""}, + {"name": "CMS_CONFIRM_VERSION4", "snippet": "CMS_CONFIRM_VERSION4 = True"}, + {"name": "LANGUAGES", "snippet": "LANGUAGES = [\n (\"{language_code}\", \"{language_name}\"),\n]"}, + {"name": "CMS_TEMPLATES", "snippet": "CMS_TEMPLATES = [\n (\"cms-base.html\", \"Default\"),\n]", "when": {"mode": ["traditional", "hybrid"]}}, + {"name": "CMS_PLACEHOLDERS", "snippet": "CMS_PLACEHOLDERS = [\n (\"cms-base.html\", (\"content\",), \"Content\"),\n]", "when": {"mode": ["headless"]}} + ], + "urls": [ + {"pattern": "path(\"api/\", include(\"djangocms_rest.urls\"))", "when": {"mode": ["headless", "hybrid"]}}, + {"pattern": "path(\"taggit_autosuggest/\", include(\"taggit_autosuggest.urls\"))", "when": {"flag": "stories"}}, + {"pattern": "path(\"\", include(\"cms.urls\"))", "when": {"mode": ["traditional", "hybrid"]}} + ], + "packages": { + "filer": "django-filer" + }, + "template_dir": { + "path": "templates", + "base_template": "cms-base.html", + "base_template_content": "{% extends \"bootstrap5/base.html\" %}{# Replace this with your CMS base template1 #}\n", + "when": {"mode": ["traditional", "hybrid"]} + } +} diff --git a/djangocms_install_rules.schema.v1.json b/djangocms_install_rules.schema.v1.json new file mode 100644 index 0000000..d5034a6 --- /dev/null +++ b/djangocms_install_rules.schema.v1.json @@ -0,0 +1,196 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/django-cms/cms-template/main/djangocms_install_rules.schema.v1.json", + "title": "django CMS install rules", + "description": "Rules used by `djangocms .` to add django CMS to an existing Django project. Fetched from the cms-template repository (branch matching the installed django CMS major.minor), with the bundled file as fallback.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string" + }, + "comment": { + "type": "string", + "description": "Free-form documentation; ignored by the installer." + }, + "installed_apps": { + "type": "array", + "description": "Apps to add to INSTALLED_APPS. Items are appended unless a before/after anchor is given.", + "items": { + "$ref": "#/definitions/anchoredRule" + } + }, + "middleware": { + "type": "array", + "description": "Middleware to add to MIDDLEWARE. Items are appended unless a before/after anchor is given.", + "items": { + "$ref": "#/definitions/anchoredRule" + } + }, + "context_processors": { + "type": "array", + "description": "Context processors to append to the template OPTIONS.", + "items": { + "$ref": "#/definitions/itemsRule" + } + }, + "settings": { + "type": "array", + "description": "Settings snippets to append to settings.py if the named setting is not already defined.", + "items": { + "$ref": "#/definitions/settingRule" + } + }, + "urls": { + "type": "array", + "description": "URL patterns to add to the project urlpatterns.", + "items": { + "$ref": "#/definitions/urlRule" + } + }, + "packages": { + "type": "object", + "description": "Maps an installed app or module name to its pip requirement, where the two differ.", + "additionalProperties": { + "type": "string" + } + }, + "template_dir": { + "type": "object", + "description": "Template directory to create, with an optional base template to seed it.", + "additionalProperties": false, + "required": ["path"], + "properties": { + "path": { + "type": "string", + "description": "Directory (relative to the project root) to create and register in TEMPLATES['DIRS']." + }, + "base_template": { + "type": "string", + "description": "Filename of the base template to create inside the template directory." + }, + "base_template_content": { + "type": "string", + "description": "Initial content of the base template." + }, + "when": { + "$ref": "#/definitions/when" + }, + "comment": { + "type": "string" + } + } + } + }, + "definitions": { + "when": { + "type": "object", + "description": "Condition for applying a rule. All given criteria must match.", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "flag": { + "type": "string", + "description": "Name of a command option that must be truthy (e.g. \"versioning\" for --versioning)." + }, + "mode": { + "type": "array", + "description": "The rule applies if the --mode value is one of these.", + "minItems": 1, + "items": { + "enum": ["traditional", "headless", "hybrid"] + } + } + } + }, + "itemsRule": { + "type": "object", + "additionalProperties": false, + "required": ["items"], + "properties": { + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "when": { + "$ref": "#/definitions/when" + }, + "comment": { + "type": "string" + } + } + }, + "anchoredRule": { + "type": "object", + "additionalProperties": false, + "required": ["items"], + "not": { + "required": ["before", "after"] + }, + "properties": { + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "before": { + "type": "string", + "description": "Existing entry before which the items are inserted." + }, + "after": { + "type": "string", + "description": "Existing entry after which the items are inserted." + }, + "when": { + "$ref": "#/definitions/when" + }, + "comment": { + "type": "string" + } + } + }, + "settingRule": { + "type": "object", + "additionalProperties": false, + "required": ["name", "snippet"], + "properties": { + "name": { + "type": "string", + "description": "Setting name; the snippet is only added if this setting is not already defined." + }, + "snippet": { + "type": "string", + "description": "Python code appended to settings.py. May contain {language_code}/{language_name} placeholders." + }, + "when": { + "$ref": "#/definitions/when" + }, + "comment": { + "type": "string" + } + } + }, + "urlRule": { + "type": "object", + "additionalProperties": false, + "required": ["pattern"], + "properties": { + "pattern": { + "type": "string", + "description": "Python expression for the URL pattern, e.g. path(\"\", include(\"cms.urls\"))." + }, + "when": { + "$ref": "#/definitions/when" + }, + "comment": { + "type": "string" + } + } + } + } +} diff --git a/project_name/settings.py-tpl b/project_name/settings.py-tpl index 255f352..65eedf7 100644 --- a/project_name/settings.py-tpl +++ b/project_name/settings.py-tpl @@ -51,10 +51,19 @@ INSTALLED_APPS = [ 'menus', 'djangocms_text', - 'djangocms_link', - 'djangocms_alias', - 'djangocms_versioning', - + 'djangocms_link',{% if alias %} + 'djangocms_alias',{% endif %}{% if versioning %} + 'djangocms_versioning',{% endif %}{% if moderation %} + 'djangocms_moderation',{% endif %} +{% if stories %} + 'djangocms_stories', + 'taggit', + 'taggit-autosuggest', +{% endif %} +{% if mode == "headless" or mode == "hybrid" %} + 'djangocms_rest', + 'rest_framework', +{% endif %} 'sekizai', 'treebeard', 'parler', @@ -197,12 +206,18 @@ CMS_CONFIRM_VERSION4 = True SITE_ID = 1 -# A base template is part of this setup +{% if mode == "headless" %}# A setting to define which placeholders are used + +CMS_PLACEHOLDERS = ( + ("base.html", ("content", "Content")) +) +{% else %}# A base template is part of this setup # https://docs.django-cms.org/en/{% if cms_docs_version %}release-{{ cms_docs_version }}.x{% else %}latest{% endif %}/reference/configuration.html#cms-templates CMS_TEMPLATES = ( ("base.html", _("Standard")), ) +{% endif %} # Enable permissions # https://docs.django-cms.org/en/{% if cms_docs_version %}release-{{ cms_docs_version }}.x{% else %}latest{% endif %}/topics/permissions.html diff --git a/project_name/urls.py-tpl b/project_name/urls.py-tpl index 76b4f7b..dc6febc 100644 --- a/project_name/urls.py-tpl +++ b/project_name/urls.py-tpl @@ -24,7 +24,9 @@ from django.views.i18n import JavaScriptCatalog urlpatterns = i18n_patterns( path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'), path('admin/', admin.site.urls), - path('filer/', include('filer.urls')), + path('filer/', include('filer.urls')),{% if stories %} + path('taggit_autosuggest', include('taggit_autosuggest.urls')){% endif %}{% if mode == "hybrid" or mode == "headless" %} + path('api', include("djangocms_rest.urls"))),{% endif %}{% if mode == "hybrid" or mode == "traditional" %} path('', include('cms.urls')), ) diff --git a/requirements.in b/requirements.in index 7b6de60..2eaea7c 100644 --- a/requirements.in +++ b/requirements.in @@ -1,7 +1,10 @@ -djangocms-versioning -djangocms-alias djangocms-frontend>=2.0.0a1 django-filer djangocms-text django-fsm<3 djangocms-simple-admin-style +{% if versioning %}djangocms-versioning +{% endif %}{% if alias %}djangocms-alias +{% endif %}{% if moderation %}djangocms-moderation +{% endir %}{% if mode == "headless" or mode == "hybrid" %}djangocms-rest +{% endif %} From 3adf03b0020b1f33e59b6f4a09974ce8cf4a8caf Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 12 Jun 2026 21:09:13 +0200 Subject: [PATCH 2/6] fix: Add missing endif --- project_name/urls.py-tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project_name/urls.py-tpl b/project_name/urls.py-tpl index dc6febc..9d53af8 100644 --- a/project_name/urls.py-tpl +++ b/project_name/urls.py-tpl @@ -27,7 +27,7 @@ urlpatterns = i18n_patterns( path('filer/', include('filer.urls')),{% if stories %} path('taggit_autosuggest', include('taggit_autosuggest.urls')){% endif %}{% if mode == "hybrid" or mode == "headless" %} path('api', include("djangocms_rest.urls"))),{% endif %}{% if mode == "hybrid" or mode == "traditional" %} - path('', include('cms.urls')), + path('', include('cms.urls')),{% endif %} ) From 2db5fbd3b2cb889cd7b45c2f0f2338dec90ee2f8 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 12 Jun 2026 21:10:49 +0200 Subject: [PATCH 3/6] fix: test action --- .github/workflows/test_startcmsproject.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_startcmsproject.yml b/.github/workflows/test_startcmsproject.yml index 950e07f..7a37325 100644 --- a/.github/workflows/test_startcmsproject.yml +++ b/.github/workflows/test_startcmsproject.yml @@ -69,4 +69,4 @@ jobs: source ./.venv/bin/activate python -m pip install --upgrade pip python -m pip install Django~=${{ matrix.django-version }} django-cms - djangocms mysite --noinput --template https://github.com/${{ github.repository }}/archive/${{ github.head_ref || github.ref_name }}.zip + djangocms mysite --noinput --name requirements.in --template https://github.com/${{ github.repository }}/archive/${{ github.head_ref || github.ref_name }}.zip From 2f1373d7122ab371cdf18eabeb9824263fd907df Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 12 Jun 2026 21:12:18 +0200 Subject: [PATCH 4/6] fix: typo --- requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.in b/requirements.in index 2eaea7c..14050fe 100644 --- a/requirements.in +++ b/requirements.in @@ -6,5 +6,5 @@ djangocms-simple-admin-style {% if versioning %}djangocms-versioning {% endif %}{% if alias %}djangocms-alias {% endif %}{% if moderation %}djangocms-moderation -{% endir %}{% if mode == "headless" or mode == "hybrid" %}djangocms-rest +{% endif %}{% if mode == "headless" or mode == "hybrid" %}djangocms-rest {% endif %} From 6738a19036f37a1f9e71103101b40041c7bea216 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 12 Jun 2026 21:13:46 +0200 Subject: [PATCH 5/6] fix: Add missing parler --- requirements.in | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.in b/requirements.in index 14050fe..36d9d91 100644 --- a/requirements.in +++ b/requirements.in @@ -3,6 +3,7 @@ django-filer djangocms-text django-fsm<3 djangocms-simple-admin-style +django-parler {% if versioning %}djangocms-versioning {% endif %}{% if alias %}djangocms-alias {% endif %}{% if moderation %}djangocms-moderation From ba31bed32fba01f2102fd00791e299daec7d47ee Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 12 Jun 2026 21:16:35 +0200 Subject: [PATCH 6/6] fix: Update requirements.in --- project_name/settings.py-tpl | 2 +- requirements.in | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project_name/settings.py-tpl b/project_name/settings.py-tpl index 65eedf7..f4ba003 100644 --- a/project_name/settings.py-tpl +++ b/project_name/settings.py-tpl @@ -59,6 +59,7 @@ INSTALLED_APPS = [ 'djangocms_stories', 'taggit', 'taggit-autosuggest', + 'parler', {% endif %} {% if mode == "headless" or mode == "hybrid" %} 'djangocms_rest', @@ -66,7 +67,6 @@ INSTALLED_APPS = [ {% endif %} 'sekizai', 'treebeard', - 'parler', 'filer', 'easy_thumbnails', diff --git a/requirements.in b/requirements.in index 36d9d91..c2567f7 100644 --- a/requirements.in +++ b/requirements.in @@ -3,9 +3,9 @@ django-filer djangocms-text django-fsm<3 djangocms-simple-admin-style -django-parler -{% if versioning %}djangocms-versioning +django-parler{% if versioning %}djangocms-versioning {% endif %}{% if alias %}djangocms-alias {% endif %}{% if moderation %}djangocms-moderation {% endif %}{% if mode == "headless" or mode == "hybrid" %}djangocms-rest +{% endif %}{% if stories %}djangocms-stories {% endif %}