Repository: collective/collective.blog (language: python)

Issue:
Rename Blog content type
We have to rename the "Blog" content type because we should be able to name the blog "Blog" so the URL can be _localhost:3000/blog_.


Discussion / hints:
Suggestion: rename it from Blog -> BlogFolder

Fix patch:
diff --git a/news/42.bug b/news/42.bug
new file mode 100644
index 0000000..0b8955d
--- /dev/null
+++ b/news/42.bug
@@ -0,0 +1,1 @@
+Rename Blog fti to BlogFolder, allowing contents with id blog to be added [@ericof]
diff --git a/src/collective/blog/profiles/default/metadata.xml b/src/collective/blog/profiles/default/metadata.xml
index 5065c17..1eab898 100644
--- a/src/collective/blog/profiles/default/metadata.xml
+++ b/src/collective/blog/profiles/default/metadata.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <metadata>
-  <version>1001</version>
+  <version>1002</version>
   <dependencies>
     <!-- <dependency>profile-plone.volto:default</dependency> -->
   </dependencies>
diff --git a/src/collective/blog/profiles/default/types.xml b/src/collective/blog/profiles/default/types.xml
index e76d0ec..6eb5e9f 100644
--- a/src/collective/blog/profiles/default/types.xml
+++ b/src/collective/blog/profiles/default/types.xml
@@ -7,6 +7,10 @@
   />
   <object meta_type="Dexterity FTI"
           name="Blog"
+          remove="True"
+  />
+  <object meta_type="Dexterity FTI"
+          name="BlogFolder"
   />
   <object meta_type="Dexterity FTI"
           name="Post"
diff --git a/src/collective/blog/profiles/default/types/Blog.xml b/src/collective/blog/profiles/default/types/BlogFolder.xml
similarity index 96%
rename from src/collective/blog/profiles/default/types/Blog.xml
rename to src/collective/blog/profiles/default/types/BlogFolder.xml
index 66c156d..206f57c 100644
--- a/src/collective/blog/profiles/default/types/Blog.xml
+++ b/src/collective/blog/profiles/default/types/BlogFolder.xml
@@ -1,20 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
 <object xmlns:i18n="http://xml.zope.org/namespaces/i18n"
         meta_type="Dexterity FTI"
-        name="Blog"
+        name="BlogFolder"
         i18n:domain="collective.blog"
 >
 
   <!-- Basic properties -->
   <property name="title"
             i18n:translate=""
-  >Blog</property>
+  >BlogFolder</property>
   <property name="description"
             i18n:translate=""
   >A folderish content item representing a Blog</property>
 
   <property name="allow_discussion">False</property>
-  <property name="factory">Blog</property>
+  <property name="factory">BlogFolder</property>
   <property name="icon_expr" />
   <property name="link_target" />
 
@@ -53,7 +53,7 @@
   </property>
 
   <!-- View information -->
-  <property name="add_view_expr">string:${folder_url}/++add++Blog</property>
+  <property name="add_view_expr">string:${folder_url}/++add++BlogFolder</property>
   <property name="immediate_view">view</property>
   <property name="default_view">view</property>
   <property name="default_view_fallback">False</property>
diff --git a/src/collective/blog/upgrades/configure.zcml b/src/collective/blog/upgrades/configure.zcml
index a95ad45..ad2eee4 100644
--- a/src/collective/blog/upgrades/configure.zcml
+++ b/src/collective/blog/upgrades/configure.zcml
@@ -14,5 +14,20 @@
         />
   </genericsetup:upgradeSteps>
 
+  <genericsetup:upgradeSteps
+      profile="collective.blog:default"
+      source="1001"
+      destination="1002"
+      >
+    <genericsetup:upgradeDepends
+        title="Rename Blog fti as BlogFolder"
+        import_steps="typeinfo"
+        />
+    <genericsetup:upgradeStep
+        title="Update old Blog objects with new portal_type"
+        handler=".v1002.recatalog_portal_type"
+        />
+  </genericsetup:upgradeSteps>
+
 
 </configure>
diff --git a/src/collective/blog/upgrades/v1002.py b/src/collective/blog/upgrades/v1002.py
new file mode 100644
index 0000000..fdf282c
--- /dev/null
+++ b/src/collective/blog/upgrades/v1002.py
@@ -0,0 +1,33 @@
+from collective.blog import logger
+from plone import api
+from Products.GenericSetup.tool import SetupTool
+
+
+TYPES_MAPPING = (("Blog", "BlogFolder"),)
+
+
+def _migrate_existing_content(old_pt: str, new_pt: str) -> int:
+    """Update existing content items."""
+    with api.env.adopt_roles(["Manager"]):
+        # Search catalog as Manager
+        brains = api.content.find(portal_type=old_pt)
+        total = len(brains)
+        logger.info(f"Convert {total} {old_pt} instances to {new_pt}")
+        for brain in brains:
+            obj = brain.getObject()
+            obj.portal_type = new_pt
+            obj.reindexObject(
+                idxs=[
+                    "portal_type",
+                    "Type",
+                ],
+            )
+        return total
+
+
+def recatalog_portal_type(setup_tool: SetupTool):
+    """Recatalog portal type for existing content items."""
+    for old_pt, new_pt in TYPES_MAPPING:
+        items = _migrate_existing_content(old_pt, new_pt)
+        logger.info(f"Converted {items} {old_pt} instances")
+    logger.info("Upgrade complete")


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 1fba462..7e8baa4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -56,14 +56,14 @@ def all_content() -> list:
         },
         {
             "_container": "/",
-            "type": "Blog",
+            "type": "BlogFolder",
             "id": "tech-blog",
             "title": "Awesome Tech Blog",
             "description": "My awesome technical blog",
         },
         {
             "_container": "/",
-            "type": "Blog",
+            "type": "BlogFolder",
             "id": "dumpster",
             "title": "General",
             "description": "Posts about life, universe and everything",
@@ -106,7 +106,7 @@ def all_content() -> list:
 @pytest.fixture
 def blogs_payload(all_content, filter_items) -> list:
     """Payload to create two blogs items."""
-    return filter_items(all_content, "Blog", True)
+    return filter_items(all_content, "BlogFolder", True)
 
 
 @pytest.fixture
diff --git a/tests/content/test_blog.py b/tests/content/test_blog.py
index 0eb5ced..e556a84 100644
--- a/tests/content/test_blog.py
+++ b/tests/content/test_blog.py
@@ -1,5 +1,6 @@
 from AccessControl.PermissionRole import rolesForPermissionOn
 from collective.blog.content.blog import Blog
+from copy import deepcopy
 from plone import api
 from plone.dexterity.content import DexterityContent
 from plone.dexterity.fti import DexterityFTI
@@ -8,7 +9,7 @@
 import pytest
 
 
-CONTENT_TYPE = "Blog"
+CONTENT_TYPE = "BlogFolder"
 
 
 @pytest.fixture
@@ -75,6 +76,24 @@ def test_create(self, blogs_payload):
         assert content.portal_type == CONTENT_TYPE
         assert isinstance(content, Blog)
 
+    @pytest.mark.parametrize(
+        "content_id,expected",
+        [
+            ("blog", "blog"),
+            ("a-blog", "a-blog"),
+            ("blog_name", "blog_name"),
+            ("blog-folder", "blog-folder"),
+            ("blog_folder", "blog_folder"),
+            ("blogfolder", "blogfolder"),
+        ],
+    )
+    def test_create_valid_ids(self, blogs_payload, content_id, expected):
+        payload = deepcopy(blogs_payload[0])
+        payload["id"] = content_id
+        with api.env.adopt_roles(["Manager"]):
+            content = api.content.create(container=self.portal, **payload)
+        assert content.id == expected
+
     def test_blog_info_behavior_applied(self, blog):
         from collective.blog.behaviors.blog import IBlogInfo
 
diff --git a/tests/setup/test_setup_install.py b/tests/setup/test_setup_install.py
index 91ec7c6..a102c6f 100644
--- a/tests/setup/test_setup_install.py
+++ b/tests/setup/test_setup_install.py
@@ -14,4 +14,4 @@ def test_browserlayer(self, browser_layers):
 
     def test_latest_version(self, profile_last_version):
         """Test latest version of default profile."""
-        assert profile_last_version(f"{PACKAGE_NAME}:default") == "1001"
+        assert profile_last_version(f"{PACKAGE_NAME}:default") == "1002"
diff --git a/tests/setup/upgrades/conftest.py b/tests/setup/upgrades/conftest.py
new file mode 100644
index 0000000..e83b607
--- /dev/null
+++ b/tests/setup/upgrades/conftest.py
@@ -0,0 +1,29 @@
+from collective.blog import PACKAGE_NAME
+from plone import api
+
+import pytest
+
+
+PROFILE_ID = f"profile-{PACKAGE_NAME}:default"
+
+
+@pytest.fixture
+def do_rollback(portal):
+    """Rollback profile config."""
+
+    def func(src: str):
+        with api.env.adopt_roles(["Manager"]):
+            setup_tool = portal.portal_setup
+            setup_tool.setLastVersionForProfile(PROFILE_ID, src)
+
+    return func
+
+
+@pytest.fixture
+def do_upgrade(portal):
+    def func(dest: str):
+        with api.env.adopt_roles(["Manager"]):
+            setup_tool = portal.portal_setup
+            setup_tool.upgradeProfile(PROFILE_ID, dest=dest)
+
+    return func
diff --git a/tests/setup/upgrades/test_upgrades_to_v1002.py b/tests/setup/upgrades/test_upgrades_to_v1002.py
new file mode 100644
index 0000000..7852591
--- /dev/null
+++ b/tests/setup/upgrades/test_upgrades_to_v1002.py
@@ -0,0 +1,69 @@
+from collective.blog.content.blog import Blog
+from collective.blog.upgrades.v1002 import TYPES_MAPPING
+from copy import deepcopy
+from plone import api
+from plone.dexterity.fti import DexterityFTI
+
+import pytest
+
+
+@pytest.fixture
+def modify_ftis(portal, get_fti):
+    """Modify fti"""
+
+    def func():
+        with api.env.adopt_roles(["Manager"]):
+            # Rename new content type back to its old name
+            types_tool = portal.portal_types
+            for old_pt, new_pt in TYPES_MAPPING:
+                types_tool.manage_renameObject(new_pt, old_pt)
+                fti = get_fti(old_pt)
+                fti.title = old_pt
+
+    return func
+
+
+@pytest.fixture
+def old_blog_factory(portal, blogs_payload):
+    def func() -> Blog:
+        with api.env.adopt_roles(["Manager"]):
+            payload = deepcopy(blogs_payload[0])
+            payload["type"] = "Blog"
+            payload["id"] = "old-blog"
+            return api.content.create(portal, **payload)
+
+    return func
+
+
+class TestUpgradeV1002:
+    src: str = "1001"
+    dest: str = "1002"
+
+    @pytest.fixture(autouse=True)
+    def _init(self, get_fti, do_rollback, do_upgrade, modify_ftis, old_blog_factory):
+        do_rollback(self.src)
+        modify_ftis()
+        self.get_fti = get_fti
+        self.old_blog = old_blog_factory()
+        self.upgrade = do_upgrade
+
+    def test_blog_fti_available_in_1001(self):
+        assert isinstance(self.get_fti("Blog"), DexterityFTI)
+        assert self.get_fti("BlogFolder") is None
+
+    def test_blogfolder_fti_available_in_1002(self, do_upgrade):
+        do_upgrade(self.dest)
+        assert isinstance(self.get_fti("BlogFolder"), DexterityFTI)
+        assert self.get_fti("Blog") is None
+
+    def test_old_blogs_migrated(self, do_upgrade):
+        with api.env.adopt_roles(["Manager"]):
+            brains = api.content.find(portal_type="BlogFolder")
+        assert len(brains) == 0
+        # Upgrade
+        do_upgrade(self.dest)
+        with api.env.adopt_roles(["Manager"]):
+            brains = api.content.find(portal_type="BlogFolder")
+        assert len(brains) == 1
+        assert self.old_blog.portal_type == "BlogFolder"
+        assert self.old_blog.Type() == "BlogFolder"