Skip to content

Commit

Permalink
Improved efficiency of rendering plugins embedded in text plugins usi…
Browse files Browse the repository at this point in the history
…ng bulk queries

This reduces the rendering of a text plugin with N embedded plugins from 3N
+ 1 queries to 3 queries.
  • Loading branch information
spookylukey committed Jan 3, 2012
1 parent df5b5b5 commit ad773e0
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 5 deletions.
40 changes: 37 additions & 3 deletions cms/models/pluginmodel.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import itertools
import os
import warnings
from datetime import datetime, date
Expand Down Expand Up @@ -61,8 +62,35 @@ def __new__(cls, name, bases, attrs):
new_class._meta.db_table = table_name

return new_class




class CMSPluginManager(models.Manager):
def bulk_get(self, ids, with_instances=True, select_placeholder=True):
"""
Retrieve a set of CMSPlugin objects, with their plugin model instances,
efficiently, and a return as dictionary of {pk:CMSPlugin object}.
"""
from cms.plugin_pool import plugin_pool
objs = list(self.get_query_set().filter(id__in=ids))
retval = dict([(o.id, o) for o in objs])
if with_instances:
# group according to plugin_type, and retrieve the child objects in
# batches.
for plugin_type, parents in itertools.groupby(objs, lambda o: o.plugin_type):
parents = list(parents)
plugin_class = plugin_pool.get_plugin(parents[0].plugin_type)
q = plugin_class.model.objects.filter(id__in=[o.id for o in parents])
if select_placeholder:
q = q.select_related('placeholder')
children = list(q)
# Now match them up
children_parent_dict = dict([(o.cmsplugin_ptr_id, o) for o in children])
for parent in parents:
child = children_parent_dict[parent.id]
parent._instance = child
return retval


class CMSPlugin(MPTTModel):
'''
The base class for a CMS plugin model. When defining a new custom plugin, you should
Expand All @@ -88,7 +116,9 @@ class CMSPlugin(MPTTModel):
lft = models.PositiveIntegerField(db_index=True, editable=False)
rght = models.PositiveIntegerField(db_index=True, editable=False)
tree_id = models.PositiveIntegerField(db_index=True, editable=False)


objects = CMSPluginManager()

class Meta:
app_label = 'cms'

Expand Down Expand Up @@ -151,6 +181,10 @@ def get_plugin_instance(self, admin=None):
from cms.plugin_pool import plugin_pool
plugin_class = plugin_pool.get_plugin(self.plugin_type)
plugin = plugin_class(plugin_class.model, admin)# needed so we have the same signature as the original ModelAdmin

if hasattr(self, '_instance'):
return self._instance, plugin

if plugin.model != self.__class__: # and self.__class__ == CMSPlugin:
# (if self is actually a subclass, getattr below would break)
try:
Expand Down
11 changes: 9 additions & 2 deletions cms/plugins/text/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,19 @@ def plugin_tags_to_user_html(text, context, placeholder):
context is the template context to use, placeholder is the placeholder name
"""
# Collect all the objects at once to minimize DB queries
plugin_ids = map(int, set(OBJ_ADMIN_RE.findall(text)))
plugin_objs = CMSPlugin.objects.bulk_get(plugin_ids,
with_instances=True,
select_placeholder=True)

# Now render and subsitute
def _render_tag(m):
plugin_id = int(m.groups()[0])
try:
obj = CMSPlugin.objects.get(pk=plugin_id)
obj = plugin_objs[plugin_id]
obj._render_meta.text_enabled = True
except CMSPlugin.DoesNotExist:
except KeyError:
# Object must have been deleted. It cannot be rendered to
# end user so just remove it from the HTML altogether
return u''
Expand Down
27 changes: 27 additions & 0 deletions cms/tests/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,33 @@ def test_inheritplugin_media(self):
response = self.client.get(page.get_absolute_url())
self.assertTrue('%scms/js/libs/jquery.tweet.js' % settings.STATIC_URL in response.content, response.content)

def test_render_textplugin(self):
# Setup
page = create_page("render test", "nav_playground.html", "en")
ph = page.placeholders.get(slot="body")
text_plugin = add_plugin(ph, "TextPlugin", "en", body="Hello World")
link_plugins = []
for i in range(0, 10):
link_plugins.append(add_plugin(ph, "LinkPlugin", "en",
target=text_plugin,
name="A Link",
url="http://django-cms.org"))
txt = text_plugin.text
txt.body = plugin_tags_to_admin_html(
'\n'.join(["{{ plugin_object %d }}" % l.cmsplugin_ptr_id
for l in link_plugins]))
txt.save()
text_plugin = self.reload(text_plugin)

# 1 query for Placeholder object (could be eliminated with select_related
# in the right place)
# 1 query for the CMSPlugin objects,
# 1 query for each type of child object (1 in this case, all are Link)
with self.assertNumQueries(3):
rendered = text_plugin.render_plugin(placeholder=ph)

self.assertTrue('A Link' in rendered)

def test_copy_textplugin(self):
"""
Test that copying of textplugins replaces references to copied plugins
Expand Down

0 comments on commit ad773e0

Please sign in to comment.