diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index c5b90e8..b247ce9 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -6,11 +6,11 @@ on: python-version: required: false type: string - default: "3.12" + default: "3.13" plone-version: required: false type: string - default: "6.1.2" + default: "6.2.0" outputs: backend: description: "Flag reporting if we should run the backend jobs" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e3c3028..09d22eb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,8 +24,14 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.13", "3.12", "3.11", "3.10"] - plone-version: ["6.1-latest", "6.0-latest"] + python-version: ["3.14", "3.13", "3.12", "3.11", "3.10"] + plone-version: ["6.2-latest", "6.1-latest", "6.0-latest"] + exclude: + # Python 3.14 requires Plone 6.2 or newer + - python-version: "3.14" + plone-version: "6.1-latest" + - python-version: "3.14" + plone-version: "6.0-latest" with: python-version: ${{ matrix.python-version }} plone-version: ${{ matrix.plone-version }} diff --git a/Makefile b/Makefile index 19201a4..1b7676a 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ BACKEND_FOLDER=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) ifdef PLONE_VERSION PLONE_VERSION := $(PLONE_VERSION) else -PLONE_VERSION := 6.1.2 +PLONE_VERSION := 6.2.0 endif PLONE_SITE_ID?=Plone diff --git a/news/20.feature b/news/20.feature new file mode 100644 index 0000000..c789c4b --- /dev/null +++ b/news/20.feature @@ -0,0 +1 @@ +Added support for Plone 6.2 and Python 3.14. @ericof diff --git a/pyproject.toml b/pyproject.toml index f7d8db0..ae27a48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ classifiers = [ "Framework :: Plone", "Framework :: Plone :: 6.0", "Framework :: Plone :: 6.1", + "Framework :: Plone :: 6.2", "Framework :: Plone :: Addon", "Framework :: Zope", "Framework :: Zope :: 5", @@ -28,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ "Products.CMFPlone>=6.0.0", @@ -44,7 +46,7 @@ test = [ "plone.app.testing", "plone.restapi[test]", "pytest-cov", - "pytest-plone>=1.0.0a1", + "pytest-plone>=1.0.0", "zest-releaser[recommended]>=9.5.0", "zestreleaser-towncrier>=1.3.0", ] diff --git a/tests/behaviors/conftest.py b/tests/behaviors/conftest.py deleted file mode 100644 index 220d717..0000000 --- a/tests/behaviors/conftest.py +++ /dev/null @@ -1,7 +0,0 @@ -import pytest - - -@pytest.fixture() -def additional_profiles() -> list[str]: - """List of additional profiles to apply on top of the default testing profile.""" - return [] diff --git a/tests/conftest.py b/tests/conftest.py index 30264db..d2b8d1d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,19 +39,8 @@ def roles_vocab(): @pytest.fixture() -def additional_profiles() -> list[str]: - """List of additional profiles to apply on top of the default testing profile.""" - return [ - "collective.person:demo", - ] - - -@pytest.fixture() -def portal(integration, roles_vocab, additional_profiles): +def portal(integration, roles_vocab): portal = integration["portal"] - setup_tool = api.portal.get_tool("portal_setup") - for profile in additional_profiles: - setup_tool.runAllImportStepsFromProfile(profile) with api.env.adopt_user(SITE_OWNER_NAME): # Set registry for roles api.portal.set_registry_record("person.roles", roles_vocab) @@ -85,9 +74,7 @@ def persons_payload() -> list: def persons(portal, persons_payload) -> dict: """Create Person content items.""" response = {} - with api.env.adopt_roles([ - "Manager", - ]): + with api.env.adopt_roles(["Manager"]): for data in persons_payload: content = api.content.create(container=portal, **data) response[content.UID()] = content.title diff --git a/tests/content/conftest.py b/tests/content/conftest.py new file mode 100644 index 0000000..e18691a --- /dev/null +++ b/tests/content/conftest.py @@ -0,0 +1,14 @@ +from plone.dexterity.fti import DexterityFTI +from plone.dexterity.interfaces import IDexterityFTI +from zope.component import queryUtility + +import pytest + + +@pytest.fixture(scope="class") +def get_fti(portal_class): + def get_fti(name: str) -> DexterityFTI: + """Get the Factory Type Information for a type by name.""" + return queryUtility(IDexterityFTI, name=name) + + return get_fti diff --git a/tests/content/test_person.py b/tests/content/test_person.py index 1ded61c..58d94bf 100644 --- a/tests/content/test_person.py +++ b/tests/content/test_person.py @@ -9,14 +9,11 @@ CONTENT_TYPE = "Person" -class TestPerson: +class TestPersonFTI: @pytest.fixture(autouse=True) - def _fti(self, get_fti, integration): + def _setup(self, get_fti): self.fti = get_fti(CONTENT_TYPE) - def test_fti(self): - assert isinstance(self.fti, DexterityFTI) - def test_factory(self): factory = self.fti.factory obj = createObject(factory) @@ -24,21 +21,49 @@ def test_factory(self): assert isinstance(obj, Person) @pytest.mark.parametrize( - "behavior", + "attr,expected", [ - "collective.person.person", - "collective.contact_behaviors.contact_info", - "volto.blocks.editable.layout", - "plone.namefromtitle", - "plone.shortname", - "plone.excludefromnavigation", - "plone.relateditems", - "plone.versioning", - "plone.leadimage", + ("title", "Person"), + ("description", ""), + ("global_allow", True), + ("filter_content_types", True), + ( + "allowed_content_types", + ( + "File", + "Image", + ), + ), + ("add_permission", "collective.person.person.add"), + ("klass", "collective.person.content.person.Person"), + ( + "behaviors", + ( + "collective.person.person", + "collective.contact_behaviors.contact_info", + "volto.blocks.editable.layout", + "plone.namefromtitle", + "plone.shortname", + "plone.leadimage", + "plone.excludefromnavigation", + "plone.relateditems", + "plone.versioning", + ), + ), ], ) - def test_has_behavior(self, get_behaviors, behavior): - assert behavior in get_behaviors(CONTENT_TYPE) + def test_fti(self, attr: str, expected): + """Test FTI values.""" + fti: DexterityFTI = self.fti + + assert isinstance(fti, DexterityFTI) + assert getattr(fti, attr) == expected + + +class TestPerson: + @pytest.fixture(autouse=True) + def _setup(self, portal): + self.portal = portal def test_create(self, portal, persons_payload): payload = persons_payload[0] diff --git a/tests/indexing/conftest.py b/tests/indexing/conftest.py deleted file mode 100644 index e3aec50..0000000 --- a/tests/indexing/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - - -@pytest.fixture() -def additional_profiles() -> list[str]: - """List of additional profiles to apply on top of the default testing profile.""" - return [ - "collective.person:demo", - "collective.person:catalog", - ] diff --git a/tests/indexing/test_catalog_metadata.py b/tests/indexing/test_catalog_metadata.py index 6b0554b..18ea1ac 100644 --- a/tests/indexing/test_catalog_metadata.py +++ b/tests/indexing/test_catalog_metadata.py @@ -4,10 +4,16 @@ import pytest +@pytest.mark.portal( + profiles=[ + "collective.person:demo", + "collective.person:catalog", + ], +) class TestCatalogMetadata: @pytest.fixture(autouse=True) - def _setup(self, portal): - self.portal = portal + def _setup(self, portal_class): + self.portal = portal_class @pytest.mark.parametrize( "query,column,expected", diff --git a/tests/restapi/conftest.py b/tests/restapi/conftest.py index b85be67..7fca82a 100644 --- a/tests/restapi/conftest.py +++ b/tests/restapi/conftest.py @@ -1,7 +1,5 @@ from plone import api from plone.app.testing import SITE_OWNER_NAME -from plone.app.testing import SITE_OWNER_PASSWORD -from plone.restapi.testing import RelativeSession import pytest import transaction @@ -31,59 +29,19 @@ def roles_vocab(): def _create_content(portal, payload): """Create a content in the root of the portal.""" - return api.content.create(container=portal, **payload) - - -def _publish_content(content): - """Publish a content.""" + content = api.content.create(container=portal, **payload) api.content.transition(content, transition="publish") + return content @pytest.fixture() -def app(functional): - return functional["app"] - - -@pytest.fixture() -def portal(functional, persons_payload, roles_vocab): +def functional_portal(functional, persons_payload, roles_vocab): portal = functional["portal"] with api.env.adopt_user(SITE_OWNER_NAME): # Set registry for roles api.portal.set_registry_record("person.roles", roles_vocab) # Create Content - content = _create_content(portal, persons_payload[0]) - with api.env.adopt_user(SITE_OWNER_NAME): - # Publish Content - _publish_content(content) + _create_content(portal, persons_payload[0]) transaction.commit() return portal - - -@pytest.fixture() -def http_request(functional): - return functional["request"] - - -@pytest.fixture() -def request_factory(portal): - def factory(): - url = portal.absolute_url() - api_session = RelativeSession(url) - api_session.headers.update({"Accept": "application/json"}) - return api_session - - return factory - - -@pytest.fixture() -def anon_request(request_factory): - return request_factory() - - -@pytest.fixture() -def manager_request(request_factory): - request = request_factory() - request.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) - yield request - request.auth = ()