[Django]-How to access the parent model of a Django-CMS plugin

7👍

Given a plugin instance,instance.get_plugin_instance() method returns a tuple containing:

  1. instance – The plugin instance
  2. plugin – the associated plugin class instance
    get_plugin_instance

so something like this to get the parent object:

instance, plugin_class = object.parent.get_plugin_instance()

3👍

Option 1

Looking at the source code of CMSPluginBase, you might be able to use the implementation of get_child_classes. Unfortunately, that method really only returns the class names, so you cannot use it directly. But I think it actually does iterate over the child instances to get the class names:

def get_child_classes(self, slot, page):
    from cms.utils.placeholder import get_placeholder_conf

    template = page and page.get_template() or None

    # config overrides..
    ph_conf = get_placeholder_conf('child_classes', slot, template, default={})
    child_classes = ph_conf.get(self.__class__.__name__, self.child_classes)
    if child_classes:
        return child_classes
    from cms.plugin_pool import plugin_pool
    installed_plugins = plugin_pool.get_all_plugins(slot, page)
    return [cls.__name__ for cls in installed_plugins]

What you’d be interested in would be these two lines:

    from cms.plugin_pool import plugin_pool
    installed_plugins = plugin_pool.get_all_plugins(slot, page)

Option 2

Another way (the one I am using in my code) is to use signals, though this also requires finding the correct objects. The code is not very readable imho (see my lingering inline comments), but it works. It was written a while ago but I am still using it with django-cms 3.2.3.

The placeholder names really are the names that you have configured for your placeholders. It’s certainly preferable to move that into the settings or somewhere. I’m not sure why I haven’t done that, though.

I’d be interested in your solution!

# signals.py
import itertools
import logging

from cms.models import CMSPlugin
from cms.plugin_pool import plugin_pool
from django.db import ProgrammingError
from django.db.models.signals import post_save

logger = logging.getLogger(__name__)
_registered_plugins = [CMSPlugin.__name__]


def on_placeholder_saved(sender, instance, created, raw, using, update_fields, **kwargs):
    """
    :param sender: Placeholder
    :param instance: instance of Placeholder
    """
    logger.debug("Placeholder SAVED: %s by sender %s", instance, sender)
    # TODO this is totally ugly - is there no generic way to find out the related names?
    placeholder_names = [
        'topicintro_abstracts',
        'topicintro_contents',
        'topicintro_links',
        'glossaryentry_explanations',
    ]
    fetch_phs = lambda ph_name: _fetch_qs_as_list(instance, ph_name)
    container = list(itertools.chain.from_iterable(map(fetch_phs, placeholder_names)))
    logger.debug("Modified Placeholder Containers %s (%s)", container, placeholder_names)
    if container:
        if len(container) > 1:
            raise ProgrammingError("Several Containers use the same placeholder.")
        else:
            # TODO change modified_by (if possible?)
            container[0].save()

def _fetch_qs_as_list(instance, field):
    """
    :param instance: a model
    :param field: optional field (might not exist on model)
    :return: the field values as list (not as RelatedManager)
    """
    qs = getattr(instance, field)
    fields = qs.all() if qs else []
    return fields

def on_cmsplugin_saved(sender, instance, created, raw, using, update_fields, **kwargs):
    """
    :param sender: CMSPlugin or subclass
    :param instance: instance of CMSPlugin
    """
    plugin_class = instance.get_plugin_class()
    logger.debug("CMSPlugin SAVED: %s; plugin class: %s", instance, plugin_class)
    if not plugin_class.name in _registered_plugins:
        post_save.connect(on_cmsplugin_saved, sender=plugin_class)
        _registered_plugins.append(plugin_class.name)
        logger.info("Registered post_save listener with %s", plugin_class.name)
    on_placeholder_saved(sender, instance.placeholder, created, raw, using, update_fields)

def connect_existing_plugins():
    plugin_types = CMSPlugin.objects.order_by('plugin_type').values_list('plugin_type').distinct()
    for plugin_type in plugin_types:
        plugin_type = plugin_type[0]
        if not plugin_type in _registered_plugins:
            plugin_class = plugin_pool.get_plugin(plugin_type)
            post_save.connect(on_cmsplugin_saved, sender=plugin_class)
            post_save.connect(on_cmsplugin_saved, sender=plugin_class.model)
            _registered_plugins.append(plugin_type)
            _registered_plugins.append(plugin_class.model.__name__)
    logger.debug("INIT registered plugins: %s", _registered_plugins)

post_save.connect(on_cmsplugin_saved, sender=CMSPlugin)

You have to setup these signals somewhere. I’m doing this in my urls.py, though the app config might be the suitable location for it? (I’m trying to avoid app configs.)

# This code has to run at server startup (and not during migrate if avoidable)
try:
    signals.connect_existing_plugins()
except db.utils.ProgrammingError:
    logger.warn('Failed to setup signals (if your DB is not setup (not tables), you can savely ignore this error.')

3👍

By the fact that children plugins always inherit the context. In the parent template you be able to do:

{% with something=instance.some_parent_field %}
  {% for plugin in instance.child_plugin_instances %}
          {% render_plugin plugin %}
  {% endfor %}
{% endwith %}

And use something in your child template.

Leave a comment