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

I created 2 django-cms plugins, a parent "Container" that can contain multiple child "Content" plugins.
When I save the child plugin I would like to access the model of the parent plugin.
from cms.plugin_pool import plugin_pool
from cms.plugin_base import CMSPluginBase
from .models import Container, Content
class ContainerPlugin(CMSPluginBase):
model = Container
name = "Foo Container"
render_template = "my_package/container.html"
allow_children = True
child_classes = ["ContentPlugin"]
class ContentPlugin(CMSPluginBase):
model = content
name = "Bar Content"
render_template = "my_package/content.html"
require_parent = True
parent_classes = ["ContainerPlugin"]
allow_children = True
def save_model(self, request, obj, form, change):
response = super(ContentPlugin, self).save_model(
request, obj, form, change
)
# here I want to access the parent's (container) model, but how?
return response
plugin_pool.register_plugin(ContainerPlugin)
plugin_pool.register_plugin(ContentPlugin)
obj is the current plugin instance, so I can get all the properties of this model, but I can't figure out how to access the parent's plugin model. There is obj.parent, but it's not the plugin instance as far as I can tell. Also tried playing around with self.cms_plugin_instance and obj.parent.get_plugin_instance() but with no success.
Any advice?

Given a plugin instance,instance.get_plugin_instance() method returns a tuple containing:
instance - The plugin instance
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()

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.')

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.

Related

Django Sitetree returning "expected str instance, LazyTitle found" in {% sitetree_page_title from menu %}

Django version: 3.2.9 & 4.0.4
Python version:3.8.10
OS: Ubuntu 20.04lts
I have a sitetree that is using a context variable in the title. When going to that path and loading the template with {% sitetree_page_title from menu %} it returns sequence item 1: expected str instance, LazyTitle found.
I didn't find any information on this exact issue, but it seems that when using context data in the title a LazyTitle object is generated. The object is not being converted to a string so the template engine is bombing.
I will include the relevant code below. I was able to get around the issue by editing sitetreeapp.py and wrapping the return of get_current_page_title() in str(), but that feels excessive.
The app in question is a small test app I'm working to layout an example CRUD app with the features our company needs. It does nothing fancy. The app isn't 100% complete yet so if anything looks out of place, it could be I haven't got to that point, but this portion should be working fine.
The sitetree in question is loaded dynamically via the config.ready method. Menu items without context variables are working without issue. I have verified that the context variable in question is available within the template. Hopefully it's something simple that I am overlooking. Any input is appreciated. I should also note that, while I have used StackOverflow for many years, I haven't posted much so please forgive my post formatting.
sitetree.py - note insert and list are working fine
from sitetree.utils import tree, item
sitetrees = [[
# setup the base tree
tree('app_model_test', items=[
# Then define items and their children with `item` function.
item('App Test', 'test_app:app_test_home',
hint='Test CRUD App',
children=[
item('Search Results', 'test_app:app_test_list',
in_menu=False, in_sitetree=False),
item('Insert Record', 'test_app:app_test_insert',
hint="Insert a new record",
),
item('Update Record {{ object.ssn }}', 'test_app:app_test_update object.id',
in_menu=False, in_sitetree=False),
])
]),
], ]
urls.py
app_name = 'test_app'
urlpatterns = [
path('insert/', views.app_test_form, name='app_test_insert'),
path('update/<int:id>/', views.app_test_form, name='app_test_update'),
path('list/', views.appTestListView.as_view(), name='app_test_list'),
]
views.py - relevant pieces
def app_test_form(request, action='add', id=0):
if id > 0:
test_obj = get_object_or_404(appTestModel, id=id)
form = appTestForm(instance=test_obj)
if request.method == 'GET':
if id == 0:
form = appTestForm()
test_obj = None
else:
if id == 0:
form = appTestForm(request.POST)
if form.is_valid():
form.save()
return redirect('/list')
# this will have to be somewhere else and contain whatever app apps are
# installed to get all the menu entries
menus = ['app_model_test']
context = {'form': form,
'title': 'app Test',
'action': action,
'menus': menus,
'object': test_obj}
return render(request, 'CRUD_base.html', context)
dyn_tree_register.py
from sitetree.sitetreeapp import register_dynamic_trees, compose_dynamic_tree
from sitetree.utils import tree, item
from . import sitetree as app_test_tree
register_dynamic_trees(
[
compose_dynamic_tree(*app_test_tree.sitetrees),
],
# Line below tells sitetree to drop and recreate cache, so that all newly registered
# dynamic trees are rendered immediately.
reset_cache=True
)
apps.py
from django.apps import AppConfig
class AppModelTestConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'app_model_test'
def ready(self):
from . import dyn_tree_register
and lastly the relevant template portion:
{% load sitetree %}
{% for menu in menus %}
<h1 class="title" id="page-title">{% sitetree_page_title from menu %}</h1>
{% endfor %}
As I mentioned, I can make this work by modifying sitetreeapp.py and wrapping the return of get_current_page_title in str():
def get_current_page_title(self, tree_alias: str, context: Context) -> str:
"""Returns resolved from sitetree title for current page.
:param tree_alias:
:param context:
"""
return str(self.get_current_page_attr('title_resolved', tree_alias, context))
Edit - 2022-04-13
As a temporary workaround I created a wrapper template tag that calls the sitetree_page_title and then renders it and wraps the output in str(). This seems hackish to me so I appreciate any insight. Short term this will get me by but I would rather not put something like this into production as I feel there's a bug on my side rather than within sitetree otherwise more people would have run into this.
custom tag and template.Node class
#register.tag()
def app_str_sitetree_page_title(parser, token):
'''
There is an issue causing sitetree's page title function to return a LazyPageTitle object which is not being converted to a string.
This function is a temporary fix until the issue is resolved to force the return to a string.
'''
from sitetree.templatetags import sitetree
ret = sitetree.sitetree_page_title(parser, token)
return StrSitetreePageTitle(ret)
class StrSitetreePageTitle(template.Node):
'''
This is the render wrapper to ensure a string is returned from sitetree_page_title
Like app_str_sitetree_page_title this is temporary
'''
def __init__(self, title_obj) -> None:
self.title_obj = title_obj
def render(self, context):
title_render = self.title_obj.render(context)
return str(title_render)

Render a Django CMS Placeholder to variable in view

I try to render a Django CMS Placeholder from a page to a variable, to return the rendered code as JSON.
So what I do is:
from cms.models.placeholdermodel import Placeholder
from cms.models.pagemodel import Page
def render(self, page_id, placeholder_slot, request):
page = Page.objects.get(id=page_id)
placeholder = page.placeholders.get(slot=placeholder_slot)
Now I want to render the placeholder to a variable. Which function do I have to call in which way to get this?
The Placeholder can be called manually and rendered like that, a few examples:
from cms import models
from django import template
def render(request):
placeholder = models.Placeholder.objects.get_or_create(slot='some_slot')
context = template.RequestContext(request)
return placeholder.render(request, width=None)
Or for json/javascript/etc. where you don't want full html but just the value:
def render_basic(request):
placeholder = models.Placeholder.objects.get_or_create(slot='some_slot')
context = template.RequestContext(request)
return placeholder.render(request, width=None, editable=False)
It's also possible (and even easier) to use the StaticPlaceholder:
from cms import models
def render(request):
placeholder = models.StaticPlaceholder.objects.get_or_create(name='name of the placeholder')
return placeholder.code
You can render it using Placeholder.render. Note that the context must contain a valid HttpRequest object under the 'request' key. width may be None. See the {% render_placeholder %} implementation for an example.

Creating a test database through the shell. Missing connection.creation.create_test_db()

The way this is supposed to be done is:
from django.db import connection
db = connection.creation.create_test_db() # Create the test db
However, the connection i import has no methods or attributes. It's type is django.db.DefaultConnectionProxy.
In the django/db/__init__.py lies the definition:
class DefaultConnectionProxy(object):
"""
Proxy for accessing the default DatabaseWrapper object's attributes. If you
need to access the DatabaseWrapper object itself, use
connections[DEFAULT_DB_ALIAS] instead.
"""
def __getattr__(self, item):
return getattr(connections[DEFAULT_DB_ALIAS], item)
def __setattr__(self, name, value):
return setattr(connections[DEFAULT_DB_ALIAS], name, value)
I've imported django.db.connections and found it has the following attributes/methods:
connections.all
connections.databases
connections.ensure_defaults
no sign of DEFAULT_DB_ALIAS.
I'm looking for ideas on how to debug this. Wouldn't want to post a ticket to Django if this has something to do with my configuration.

Django cms accessing extended property

I've extended the Django cms Page model into ExtendedPage model and added page_image to it.
How can I now acces the page_image property in a template.
I'd like to access the page_image property for every child object in a navigation menu... creating a menu with images...
I've extended the admin and I have the field available for editing (adding the picture)
from django.db import models
from django.utils.translation import ugettext_lazy as _
from cms.models.pagemodel import Page
from django.conf import settings
class ExtendedPage(models.Model):
page = models.OneToOneField(Page, unique=True, verbose_name=_("Page"), editable=False, related_name='extended_fields')
page_image = models.ImageField(upload_to=settings.MEDIA_ROOT, verbose_name=_("Page image"), blank=True)
Thank you!
BR
request.current_page.extended_fields.page_image
should work if you are using < 2.4. In 2.4 they introduced a new two page system (published/draft) so you might need
request.current_page.publisher_draft.extended_fields.page_image
I usually write some middleware or a template processor to handle this instead of doing it repetitively in the template. Something like:
class PageOptions(object):
def process_request(self, request):
request.options = dict()
if not request.options and request.current_page:
extended_fields = None
try:
extended_fields = request.current_page.extended_fields
except:
try:
custom_settings = request.current_page.publisher_draft.extended_fields
except:
pass
if extended_fields:
for field in extended_fields._meta.fields:
request.options[field.name] = getattr(extended_fields, field.name)
return None
will allow you to simply do {{ request.options.page_image }}

Django with Pluggable MongoDB Storage troubles

I'm trying to use django, and mongoengine to provide the storage backend only with GridFS. I still have a MySQL database.
I'm running into a strange (to me) error when I'm deleting from the django admin and am wondering if I am doing something incorrectly.
my code looks like this:
# settings.py
from mongoengine import connect
connect("mongo_storage")
# models.py
from mongoengine.django.storage import GridFSStorage
class MyFile(models.Model):
name = models.CharField(max_length=50)
content = models.FileField(upload_to="appsfiles", storage=GridFSStorage())
creation_time = models.DateTimeField(auto_now_add=True)
last_update_time = models.DateTimeField(auto_now=True)
I am able to upload files just fine, but when I delete them, something seems to break and the mongo database seems to get in an unworkable state until I manually delete all FileDocument.objects. When this happens I can't upload files or delete them from the django interface.
From the stack trace I have:
/home/projects/vector/src/mongoengine/django/storage.py in _get_doc_with_name
doc = [d for d in docs if getattr(d, self.field).name == name] ...
▼ Local vars
Variable Value
_[1]
[]
d
docs
Error in formatting: cannot set options after executing query
name
u'testfile.pdf'
self
/home/projects/vector/src/mongoengine/fields.py in __getattr__
raise AttributeError
Am I using this feature incorrectly?
UPDATE:
thanks to #zeekay's answer I was able to get a working gridfs storage plugin to work. I ended up not using mongoengine at all. I put my adapted solution on github. There is a clear sample project showing how to use it. I also uploaded the project to pypi.
Another Update:
I'd highly recommend the django-storages project. It has lots of storage backed options and is used by many more people than my original proposed solution.
I think you are better off not using MongoEngine for this, I haven't had much luck with it either. Here is a drop-in replacement for mongoengine.django.storage.GridFSStorage, which works with the admin.
from django.core.files.storage import Storage
from django.conf import settings
from pymongo import Connection
from gridfs import GridFS
class GridFSStorage(Storage):
def __init__(self, host='localhost', port=27017, collection='fs'):
for s in ('host', 'port', 'collection'):
name = 'GRIDFS_' + s.upper()
if hasattr(settings, name):
setattr(self, s, getattr(settings, name))
for s, v in zip(('host', 'port', 'collection'), (host, port, collection)):
if v:
setattr(self, s, v)
self.db = Connection(host=self.host, port=self.port)[self.collection]
self.fs = GridFS(self.db)
def _save(self, name, content):
self.fs.put(content, filename=name)
return name
def _open(self, name, *args, **kwars):
return self.fs.get_last_version(filename=name)
def delete(self, name):
oid = fs.get_last_version(filename=name)._id
self.fs.delete(oid)
def exists(self, name):
return self.fs.exists({'filename': name})
def size(self, name):
return self.fs.get_last_version(filename=name).length
GRIDFS_HOST, GRIDFS_PORT and GRIDFS_COLLECTION can be defined in your settings or passed as host, port, collection keyword arguments to GridFSStorage in your model's FileField.
I referred to Django's custom storage documenation, and loosely followed this answer to a similar question.