Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend_addon/copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ author_email:
help: "Author email"
default: "dev@plone.org"

github_organization:
type: str
help: "GitHub organization or user for project URLs"
default: "{{ package_name.split('.')[:-1] | join('.') or 'collective' }}"

# Computed values - package folder structure
package_folder:
type: str
Expand Down
3 changes: 1 addition & 2 deletions backend_addon/template/.gitignore.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ coverage.xml
.pytype/
.ruff_cache/

# Translations
# Translations (compiled catalogs are generated; .pot/.po are tracked)
*.mo
*.pot

# Environments
.env
Expand Down
14 changes: 10 additions & 4 deletions backend_addon/template/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ test = [
]

[project.urls]
Homepage = "https://github.com/collective/{{ package_name }}"
Documentation = "https://github.com/collective/{{ package_name }}"
Repository = "https://github.com/collective/{{ package_name }}.git"
Issues = "https://github.com/collective/{{ package_name }}/issues"
Homepage = "https://github.com/{{ github_organization }}/{{ package_name }}"
Documentation = "https://github.com/{{ github_organization }}/{{ package_name }}"
Repository = "https://github.com/{{ github_organization }}/{{ package_name }}.git"
Issues = "https://github.com/{{ github_organization }}/{{ package_name }}/issues"

[project.entry-points."z3c.autoinclude.plugin"]
target = "plone"
Expand Down Expand Up @@ -99,8 +99,14 @@ ignore = ["E731"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]
# Locale update helper shells out to fixed translation tools (i18ndude/msginit).
"src/**/locales/update.py" = ["S603", "S607"]

[tool.ruff.lint.isort]
# Match the single-import-per-line, alphabetical layout the templates emit.
force-single-line = true
order-by-type = false
lines-after-imports = 2
known-first-party = ["{{ package_name.split('.')[0] }}"]

[tool.ruff.format]
Expand Down
5 changes: 5 additions & 0 deletions backend_addon/template/src/{{package_folder}}/i18n.py.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Message factory for the {{ package_name }} i18n domain."""
from zope.i18nmessageid import MessageFactory


_ = MessageFactory("{{ package_name }}")
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Usage:
python update.py
"""

import glob
import os
import subprocess
from importlib.resources import files
Expand All @@ -17,7 +18,7 @@ locale_path = target_path + "locales/"
i18ndude = "i18ndude"

# ignore node_modules files resulting in errors
excludes = '"*.html *json-schema*.xml"'
excludes = "*.html *json-schema*.xml"


def locale_folder_setup():
Expand All @@ -29,25 +30,38 @@ def locale_folder_setup():
continue
lc_messages_path = lang + "/LC_MESSAGES/"
os.mkdir(lc_messages_path)
cmd = f"msginit --locale={lang} --input={domain}.pot --output={lang}/LC_MESSAGES/{domain}.po"
subprocess.call(cmd, shell=True)
subprocess.call(
[
"msginit",
f"--locale={lang}",
f"--input={domain}.pot",
f"--output={lang}/LC_MESSAGES/{domain}.po",
]
)
os.chdir("../../../../")


def _rebuild():
cmd = (
f"{i18ndude} rebuild-pot --pot {locale_path}/{domain}.pot "
f"--exclude {excludes} --create {domain} {target_path}"
subprocess.call(
[
i18ndude,
"rebuild-pot",
"--pot",
f"{locale_path}/{domain}.pot",
"--exclude",
excludes,
"--create",
domain,
target_path,
]
)
subprocess.call(cmd, shell=True)


def _sync():
cmd = (
f"{i18ndude} sync --pot {locale_path}/{domain}.pot "
f"{locale_path}*/LC_MESSAGES/{domain}.po"
po_files = glob.glob(f"{locale_path}*/LC_MESSAGES/{domain}.po")
subprocess.call(
[i18ndude, "sync", "--pot", f"{locale_path}/{domain}.pot", *po_files]
)
subprocess.call(cmd, shell=True)


def update_locale():
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""Testing setup for {{ package_name }}."""
import os

import plone.app.theming
import plone.restapi
from plone.app.testing import FunctionalTesting
from plone.app.testing import IntegrationTesting
from plone.app.testing import PloneSandboxLayer
from plone.app.testing import SITE_OWNER_NAME
from plone.app.testing import SITE_OWNER_PASSWORD
from plone.testing.zope import WSGI_SERVER_FIXTURE

import plone.app.theming
import plone.restapi

import {{ package_name }}


Expand All @@ -17,6 +18,8 @@ class {{ package_name.replace('.', ' ').title().replace(' ', '') }}Layer(PloneSa

def setUpZope(self, app, configurationContext):
"""Set up Zope."""
# Compile .po -> .mo so add-on translations load during tests.
os.environ.setdefault("zope_i18n_compile_mo_files", "true")
self.loadZCML(package=plone.app.theming)
self.loadZCML(package=plone.restapi)
self.loadZCML(package={{ package_name }})
Expand Down
7 changes: 3 additions & 4 deletions backend_addon/template/tests/test_setup.py.jinja
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Test {{ package_name }} installation."""
import pytest

from plone import api
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
Expand All @@ -20,11 +19,11 @@ class TestSetup:

def test_browserlayer(self):
"""Test browserlayer is registered."""
from plone.browserlayer import utils
# Add actual browserlayer check if you have one
# Add an actual browserlayer check if your addon registers one, e.g.:
# from plone.browserlayer import utils
# from {{ package_name }}.interfaces import I{{ package_name.replace('.', ' ').title().replace(' ', '') }}Layer
# assert I{{ package_name.replace('.', ' ').title().replace(' ', '') }}Layer in utils.registered_layers()
pass
assert True


class TestUninstall:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""{{ behavior_name }} behavior."""
from plone.autoform.interfaces import IFormFieldProvider
from plone.supermodel import model
from zope import schema
from zope.interface import Interface
from zope.interface import provider
{%- if behavior_factory %}
from zope.interface import implementer
from plone.dexterity.interfaces import IDexterityContent
{%- endif %}
from plone.supermodel import model
{%- if behavior_factory %}
from zope.component import adapter
from zope.interface import implementer
{%- endif %}
{%- if behavior_marker %}
from zope.interface import Interface
{%- endif %}
from zope.interface import provider


{%- if behavior_marker %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Tests for {{ behavior_interface }} behavior."""
import pytest

from plone.behavior.interfaces import IBehavior
from zope.component import getUtility

Expand All @@ -20,7 +19,7 @@ class TestBehavior{{ behavior_class }}:
"""Test behavior is registered."""
behavior = getUtility(
IBehavior,
name="{{ package_name }}.{{ behavior_module }}",
name="{{ package_name }}.behaviors.{{ behavior_module }}.{{ behavior_interface }}",
)
assert behavior is not None
assert behavior.title
Expand All @@ -30,7 +29,7 @@ class TestBehavior{{ behavior_class }}:
"""Test behavior marker interface."""
behavior = getUtility(
IBehavior,
name="{{ package_name }}.{{ behavior_module }}",
name="{{ package_name }}.behaviors.{{ behavior_module }}.{{ behavior_interface }}",
)
from {{ package_name }}.behaviors.{{ behavior_module }} import {{ behavior_interface }}Marker
assert behavior.marker == {{ behavior_interface }}Marker
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""{{ content_type_name }} content type."""
from plone.dexterity.content import {{ content_type_base }}
from plone.supermodel import model
from zope import schema
from zope.interface import implementer


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Tests for {{ content_type_name }} content type."""
import pytest

from plone import api
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
Expand All @@ -19,6 +18,19 @@ class TestContentType{{ content_type_class }}:
def _setup(self, integration):
self.portal = integration["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
{%- if global_allow %}
# Globally addable: create directly in the portal root.
self.container = self.portal
{%- else %}
# Not globally addable: create a {{ parent_content_type_resolved }}
# parent that allows this type, and add inside it.
self.container = api.content.create(
container=self.portal,
type="{{ parent_content_type_resolved }}",
id="parent-container",
title="Parent Container",
)
{%- endif %}

def test_fti(self):
"""Test FTI is installed."""
Expand All @@ -38,10 +50,8 @@ class TestContentType{{ content_type_class }}:

def test_factory(self):
"""Test content factory."""
fti = queryUtility(IDexterityFTI, name="{{ content_type_portal_type }}")
factory = fti.factory
obj = api.content.create(
container=self.portal,
container=self.container,
type="{{ content_type_portal_type }}",
id="test-{{ content_type_module }}",
title="Test {{ content_type_name }}",
Expand All @@ -52,7 +62,7 @@ class TestContentType{{ content_type_class }}:
def test_adding(self):
"""Test content can be added."""
obj = api.content.create(
container=self.portal,
container=self.container,
type="{{ content_type_portal_type }}",
id="test-{{ content_type_module }}",
title="Test {{ content_type_name }}",
Expand All @@ -62,15 +72,24 @@ class TestContentType{{ content_type_class }}:
def test_deleting(self):
"""Test content can be deleted."""
obj = api.content.create(
container=self.portal,
container=self.container,
type="{{ content_type_portal_type }}",
id="test-{{ content_type_module }}",
title="Test {{ content_type_name }}",
)
api.content.delete(obj=obj)
assert "test-{{ content_type_module }}" not in self.portal.objectIds()
assert "test-{{ content_type_module }}" not in self.container.objectIds()

def test_global_allow(self):
"""Test content type global_allow setting."""
fti = queryUtility(IDexterityFTI, name="{{ content_type_portal_type }}")
assert fti.global_allow is {{ global_allow | string | title }}
{%- if not global_allow %}

def test_addable_in_parent(self):
"""Test the type is addable inside its {{ parent_content_type_resolved }} parent."""
addable = [
fti.getId() for fti in self.container.allowedContentTypes()
]
assert "{{ content_type_portal_type }}" in addable
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Tests for {{ controlpanel_name }} control panel."""
import pytest

from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID

Expand Down
1 change: 0 additions & 1 deletion form/template/tests/test_form_{{form_module}}.py.jinja
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Tests for {{ form_class_name }} form."""
import pytest

from plone import api
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Tests for {{ indexer_name }} indexer."""
import pytest

from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID

Expand Down
58 changes: 58 additions & 0 deletions language/copier.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
_min_copier_version: "9.0.0"
_subdirectory: template
_answers_file: ".copier-answers.language-{{ language_code }}.yml"

_plonecli:
type: sub
parent: backend_addon
description: "Add a translation language (locale .po catalog) to the addon"

_tasks:
- command:
- "uv"
- "run"
- "--no-project"
- "--with"
- "tomlkit"
- "python3"
- "{{ _copier_conf.src_path }}/copier_hooks.py"
- "validate"
- "{{ _copier_conf.dst_path }}"
when: "{{ _copier_conf.operation == 'copy' }}"
- command:
- "uv"
- "run"
- "--no-project"
- "--with"
- "tomlkit"
- "python3"
- "{{ _copier_conf.src_path }}/copier_hooks.py"
- "post_copy"
- "{{ _copier_conf.dst_path }}"
- "{{ language_code }}"

# Language to add
language_code:
type: str
help: "Language code (ISO 639-1, e.g. 'de', 'fr', 'es')"
validator: "{% if not language_code %}Language code is required{% endif %}"

language_name:
type: str
help: "Human-readable language name (e.g. 'German')"
default: "{{ language_code }}"

# Package info - read from parent addon
package_name:
type: str
help: "Parent addon package name"

package_folder:
type: str
help: "Parent addon package folder (e.g., collective/mypackage)"
default: "{{ package_name | replace('.', '/') }}"

current_date:
type: str
default: "{{ '%Y-%m-%d' | strftime }}"
when: false
Loading
Loading