Repository: SFDO-Tooling/CumulusCI
Pull request #3364 (2022-09-20 00:17:22)

Add task set_object_settings to enable and disable object settings
New home for #3335

# Changes

New Metadata ETL task set_object_settings to enable and disable settings on standard and custom objects


prescod: @jlantz Can you look at the lint errors please?

Patch:
diff --git a/.gitignore b/.gitignore
index 64c0b4931..f6ad1a69d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,5 @@ pabot_results/
 .pabotsuitenames
 large_cassettes/
 github_release_notes.html
-tmp
\ No newline at end of file
+tmp
+pytestdebug.log
diff --git a/cumulusci/cumulusci.yml b/cumulusci/cumulusci.yml
index dc8bcfb76..d97addcfc 100644
--- a/cumulusci/cumulusci.yml
+++ b/cumulusci/cumulusci.yml
@@ -552,6 +552,10 @@ tasks:
         class_path: cumulusci.tasks.metadata_etl.SetDuplicateRuleStatus
         options:
             namespace_inject: $project_config.project__package__namespace
+    set_object_settings:
+        description: Enable and disable object level settings on standard and custom objects
+        class_path: cumulusci.tasks.metadata_etl.SetObjectSettings
+        group: Metadata Transformations
     set_organization_wide_defaults:
         group: Metadata Transformations
         description: Sets the Organization-Wide Defaults for specific sObjects, and waits for sharing recalculation to complete.
diff --git a/cumulusci/tasks/metadata_etl/__init__.py b/cumulusci/tasks/metadata_etl/__init__.py
index 267a0bd3e..f4e97b31b 100644
--- a/cumulusci/tasks/metadata_etl/__init__.py
+++ b/cumulusci/tasks/metadata_etl/__init__.py
@@ -8,6 +8,7 @@ from cumulusci.tasks.metadata_etl.base import (
 )
 from cumulusci.tasks.metadata_etl.duplicate_rules import SetDuplicateRuleStatus
 from cumulusci.tasks.metadata_etl.layouts import AddRelatedLists
+from cumulusci.tasks.metadata_etl.objects import SetObjectSettings
 from cumulusci.tasks.metadata_etl.permissions import AddPermissionSetPermissions
 from cumulusci.tasks.metadata_etl.value_sets import AddValueSetEntries
 from cumulusci.tasks.metadata_etl.sharing import SetOrgWideDefaults
@@ -20,6 +21,7 @@ flake8 = (
     AddRelatedLists,
     AddPermissionSetPermissions,
     AddValueSetEntries,
+    SetObjectSettings,
     SetOrgWideDefaults,
     MetadataOperation,
     SetDuplicateRuleStatus,
diff --git a/cumulusci/tasks/metadata_etl/objects.py b/cumulusci/tasks/metadata_etl/objects.py
new file mode 100644
index 000000000..c4f7e33f3
--- /dev/null
+++ b/cumulusci/tasks/metadata_etl/objects.py
@@ -0,0 +1,68 @@
+from cumulusci.core.exceptions import TaskOptionsError
+from cumulusci.core.utils import process_list_arg
+from cumulusci.tasks.metadata_etl import MetadataSingleEntityTransformTask
+from cumulusci.utils.xml.metadata_tree import MetadataElement
+
+OBJ_SETTINGS = [
+    "Activities",
+    "BulkApi",
+    "Feeds",
+    "History",
+    "Licensing",
+    "Reports",
+    "Search",
+    "Sharing",
+    "StreamingApi",
+]
+OBJ_SETTINGS_STR = ", ".join(OBJ_SETTINGS)
+
+
+class SetObjectSettings(MetadataSingleEntityTransformTask):
+    entity = "CustomObject"
+    task_options = {
+        "enable": {
+            "description": f"Array of object settings to enable. Uses the setting name.  Available values: {OBJ_SETTINGS_STR}"
+        },
+        "disable": {
+            "description": f"Array of object settings to disable. Uses the setting name.  Available values: {OBJ_SETTINGS_STR}"
+        },
+        **MetadataSingleEntityTransformTask.task_options,
+    }
+
+    def _init_options(self, kwargs):
+        super()._init_options(kwargs)
+        if not self.options.get("enable") and not self.options.get("disable"):
+            raise TaskOptionsError(
+                "You must provide values for either 'enable' or 'disable'"
+            )
+        self.options["enable"] = self._process_settings(self.options.get("enable"))
+        self.options["disable"] = self._process_settings(self.options.get("disable"))
+
+    def _process_settings(self, settings):
+        settings = process_list_arg(settings)
+        if settings is None:
+            settings = []
+        invalid = []
+        for setting in settings:
+            if setting not in OBJ_SETTINGS:
+                invalid.append(setting)
+        if invalid:
+            invalid_settings = ", ".join(invalid)
+            raise TaskOptionsError(
+                f"Invalid settings: {invalid_settings}.  Valid settings are: {OBJ_SETTINGS_STR}"
+            )
+        return settings
+
+    def _transform_entity(self, metadata: MetadataElement, api_name: str):
+        for setting in self.options["enable"]:
+            self._apply_setting(metadata, setting, True)
+        for setting in self.options["disable"]:
+            self._apply_setting(metadata, setting, False)
+        return metadata
+
+    def _apply_setting(self, metadata: MetadataElement, setting: str, enable: bool):
+        name = f"enable{setting}"
+        elem = metadata.find(name)
+        if elem is None:
+            elem = metadata.append(name)
+        elem.text = "true" if enable else "false"


Test patch:
diff --git a/cumulusci/tasks/metadata_etl/tests/test_objects.py b/cumulusci/tasks/metadata_etl/tests/test_objects.py
new file mode 100644
index 000000000..483602bbe
--- /dev/null
+++ b/cumulusci/tasks/metadata_etl/tests/test_objects.py
@@ -0,0 +1,236 @@
+import pytest
+
+from cumulusci.core.exceptions import TaskOptionsError
+from cumulusci.tasks.metadata_etl import SetObjectSettings
+from cumulusci.tasks.salesforce.tests.util import create_task
+from cumulusci.utils.xml import metadata_tree
+
+MD = "{%s}" % metadata_tree.METADATA_NAMESPACE
+
+CUSTOMOBJECT_SETTINGS_XML = b"""<?xml version="1.0" encoding="UTF-8"?>
+<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
+    <sharingModel>Read</sharingModel>
+    <externalSharingModel>Read</externalSharingModel>
+    <label>Test</label>
+    <pluralLabel>Tests</pluralLabel>
+    <nameField>
+        <label>Test Name</label>
+        <trackHistory>false</trackHistory>
+        <type>Text</type>
+    </nameField>
+    <deploymentStatus>Deployed</deploymentStatus>
+    <enableActivities>true</enableActivities>
+    <enableBulkApi>true</enableBulkApi>
+    <enableFeeds>false</enableFeeds>
+    <enableHistory>false</enableHistory>
+</CustomObject>"""
+
+
+class TestSetObjectSettings:
+    def test_options__require_enable_or_disable(self):
+        with pytest.raises(TaskOptionsError) as e:
+            create_task(
+                SetObjectSettings,
+                {
+                    "api_names": "Test__c",
+                },
+            )
+        assert (
+            e.value.args[0]
+            == "You must provide values for either 'enable' or 'disable'"
+        )
+
+    def test_options__invalid_setting(self):
+        with pytest.raises(TaskOptionsError) as e:
+            create_task(
+                SetObjectSettings,
+                {
+                    "api_names": "Test__c",
+                    "enable": ["Foo"],
+                },
+            )
+        assert e.value.args[0].startswith("Invalid settings: Foo.")
+
+    def test_options__invalid_settings(self):
+        with pytest.raises(TaskOptionsError) as e:
+            create_task(
+                SetObjectSettings,
+                {
+                    "api_names": "Test__c",
+                    "enable": ["Foo", "Bar"],
+                },
+            )
+        assert e.value.args[0].startswith("Invalid settings: Foo, Bar.")
+
+    def test_enable__single(self):
+        task = create_task(
+            SetObjectSettings,
+            {
+                "api_names": "Test__c",
+                "enable": ["Feeds"],
+            },
+        )
+        tree = metadata_tree.fromstring(CUSTOMOBJECT_SETTINGS_XML)
+
+        result = task._transform_entity(tree, "Test__c")
+
+        entry = result._element.findall(f".//{MD}enableFeeds")
+        assert len(entry) == 1
+        assert entry[0].text == "true"
+
+        entry = result._element.findall(f".//{MD}enableHistory")
+        assert len(entry) == 1
+        assert entry[0].text == "false"
+
+    def test_enable__single_missing(self):
+        task = create_task(
+            SetObjectSettings,
+            {
+                "api_names": "Test__c",
+                "enable": ["Sharing"],
+            },
+        )
+        tree = metadata_tree.fromstring(CUSTOMOBJECT_SETTINGS_XML)
+
+        result = task._transform_entity(tree, "Test__c")
+
+        entry = result._element.findall(f".//{MD}enableSharing")
+        assert len(entry) == 1
+        assert entry[0].text == "true"
+
+    def test_enable__no_change(self):
+        task = create_task(
+            SetObjectSettings,
+            {
+                "api_names": "Test__c",
+                "enable": ["Activities"],
+            },
+        )
+        tree = metadata_tree.fromstring(CUSTOMOBJECT_SETTINGS_XML)
+
+        result = task._transform_entity(tree, "Test__c")
+
+        entry = result._element.findall(f".//{MD}enableActivities")
+        assert len(entry) == 1
+        assert entry[0].text == "true"
+
+    def test_enable__multiple(self):
+        task = create_task(
+            SetObjectSettings,
+            {
+                "api_names": "Test__c",
+                "enable": ["Feeds", "History"],
+            },
+        )
+        tree = metadata_tree.fromstring(CUSTOMOBJECT_SETTINGS_XML)
+
+        result = task._transform_entity(tree, "Test__c")
+
+        entry = result._element.findall(f".//{MD}enableFeeds")
+        assert len(entry) == 1
+        assert entry[0].text == "true"
+
+        entry = result._element.findall(f".//{MD}enableHistory")
+        assert len(entry) == 1
+        assert entry[0].text == "true"
+
+    def test_disable__single(self):
+        task = create_task(
+            SetObjectSettings,
+            {
+                "api_names": "Test__c",
+                "disable": ["Activities"],
+            },
+        )
+        tree = metadata_tree.fromstring(CUSTOMOBJECT_SETTINGS_XML)
+
+        result = task._transform_entity(tree, "Test__c")
+
+        entry = result._element.findall(f".//{MD}enableActivities")
+        assert len(entry) == 1
+        assert entry[0].text == "false"
+
+        entry = result._element.findall(f".//{MD}enableBulkApi")
+        assert len(entry) == 1
+        assert entry[0].text == "true"
+
+    def test_disable__single_missing(self):
+        task = create_task(
+            SetObjectSettings,
+            {
+                "api_names": "Test__c",
+                "disable": ["Sharing"],
+            },
+        )
+        tree = metadata_tree.fromstring(CUSTOMOBJECT_SETTINGS_XML)
+
+        result = task._transform_entity(tree, "Test__c")
+
+        entry = result._element.findall(f".//{MD}enableSharing")
+        assert len(entry) == 1
+        assert entry[0].text == "false"
+
+    def test_disable__no_change(self):
+        task = create_task(
+            SetObjectSettings,
+            {
+                "api_names": "Test__c",
+                "disable": ["Feeds"],
+            },
+        )
+        tree = metadata_tree.fromstring(CUSTOMOBJECT_SETTINGS_XML)
+
+        result = task._transform_entity(tree, "Test__c")
+
+        entry = result._element.findall(f".//{MD}enableFeeds")
+        assert len(entry) == 1
+        assert entry[0].text == "false"
+
+    def test_disable__multiple(self):
+        task = create_task(
+            SetObjectSettings,
+            {
+                "api_names": "Test__c",
+                "disable": ["Activities", "BulkApi"],
+            },
+        )
+        tree = metadata_tree.fromstring(CUSTOMOBJECT_SETTINGS_XML)
+
+        result = task._transform_entity(tree, "Test__c")
+
+        entry = result._element.findall(f".//{MD}enableActivities")
+        assert len(entry) == 1
+        assert entry[0].text == "false"
+
+        entry = result._element.findall(f".//{MD}enableBulkApi")
+        assert len(entry) == 1
+        assert entry[0].text == "false"
+
+    def test_enable_and_disable(self):
+        task = create_task(
+            SetObjectSettings,
+            {
+                "api_names": "Test__c",
+                "enable": ["Feeds", "History"],
+                "disable": ["Activities", "BulkApi"],
+            },
+        )
+        tree = metadata_tree.fromstring(CUSTOMOBJECT_SETTINGS_XML)
+
+        result = task._transform_entity(tree, "Test__c")
+
+        entry = result._element.findall(f".//{MD}enableActivities")
+        assert len(entry) == 1
+        assert entry[0].text == "false"
+
+        entry = result._element.findall(f".//{MD}enableBulkApi")
+        assert len(entry) == 1
+        assert entry[0].text == "false"
+
+        entry = result._element.findall(f".//{MD}enableFeeds")
+        assert len(entry) == 1
+        assert entry[0].text == "true"
+
+        entry = result._element.findall(f".//{MD}enableHistory")
+        assert len(entry) == 1
+        assert entry[0].text == "true"