CMS_PLACEHOLDER_CONF Behavior in Django CMS 5.x

Recently updated on

The `CMS_PLACEHOLDER_CONF` setting is a dictionary whose keys correspond to the names of placeholders in your templates. Each corresponding value typically includes a "plugins" list, which specifies which plugins can be added to that placeholder. Prior to Django CMS 5.0, if one of those plugins was a container plugin, the container class's own `child_classes` attribute (in the plugin's "cms_plugins.py") determined which other plugins could be added beneath it.  In other words, `CMS_PLACEHOLDER_CONF` defined which plugins were available at the root level of a placeholder, while the plugin classes themselves controlled the structure of the plugin tree below that point. A container plugin therefore defined its own rules about what other plugins could be added as its children and/or which plugins could serve as its parents.  It was not necessary to include those child or parent plugins in `CMS_PLACEHOLDER_CONF`.

But as of Django CMS 5.0, the interpretation of `CMS_PLACEHOLDER_CONF` has changed. Now, the "plugins" list no longer defines only the plugin types that may be used at the root level of the placeholder. Instead it functions as a canonical list of plugin types that are permitted to be used *anywhere* in the placeholder, including as descendants of other plugins.

As a consequence, declaring a container plugin in the plugins list is no longer sufficient to make its class-defined child plugins available beneath it. Those child plugins must now also be included in the placeholder's "plugins" list. But (if the plugin's own attributes don't forbid it) this also makes those child plugins available at the placeholder's root level, which may not be desirable.  In that case, a `parent_classes` restriction must now be added to the placeholder's entry in `CMS_PLACEHOLDER_CONF`.  These changes must be applied for every placeholder in the configuration. Thus, migrating a project with many placeholders from CMS 4.x to 5.x may result in a `CMS_PLACEHOLDER_CONF` that is considerably larger than before.

As an example, suppose we are using the following plugin classes as container plugins:

```
    class FooContainerPlugin(CMSPluginBase):
        ...
        allow_children = True
        child_classes = ["FooPlugin"]

    class BarContainerPlugin(CMSPluginBase):
        ...

Let us further assume the existence of plugins meant to go inside the containers (e.g., `FooPlugin`). Note that `FooContainerPlugin` defines its own `child_classes` but `BarContainerPlugin` does not.

If we had the following configuration for "my_placeholder" under CMS 4.x,

    CMS_PLACEHOLDER_CONF = {
        "my_placeholder": {
            "plugins": ["FooContainerPlugin", "BarContainerPlugin"],
            "child_classes": {
                "BarContainerPlugin": ["BarPlugin1", "BarPlugin2", "BarPlugin3","BarPlugin4", "BarPlugin5"],
            }
        },
        ...
    }
```

then `FooPlugin` could be added to `FooContainerPlugin` despite not being explicitly listed in `CMS_PLACEHOLDER_CONF`.  However, it would *only* be available as a child of `FooContainerPlugin`.  It would not be available to be added at the root level of the placeholder.  Similarly, the various child plugins under `BarContainerPlugin` would only be available to be added as children of `BarContainerPlugin`.

To preserve this behavior under CMS 5.x, we would need to change our setting to this:
```
    CMS_PLACEHOLDER_CONF = {
        "my_placeholder": {
            "plugins": [
                "FooContainerPlugin", "FooPlugin", "BarContainerPlugin", "BarPlugin1", "BarPlugin2", "BarPlugin3","BarPlugin4", "BarPlugin5"
            ],
            "child_classes": {
                "BarContainerPlugin": ["BarPlugin1", "BarPlugin2", "BarPlugin3","BarPlugin4", "BarPlugin5"]
            }
            "parent_classes": {
                "FooPlugin": ["FooContainerPlugin"],
                "BarPlugin1": ["BarContainerPlugin"],
                "BarPlugin2": ["BarContainerPlugin"],
                "BarPlugin3": ["BarContainerPlugin"],
                "BarPlugin4": ["BarContainerPlugin"],
                "BarPlugin5": ["BarContainerPlugin"]
            }
        },
        ...
    }
```
Note that we don't need to include `FooContainerPlugin` in the `child_classes` entry because in that case the plugin class defines its own `child_classes` attribute.  But `BarContainerPlugin` is less specific, so we need to define its `child_classes` and the corresponding `parent_classes` restrictions for its contained plugins.  The resulting setting is noticeably larger, and similar changes might be required for every placeholder in `CMS_PLACEHOLDER_CONF`.  
 
The motivation for this change is presumably to make `CMS_PLACEHOLDER_CONF` the single source of truth for which plugins can appear under which placeholders.  But does this actually solve a problem?  Is the new behavior, in spite of the extra work described above, still an improvement over the old?  Only website developers are configuring `CMS_PLACEHOLDER_CONF` and assigning to it the plugin classes that meet their needs and the needs of their clients.  At least in our case, those same developers are also developing many of the plugin classes to be used in the placeholders. In the case of a container plugin, the plugin's own `child_classes` and `parent_classes` attributes already seem like an appropriate place to codify which other plugins the class may contain and/or be contained by.
 
Even for third-party plugin classes over which developers may not have direct control, the 4.x behavior already allowed them to restrict those classes' child and parent plugins.  Using our example above, we can disallow children under `FooContainerPlugin` and restrict the contents of `BarContainerPlugin` to a subset of its self-declared `child_classes` like this:
```
    CMS_PLACEHOLDER_CONF = {
        "my_placeholder": {
            "plugins": ["FooContainerPlugin", "BarContainerPlugin"],
            "child_classes": {
                "FooContainerPlugin": None,
                "BarContainerPlugin": ["BarPlugin1", "BarPlugin2"]
            }
        },
    }
```
Thus the old behavior already enabled the user to restrict a parent plugin's child classes beyond what was defined in that plugin's own `child_classes` attribute, and did so in an intuitive way.  The crux of our argument here is that the new behavior offers no benefit for developers and maintainers of projects that use `CMS_PLACEHOLDER_CONF`, and indeed makes their lives more difficult inasmuch as the `CMS_PLACEHOLDER_CONF` setting will be more cluttered and less immediately comprehensible than before.
 
We posted our opinion to the Django CMS github issues board ("https://github.com/django-cms/django-cms/issues/8432"), and one of the developers there generously provided a function, `expand_child_classes`, that aids in the conversion of a 4.x `CMS_PLACEHOLDER_CONF` to a 5.x version that maintains the same behavior.  The function works by adding plugin classes as needed to the "plugins" list and by generating the corresponding `parent_classes` entries if necessary. Again using our example above, we can thus recapture the 4.x behavior by changing our setting to this:
```
    CMS_PLACEHOLDER_CONF = {
        "my_placeholder": expand_child_classes({
            "plugins": ["FooContainerPlugin", "BarContainerPlugin"],
            "child_classes": {
                "FooContainerPlugin": ["FooPlugin"],
                "BarContainerPlugin": ["BarPlugin1", "BarPlugin2", "BarPlugin3","BarPlugin4", "BarPlugin5"],
            }
        }),
        ...
    }
```
But even with the helper, some new rules must be followed. Under the old rules, a plugin named in the `child_classes` attribute of the container plugin's class definition did not need to be named in `CMS_PLACEHOLDER_CONF`. Now it must be mentioned somewhere, even if only in the `child_classes` entry for that placeholder, as the helper has no knowledge of relationships declared on the plugin classes themselves.  Similarly, if a plugin class already declares a `parent_classes` attribute, those relationships must be repeated in `CMS_PLACEHOLDER_CONF`, because any parent_classes generated by the helper will take precedence for that placeholder, potentially nullifying the class's own declared `parent_classes`.  Finally, the helper function is a double-edged sword.  It removes much of the mechanical drudgery of converting a 4.x settings to a 5.x one, but at the same time it obscures the final structure of `CMS_PLACEHOLDER_CONF`, as the lists and dictionaries that make up the setting are being modified "under the hood".
 
Therefore we would like to offer the following solution, which monkey-patches Django CMS so that `CMS_PLACEHOLDER_CONF` is once again interpreted the way it was prior to Django CMS 5.0.  Specifically, it restores the Django CMS 4.x implementation of `CMSPluginBase.get_child_plugin_candidates()`. Instead of filtering candidate child plugins through the placeholder's plugins list, it again considers every registered plugin and then lets the plugins' own `child_classes` and `parent_classes` attributes determine which relationships are valid.
 
To use the patch, create an app directory called "cms_patch" (or whatever you like), and create the following "apps.py" inside it:
```
from django.apps import AppConfig

class CMSPatchConfig(AppConfig):
    name = "my_project.apps.cms_patch"
    def ready(self):
        from cms.plugin_base import CMSPluginBase
        from cms.plugin_pool import plugin_pool

        @classmethod
        def legacy_get_child_plugin_candidates(cls, slot, page=None):
            return plugin_pool.registered_plugins

        CMSPluginBase.get_child_plugin_candidates = legacy_get_child_plugin_candidates
```
Add your new app to your project's `INSTALLED_APPS` and you should be all done. The patch is narrowly scoped, replacing only `CMSPluginBase.get_child_plugin_candidates()`. It should therefore be straightforward to review when upgrading Django CMS in the future.
 
Whether one prefers the 4.x or the 5.x interpretation of `CMS_PLACEHOLDER_CONF`, understanding the distinction is important when upgrading existing projects. Hopefully the helper function described above -- or, where appropriate, the monkey patch presented here -- will make that transition a little easier.

Share , ,
If you're getting even a smidge of value from this post, would you please take a sec and share it? It really does help.