Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit de25506

Browse files
authored
perf: Optimize placeholder and plugin utilities (#8319)
* feat: Add lru_cache for placeholder search in templates * fix: Combine queries * fix: cache instead of lru_cache * fix: Deactivate placeholder cache for DEBUG=True * fix: simplify scan placeholders * Fix mysql * Check mysql bulk_create * fix: Work-around for mysql * perf: Minor execution time improvements * perf: Minor python perf improvements * perf: drop unnecessary sorting * fix: Remove commented out code * fix: typo * fix: Style improvements * fix: Minor adjustments * fix: model close bug
1 parent dc20af8 commit de25506

20 files changed

+294
-215
lines changed

cms/models/placeholderpluginmodel.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ class PlaceholderReference(CMSPlugin):
1919

2020
@cached_property
2121
def placeholder_ref(self):
22-
return get_placeholder_from_slot(self.placeholders, "clipboard")
22+
placeholder = get_placeholder_from_slot(self.placeholders, "clipboard")
23+
placeholder.source = self
24+
return placeholder
2325

2426
class Meta:
2527
app_label = 'cms'

cms/models/pluginmodel.py

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import annotations
22

33
import os
4+
import re
45
import warnings
56
from datetime import date
6-
from functools import cache
7+
from functools import cache, cached_property
78

89
from django.core.exceptions import ObjectDoesNotExist
910
from django.db import connection, connections, models, router
@@ -202,21 +203,23 @@ def __repr__(self):
202203
return display
203204

204205
def get_plugin_name(self):
205-
from cms.plugin_pool import plugin_pool
206-
207-
return plugin_pool.get_plugin(self.plugin_type).name
206+
return self.plugin_class.name
208207

209208
def get_short_description(self):
210209
instance = self.get_plugin_instance()[0]
211210
if instance is not None:
212211
return force_str(instance)
213212
return _("<Empty>")
214213

215-
def get_plugin_class(self):
214+
@cached_property
215+
def plugin_class(self):
216216
from cms.plugin_pool import plugin_pool
217217

218218
return plugin_pool.get_plugin(self.plugin_type)
219219

220+
def get_plugin_class(self):
221+
return self.plugin_class
222+
220223
def get_plugin_class_instance(self, admin=None):
221224
plugin_class = self.get_plugin_class()
222225
# needed so we have the same signature as the original ModelAdmin
@@ -264,7 +267,8 @@ def get_bound_plugin(self):
264267
return self._inst
265268

266269
def get_plugin_info(self, children=None, parents=None):
267-
plugin_class = self.get_plugin_class()
270+
plugin_class = self.plugin_class
271+
268272
return {
269273
'type': 'plugin',
270274
'position': self.position,
@@ -445,26 +449,32 @@ def get_action_urls(self, js_compat=True):
445449
This method replaces the set of legacy methods `get_add_url`, ``get_edit_url`, `get_move_url`,
446450
`get_delete_url`, `get_copy_url`.
447451
"""
452+
if not hasattr(CMSPlugin, '_edit_url'):
453+
CMSPlugin._edit_url = admin_reverse('cms_placeholder_edit_plugin', args=(0,))
454+
CMSPlugin._add_url = admin_reverse('cms_placeholder_add_plugin')
455+
CMSPlugin._delete_url = admin_reverse('cms_placeholder_delete_plugin', args=(0,))
456+
CMSPlugin._move_url = admin_reverse('cms_placeholder_move_plugin')
457+
CMSPlugin._copy_url = admin_reverse('cms_placeholder_copy_plugins')
458+
448459
if js_compat:
449460
# TODO: Remove this condition
450461
# once the javascript files have been refactored
451462
# to use the new naming schema (ending in _url).
452-
data = {
453-
'edit_plugin': admin_reverse('cms_placeholder_edit_plugin', args=(self.pk,)),
454-
'add_plugin': admin_reverse('cms_placeholder_add_plugin'),
455-
'delete_plugin': admin_reverse('cms_placeholder_delete_plugin', args=(self.pk,)),
456-
'move_plugin': admin_reverse('cms_placeholder_move_plugin'),
457-
'copy_plugin': admin_reverse('cms_placeholder_copy_plugins'),
463+
return {
464+
'edit_plugin': re.sub(r"/0/", f"/{self.pk}/", CMSPlugin._edit_url),
465+
'add_plugin': CMSPlugin._add_url,
466+
'delete_plugin': re.sub(r"/0/", f"/{self.pk}/", CMSPlugin._delete_url),
467+
'move_plugin': CMSPlugin._move_url,
468+
'copy_plugin': CMSPlugin._copy_url,
458469
}
459470
else:
460-
data = {
461-
'edit_url': admin_reverse('cms_placeholder_edit_plugin', args=(self.pk,)),
462-
'add_url': admin_reverse('cms_placeholder_add_plugin'),
463-
'delete_url': admin_reverse('cms_placeholder_delete_plugin', args=(self.pk,)),
464-
'move_url': admin_reverse('cms_placeholder_move_plugin'),
465-
'copy_url': admin_reverse('cms_placeholder_copy_plugins'),
471+
return {
472+
'edit_url': re.sub(r"/0/", f"/{self.pk}/", CMSPlugin._edit_url),
473+
'add_url': CMSPlugin._add_url,
474+
'delete_url': re.sub(r"/0/", f"/{self.pk}/", CMSPlugin._delete_url),
475+
'move_url': CMSPlugin._move_url,
476+
'copy_url': CMSPlugin._copy_url,
466477
}
467-
return data
468478

469479

470480
def get_plugin_media_path(instance, filename):

cms/plugin_base.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,7 @@ def __new__(cls, name, bases, attrs):
8585
return new_plugin
8686

8787

88-
T = TypeVar("T", bound=Callable)
89-
90-
91-
def template_slot_caching(method: T) -> T:
88+
def template_slot_caching(method: Callable) -> Callable:
9289
"""
9390
Decorator that enables global caching for methods based on placeholder slots and templates.
9491
@@ -109,12 +106,8 @@ def get_child_class_overrides(cls, slot: str, page: Optional[Page] = None, insta
109106
pass
110107
"""
111108

112-
@wraps(method)
113-
def wrapper(*args, **kwargs):
114-
return method(*args, **kwargs)
115-
116-
wrapper._template_slot_caching = True
117-
return cast(T, wrapper)
109+
method._template_slot_caching = True
110+
return method
118111

119112

120113
class CMSPluginBase(admin.ModelAdmin, metaclass=CMSPluginBaseMetaclass):
@@ -736,7 +729,7 @@ def get_child_plugin_candidates(cls, slot: str, page: Optional[Page] = None) ->
736729
# where only text-only plugins are allowed.
737730
from cms.plugin_pool import plugin_pool
738731

739-
return sorted(plugin_pool.get_all_plugins(slot, page, root_plugin=False), key=attrgetter("module", "name"))
732+
return plugin_pool.get_all_plugins(slot, page, root_plugin=False)
740733

741734
@classmethod
742735
@template_slot_caching
@@ -753,8 +746,12 @@ def get_child_classes(
753746
installed_plugins = cls.get_child_plugin_candidates(slot, page)
754747

755748
if child_classes:
749+
# Override skips check if current class is valid parent of child classes
756750
return [plugin.__name__ for plugin in installed_plugins if plugin.__name__ in child_classes]
757751

752+
if only_uncached:
753+
installed_plugins = [plugin for plugin in installed_plugins if not plugin.cache_parent_classes]
754+
758755
child_classes = []
759756
plugin_type = cls.__name__
760757

@@ -767,12 +764,11 @@ def get_child_classes(
767764
# If there are no restrictions then the plugin
768765
# is a valid child class.
769766
for plugin_class in installed_plugins:
770-
if not only_uncached or not plugin_class.cache_parent_classes:
771-
allowed_parents = plugin_class.get_parent_classes(slot, page, instance)
772-
if not allowed_parents or plugin_type in allowed_parents:
773-
# Plugin has no parent restrictions or
774-
# Current plugin (self) is a configured parent
775-
child_classes.append(plugin_class.__name__)
767+
allowed_parents = plugin_class.get_parent_classes(slot, page, instance)
768+
if not allowed_parents or plugin_type in allowed_parents:
769+
# Plugin has no parent restrictions or
770+
# Current plugin (self) is a configured parent
771+
child_classes.append(plugin_class.__name__)
776772

777773
return child_classes
778774

cms/plugin_pool.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ def __init__(self):
2222
self.plugins = {}
2323
self.discovered = False
2424
self.global_restrictions_cache = {
25+
# Initialize the global restrictions cache for each CMS_PLACEHOLDER_CONF
26+
# granularity that contains "parent_classes" or "child_classes" overwrites
2527
None: {},
26-
**{key: {} for key in get_cms_setting("PLACEHOLDER_CONF").keys()},
28+
**{key: {} for key, value in get_cms_setting("PLACEHOLDER_CONF").items()
29+
if "parent_classes" in value or "child_classes" in value},
2730
}
2831
self.global_template_restrictions = any(".htm" in (key or "") for key in self.global_restrictions_cache)
2932

@@ -41,6 +44,8 @@ def discover_plugins(self):
4144

4245
autodiscover_modules("cms_plugins")
4346
self.discovered = True
47+
# Sort plugins by their module and name
48+
self.plugins = dict(sorted(self.plugins.items(), key=lambda key: (key[1].module, key[1].name)))
4449

4550
def clear(self):
4651
self.discovered = False
@@ -135,7 +140,6 @@ def get_all_plugins(
135140
):
136141
from cms.utils.placeholder import get_placeholder_conf
137142

138-
self.discover_plugins()
139143
plugins = self.plugins.values()
140144
template = (
141145
lazy(page.get_template, str)() if page else None
@@ -185,7 +189,6 @@ def get_plugin(self, name) -> type[CMSPluginBase]:
185189
"""
186190
Retrieve a plugin from the cache.
187191
"""
188-
self.discover_plugins()
189192
return self.plugins[name]
190193

191194
def get_patterns(self) -> list[URLResolver]:
@@ -210,7 +213,6 @@ def get_patterns(self) -> list[URLResolver]:
210213
return url_patterns
211214

212215
def get_system_plugins(self) -> list[str]:
213-
self.discover_plugins()
214216
return [plugin.__name__ for plugin in self.plugins.values() if plugin.system]
215217

216218
@cached_property
@@ -240,7 +242,7 @@ def get_restrictions_cache(self, request_cache: dict, instance: CMSPluginBase, p
240242
be recalculated for each request.
241243
242244
Args:
243-
request_cache (dict): The current request cache.
245+
request_cache (dict): The current request cache (only filled is non globally cacheable).
244246
instance (CMSPluginBase): The plugin instance for which to retrieve the restrictions cache.
245247
page (Optional[Page]): The page associated with the plugin instance, if any.
246248
@@ -256,11 +258,11 @@ def get_restrictions_cache(self, request_cache: dict, instance: CMSPluginBase, p
256258
else:
257259
template = ""
258260

259-
if f"{template} {slot}" in self.global_restrictions_cache:
261+
if template and f"{template} {slot}" in self.global_restrictions_cache:
260262
return self.global_restrictions_cache[f"{template} {slot}"]
261263
if template and template in self.global_restrictions_cache:
262264
return self.global_restrictions_cache[template]
263-
if slot in self.global_restrictions_cache:
265+
if slot and slot in self.global_restrictions_cache:
264266
return self.global_restrictions_cache[slot]
265267
return self.global_restrictions_cache[None]
266268

cms/plugin_rendering.py

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,10 @@
3939
logger = logging.getLogger(__name__)
4040

4141

42-
def _unpack_plugins(parent_plugin: CMSPlugin) -> list[CMSPlugin]:
43-
found_plugins = []
44-
42+
def _unpack_plugins(parent_plugin: CMSPlugin) -> Generator[CMSPlugin, None, None]:
43+
yield parent_plugin
4544
for plugin in parent_plugin.child_plugin_instances or []:
46-
found_plugins.append(plugin)
47-
48-
if plugin.child_plugin_instances:
49-
found_plugins.extend(_unpack_plugins(plugin))
50-
return found_plugins
45+
yield from _unpack_plugins(plugin)
5146

5247

5348
class RenderedPlaceholder:
@@ -95,12 +90,12 @@ class BaseRenderer:
9590

9691
def __init__(self, request: HttpRequest):
9792
self.request = request
98-
self._cached_templates = {}
9993
self._cached_plugin_classes = {}
10094
self._placeholders_content_cache = {}
10195
self._placeholders_by_page_cache = {}
10296
self._rendered_placeholders = OrderedDict()
10397
self._rendered_plugins_by_placeholder = {}
98+
self._plugins_with_perms = None
10499

105100
@cached_property
106101
def current_page(self) -> Page:
@@ -128,20 +123,25 @@ def plugin_pool(self) -> PluginPool:
128123
def request_language(self) -> str:
129124
return get_language_from_request(self.request)
130125

126+
def get_plugins_with_perms(self) -> list[CMSPlugin]:
127+
if self._plugins_with_perms is None:
128+
registered_plugins = self.plugin_pool.registered_plugins
129+
can_add_plugin = partial(
130+
has_plugin_permission, user=self.request.user, permission_type="add"
131+
)
132+
self._plugins_with_perms = [
133+
plugin
134+
for plugin in registered_plugins
135+
if can_add_plugin(plugin_type=plugin.value)
136+
]
137+
138+
return self._plugins_with_perms
139+
131140
def get_placeholder_plugin_menu(
132141
self, placeholder: Placeholder, page: Optional[Page] = None
133142
):
134-
registered_plugins = self.plugin_pool.registered_plugins
135-
can_add_plugin = partial(
136-
has_plugin_permission, user=self.request.user, permission_type="add"
137-
)
138-
plugins = [
139-
plugin
140-
for plugin in registered_plugins
141-
if can_add_plugin(plugin_type=plugin.value)
142-
]
143143
plugin_menu = get_toolbar_plugin_struct(
144-
plugins=plugins,
144+
plugins=self.get_plugins_with_perms(),
145145
slot=placeholder.slot,
146146
page=page,
147147
)
@@ -165,7 +165,8 @@ def get_plugin_toolbar_js(self, plugin: CMSPlugin, page: Optional[Page] = None):
165165
)
166166
child_classes, parent_classes = get_plugin_restrictions(
167167
plugin=plugin,
168-
restrictions_cache=placeholder_cache,
168+
page=page,
169+
restrictions_cache=placeholder_cache, # Store non-global plugin-restriction in placeholder_cache
169170
)
170171
content = get_plugin_toolbar_js(
171172
plugin,
@@ -736,13 +737,7 @@ def get_plugins_to_render(self, *args, **kwargs):
736737
plugins = super().get_plugins_to_render(*args, **kwargs)
737738

738739
for plugin in plugins:
739-
yield plugin
740-
741-
if not plugin.child_plugin_instances:
742-
continue
743-
744-
for plugin in _unpack_plugins(plugin):
745-
yield plugin
740+
yield from _unpack_plugins(plugin)
746741

747742
def render_placeholder(self, placeholder, language, page=None):
748743
rendered_plugins = self.render_plugins(

cms/static/cms/js/modules/cms.modal.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,15 +1005,15 @@ class Modal {
10051005
true
10061006
);
10071007
} else {
1008+
// hello ckeditor
1009+
Helpers.removeEventListener('modal-close.text-plugin');
1010+
that.close();
10081011
// Serve the data bridge:
10091012
// We have a special case here cause the CMS namespace
10101013
// can be either inside the current window or the parent
10111014
const dataBridge = body[0].querySelector('script#data-bridge');
10121015

10131016
if (dataBridge) {
1014-
// hello ckeditor
1015-
Helpers.removeEventListener('modal-close.text-plugin');
1016-
that.close();
10171017
// the dataBridge is used to access plugin information from different resources
10181018
// Do NOT move this!!!
10191019
try {

cms/templatetags/cms_js_tags.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,12 @@ def bool(value):
3434
def render_cms_structure_js(context, renderer, obj):
3535
markup_bits = []
3636
obj_placeholders_by_slot = rescan_placeholders_for_obj(obj)
37-
declared_placeholders = get_declared_placeholders_for_obj(obj)
3837
try:
3938
lang = context["request"].toolbar.request_language
4039
except AttributeError:
4140
lang = None
4241

43-
for placeholder_node in declared_placeholders:
44-
obj_placeholder = obj_placeholders_by_slot.get(placeholder_node.slot)
45-
42+
for obj_placeholder in obj_placeholders_by_slot.values():
4643
if obj_placeholder:
4744
placeholder_js = renderer.render_placeholder(obj_placeholder, language=lang, page=obj)
4845
markup_bits.append(placeholder_js)

cms/test_utils/util/context_managers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from tempfile import _exists, mkdtemp, template
66

77
from django.contrib.auth import get_user_model
8+
from django.test.utils import override_settings
89
from django.utils.translation import activate, get_language
910

1011
from cms.apphook_pool import apphook_pool
@@ -173,3 +174,15 @@ def __init__(self):
173174
def __call__(self, *args, **kwargs):
174175
self.call_count += 1
175176
self.calls.append((args, kwargs))
177+
178+
179+
@contextmanager
180+
def override_placeholder_conf(CMS_PLACEHOLDER_CONF):
181+
from cms.utils.placeholder import _clear_placeholder_conf_cache
182+
183+
# Call _get_placeholder_settings after changing the setting
184+
with override_settings(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
185+
_clear_placeholder_conf_cache() # Clear cache if needed
186+
yield
187+
# Call _get_placeholder_settings after resetting the setting
188+
_clear_placeholder_conf_cache() # Clear cache if needed

0 commit comments

Comments
 (0)