Merge pull request #325 from nephila/feature/optimizations

Optimize querysets
This commit is contained in:
Iacopo Spalletti 2016-09-12 08:17:03 +02:00 committed by GitHub
commit 78001259a1
7 changed files with 72 additions and 28 deletions

View file

@ -4,6 +4,12 @@
History History
======= =======
******************
0.8.9 (unreleased)
******************
* Optimized querysets
****************** ******************
0.8.8 (2016-09-04) 0.8.8 (2016-09-04)
****************** ******************

View file

@ -26,6 +26,7 @@ class BlogCategoryMenu(CMSAttachMenu):
Handles all types of blog menu Handles all types of blog menu
""" """
name = _('Blog menu') name = _('Blog menu')
_config = {}
def get_nodes(self, request): def get_nodes(self, request):
""" """
@ -46,7 +47,11 @@ class BlogCategoryMenu(CMSAttachMenu):
posts_menu = False posts_menu = False
config = False config = False
if hasattr(self, 'instance') and self.instance: if hasattr(self, 'instance') and self.instance:
config = BlogConfig.objects.get(namespace=self.instance.application_namespace) if not self._config.get(self.instance.application_namespace, False):
self._config[self.instance.application_namespace] = BlogConfig.objects.get(
namespace=self.instance.application_namespace
)
config = self._config[self.instance.application_namespace]
if config and config.menu_structure in (MENU_TYPE_COMPLETE, MENU_TYPE_CATEGORIES): if config and config.menu_structure in (MENU_TYPE_COMPLETE, MENU_TYPE_CATEGORIES):
categories_menu = True categories_menu = True
if config and config.menu_structure in (MENU_TYPE_COMPLETE, MENU_TYPE_POSTS): if config and config.menu_structure in (MENU_TYPE_COMPLETE, MENU_TYPE_POSTS):
@ -57,7 +62,8 @@ class BlogCategoryMenu(CMSAttachMenu):
if config: if config:
categories = categories.namespace(self.instance.application_namespace) categories = categories.namespace(self.instance.application_namespace)
categories = categories.active_translations(language).distinct() categories = categories.active_translations(language).distinct()
categories = categories.order_by('parent__id', 'translations__name') categories = categories.order_by('parent__id', 'translations__name').\
select_related('app_config').prefetch_related('translations')
for category in categories: for category in categories:
node = NavigationNode( node = NavigationNode(
category.name, category.name,
@ -65,8 +71,8 @@ class BlogCategoryMenu(CMSAttachMenu):
'{0}-{1}'.format(category.__class__.__name__, category.pk), '{0}-{1}'.format(category.__class__.__name__, category.pk),
( (
'{0}-{1}'.format( '{0}-{1}'.format(
category.__class__.__name__, category.parent.id category.__class__.__name__, category.parent_id
) if category.parent else None ) if category.parent_id else None
) )
) )
nodes.append(node) nodes.append(node)
@ -75,7 +81,8 @@ class BlogCategoryMenu(CMSAttachMenu):
posts = Post.objects posts = Post.objects
if hasattr(self, 'instance') and self.instance: if hasattr(self, 'instance') and self.instance:
posts = posts.namespace(self.instance.application_namespace) posts = posts.namespace(self.instance.application_namespace)
posts = posts.active_translations(language).distinct() posts = posts.active_translations(language).distinct().\
select_related('app_config').prefetch_related('translations', 'categories')
for post in posts: for post in posts:
post_id = None post_id = None
parent = None parent = None
@ -106,6 +113,8 @@ class BlogNavModifier(Modifier):
a particular blog post is viewed, a particular blog post is viewed,
a corresponding category is selected in menu a corresponding category is selected in menu
""" """
_config = {}
def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb): def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
""" """
Actual modifier function Actual modifier function
@ -124,7 +133,9 @@ class BlogNavModifier(Modifier):
if app and app.app_config: if app and app.app_config:
namespace = resolve(request.path).namespace namespace = resolve(request.path).namespace
config = app.get_config(namespace) if not self._config.get(namespace, False):
self._config[namespace] = app.get_config(namespace)
config = self._config[namespace]
try: try:
if config and ( if config and (
not isinstance(config, BlogConfig) or not isinstance(config, BlogConfig) or

View file

@ -15,6 +15,7 @@ from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_bytes, force_text, python_2_unicode_compatible from django.utils.encoding import force_bytes, force_text, python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.html import escape, strip_tags from django.utils.html import escape, strip_tags
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import get_language, ugettext_lazy as _ from django.utils.translation import get_language, ugettext_lazy as _
@ -76,7 +77,7 @@ class BlogCategory(TranslatableModel):
verbose_name = _('blog category') verbose_name = _('blog category')
verbose_name_plural = _('blog categories') verbose_name_plural = _('blog categories')
@property @cached_property
def count(self): def count(self):
return self.blog_posts.namespace(self.app_config.namespace).published().count() return self.blog_posts.namespace(self.app_config.namespace).published().count()
@ -359,6 +360,16 @@ class BasePostPlugin(CMSPlugin):
class Meta: class Meta:
abstract = True abstract = True
def optimize(self, qs):
"""
Apply select_related / prefetch_related to optimize the view queries
:param qs: queryset to optimize
:return: optimized queryset
"""
return qs.select_related('app_config').prefetch_related(
'translations', 'categories', 'categories__translations', 'categories__app_config'
)
def post_queryset(self, request=None): def post_queryset(self, request=None):
language = get_language() language = get_language()
posts = Post._default_manager posts = Post._default_manager
@ -367,7 +378,7 @@ class BasePostPlugin(CMSPlugin):
posts = posts.active_translations(language_code=language) posts = posts.active_translations(language_code=language)
if not request or not getattr(request, 'toolbar', False) or not request.toolbar.edit_mode: if not request or not getattr(request, 'toolbar', False) or not request.toolbar.edit_mode:
posts = posts.published() posts = posts.published()
return posts.all() return self.optimize(posts.all())
@python_2_unicode_compatible @python_2_unicode_compatible
@ -398,7 +409,7 @@ class LatestPostsPlugin(BasePostPlugin):
posts = posts.filter(tags__in=list(self.tags.all())) posts = posts.filter(tags__in=list(self.tags.all()))
if self.categories.exists(): if self.categories.exists():
posts = posts.filter(categories__in=list(self.categories.all())) posts = posts.filter(categories__in=list(self.categories.all()))
return posts.distinct()[:self.latest_posts] return self.optimize(posts.distinct())[:self.latest_posts]
@python_2_unicode_compatible @python_2_unicode_compatible

View file

@ -21,6 +21,16 @@ User = get_user_model()
class BaseBlogView(AppConfigMixin, ViewUrlMixin): class BaseBlogView(AppConfigMixin, ViewUrlMixin):
model = Post model = Post
def optimize(self, qs):
"""
Apply select_related / prefetch_related to optimize the view queries
:param qs: queryset to optimize
:return: optimized queryset
"""
return qs.select_related('app_config').prefetch_related(
'translations', 'categories', 'categories__translations', 'categories__app_config'
)
def get_view_url(self): def get_view_url(self):
if not self.view_url_name: if not self.view_url_name:
raise ImproperlyConfigured( raise ImproperlyConfigured(
@ -45,7 +55,7 @@ class BaseBlogView(AppConfigMixin, ViewUrlMixin):
if not getattr(self.request, 'toolbar', False) or not self.request.toolbar.edit_mode: if not getattr(self.request, 'toolbar', False) or not self.request.toolbar.edit_mode:
queryset = queryset.published() queryset = queryset.published()
setattr(self.request, get_setting('CURRENT_NAMESPACE'), self.config) setattr(self.request, get_setting('CURRENT_NAMESPACE'), self.config)
return queryset.on_site() return self.optimize(queryset.on_site())
def get_template_names(self): def get_template_names(self):
template_path = (self.config and self.config.template_prefix) or 'djangocms_blog' template_path = (self.config and self.config.template_prefix) or 'djangocms_blog'
@ -83,7 +93,7 @@ class PostDetailView(TranslatableSlugMixin, BaseBlogView, DetailView):
queryset = self.model._default_manager.all() queryset = self.model._default_manager.all()
if not getattr(self.request, 'toolbar', False) or not self.request.toolbar.edit_mode: if not getattr(self.request, 'toolbar', False) or not self.request.toolbar.edit_mode:
queryset = queryset.published() queryset = queryset.published()
return queryset return self.optimize(queryset)
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
# submit object to cms to get corrent language switcher and selected category behavior # submit object to cms to get corrent language switcher and selected category behavior
@ -116,7 +126,7 @@ class PostArchiveView(BaseBlogListView, ListView):
qs = qs.filter(**{'%s__month' % self.date_field: self.kwargs['month']}) qs = qs.filter(**{'%s__month' % self.date_field: self.kwargs['month']})
if 'year' in self.kwargs: if 'year' in self.kwargs:
qs = qs.filter(**{'%s__year' % self.date_field: self.kwargs['year']}) qs = qs.filter(**{'%s__year' % self.date_field: self.kwargs['year']})
return qs return self.optimize(qs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['month'] = int(self.kwargs.get('month')) if 'month' in self.kwargs else None kwargs['month'] = int(self.kwargs.get('month')) if 'month' in self.kwargs else None
@ -132,7 +142,7 @@ class TaggedListView(BaseBlogListView, ListView):
def get_queryset(self): def get_queryset(self):
qs = super(TaggedListView, self).get_queryset() qs = super(TaggedListView, self).get_queryset()
return qs.filter(tags__slug=self.kwargs['tag']) return self.optimize(qs.filter(tags__slug=self.kwargs['tag']))
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['tagged_entries'] = (self.kwargs.get('tag') kwargs['tagged_entries'] = (self.kwargs.get('tag')
@ -148,7 +158,7 @@ class AuthorEntriesView(BaseBlogListView, ListView):
qs = super(AuthorEntriesView, self).get_queryset() qs = super(AuthorEntriesView, self).get_queryset()
if 'username' in self.kwargs: if 'username' in self.kwargs:
qs = qs.filter(**{'author__%s' % User.USERNAME_FIELD: self.kwargs['username']}) qs = qs.filter(**{'author__%s' % User.USERNAME_FIELD: self.kwargs['username']})
return qs return self.optimize(qs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['author'] = User.objects.get(**{User.USERNAME_FIELD: self.kwargs.get('username')}) kwargs['author'] = User.objects.get(**{User.USERNAME_FIELD: self.kwargs.get('username')})
@ -178,7 +188,7 @@ class CategoryEntriesView(BaseBlogListView, ListView):
qs = super(CategoryEntriesView, self).get_queryset() qs = super(CategoryEntriesView, self).get_queryset()
if 'category' in self.kwargs: if 'category' in self.kwargs:
qs = qs.filter(categories=self.category.pk) qs = qs.filter(categories=self.category.pk)
return qs return self.optimize(qs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['category'] = self.category kwargs['category'] = self.category

View file

@ -12,6 +12,7 @@ from haystack.constants import DEFAULT_ALIAS
from parler.utils.context import smart_override from parler.utils.context import smart_override
from djangocms_blog.cms_appconfig import BlogConfig from djangocms_blog.cms_appconfig import BlogConfig
from djangocms_blog.cms_menus import BlogCategoryMenu, BlogNavModifier
from djangocms_blog.models import BlogCategory, Post, ThumbnailOption from djangocms_blog.models import BlogCategory, Post, ThumbnailOption
User = get_user_model() User = get_user_model()
@ -214,3 +215,8 @@ class BaseTest(BaseTestCase):
unified_index = search_conn.get_unified_index() unified_index = search_conn.get_unified_index()
index = unified_index.get_index(Post) index = unified_index.get_index(Post)
return index return index
def _reset_menus(self):
cache.clear()
BlogCategoryMenu._config = {}
BlogNavModifier._config = {}

View file

@ -2,8 +2,7 @@
from __future__ import absolute_import, print_function, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from aldryn_apphooks_config.utils import get_app_instance from aldryn_apphooks_config.utils import get_app_instance
from django.core.cache import cache from django.utils.translation import activate
from django.utils.translation import activate, override
from menus.menu_pool import menu_pool from menus.menu_pool import menu_pool
from parler.utils.context import smart_override, switch_language from parler.utils.context import smart_override, switch_language
@ -21,7 +20,7 @@ class MenuTest(BaseTest):
def setUp(self): def setUp(self):
super(MenuTest, self).setUp() super(MenuTest, self).setUp()
self.cats = [self.category_1] self.cats = [self.category_1]
cache.clear() self._reset_menus()
for i, lang_data in enumerate(self._categories_data): for i, lang_data in enumerate(self._categories_data):
cat = self._get_category(lang_data['en']) cat = self._get_category(lang_data['en'])
if 'it' in lang_data: if 'it' in lang_data:
@ -34,7 +33,7 @@ class MenuTest(BaseTest):
# All cms menu modifiers should be removed from menu_pool.modifiers # All cms menu modifiers should be removed from menu_pool.modifiers
# so that they do not interfere with our menu nodes # so that they do not interfere with our menu nodes
menu_pool.modifiers = [m for m in menu_pool.modifiers if m.__module__.startswith('djangocms_blog')] menu_pool.modifiers = [m for m in menu_pool.modifiers if m.__module__.startswith('djangocms_blog')]
cache.clear() self._reset_menus()
def test_menu_nodes(self): def test_menu_nodes(self):
""" """
@ -52,7 +51,7 @@ class MenuTest(BaseTest):
cats_url = set([cat.get_absolute_url() for cat in self.cats if cat.has_translation(lang)]) cats_url = set([cat.get_absolute_url() for cat in self.cats if cat.has_translation(lang)])
self.assertTrue(cats_url.issubset(nodes_url)) self.assertTrue(cats_url.issubset(nodes_url))
cache.clear() self._reset_menus()
posts[0].categories.clear() posts[0].categories.clear()
for lang in ('en', 'it'): for lang in ('en', 'it'):
with smart_override(lang): with smart_override(lang):
@ -82,7 +81,7 @@ class MenuTest(BaseTest):
# No item in the menu # No item in the menu
self.app_config_1.app_data.config.menu_structure = MENU_TYPE_NONE self.app_config_1.app_data.config.menu_structure = MENU_TYPE_NONE
self.app_config_1.save() self.app_config_1.save()
cache.clear() self._reset_menus()
for lang in languages: for lang in languages:
request = self.get_page_request(None, self.user, r'/%s/page-two/' % lang) request = self.get_page_request(None, self.user, r'/%s/page-two/' % lang)
with smart_override(lang): with smart_override(lang):
@ -94,7 +93,7 @@ class MenuTest(BaseTest):
# Only posts in the menu # Only posts in the menu
self.app_config_1.app_data.config.menu_structure = MENU_TYPE_POSTS self.app_config_1.app_data.config.menu_structure = MENU_TYPE_POSTS
self.app_config_1.save() self.app_config_1.save()
cache.clear() self._reset_menus()
for lang in languages: for lang in languages:
request = self.get_page_request(None, self.user, r'/%s/page-two/' % lang) request = self.get_page_request(None, self.user, r'/%s/page-two/' % lang)
with smart_override(lang): with smart_override(lang):
@ -106,7 +105,7 @@ class MenuTest(BaseTest):
# Only categories in the menu # Only categories in the menu
self.app_config_1.app_data.config.menu_structure = MENU_TYPE_CATEGORIES self.app_config_1.app_data.config.menu_structure = MENU_TYPE_CATEGORIES
self.app_config_1.save() self.app_config_1.save()
cache.clear() self._reset_menus()
for lang in languages: for lang in languages:
request = self.get_page_request(None, self.user, r'/%s/page-two/' % lang) request = self.get_page_request(None, self.user, r'/%s/page-two/' % lang)
with smart_override(lang): with smart_override(lang):
@ -118,7 +117,7 @@ class MenuTest(BaseTest):
# Both types in the menu # Both types in the menu
self.app_config_1.app_data.config.menu_structure = MENU_TYPE_COMPLETE self.app_config_1.app_data.config.menu_structure = MENU_TYPE_COMPLETE
self.app_config_1.save() self.app_config_1.save()
cache.clear() self._reset_menus()
for lang in languages: for lang in languages:
request = self.get_page_request(None, self.user, r'/%s/page-two/' % lang) request = self.get_page_request(None, self.user, r'/%s/page-two/' % lang)
with smart_override(lang): with smart_override(lang):
@ -148,7 +147,7 @@ class MenuTest(BaseTest):
request = self.get_page_request( request = self.get_page_request(
pages[1], self.user, path=obj.get_absolute_url() pages[1], self.user, path=obj.get_absolute_url()
) )
cache.clear() self._reset_menus()
menu_pool.clear(all=True) menu_pool.clear(all=True)
view_obj = view_cls() view_obj = view_cls()
view_obj.request = request view_obj.request = request
@ -173,7 +172,7 @@ class MenuTest(BaseTest):
request = self.get_page_request( request = self.get_page_request(
pages[1], self.user, path=obj.get_absolute_url() pages[1], self.user, path=obj.get_absolute_url()
) )
cache.clear() self._reset_menus()
menu_pool.clear(all=True) menu_pool.clear(all=True)
view_obj = view_cls() view_obj = view_cls()
view_obj.request = request view_obj.request = request

View file

@ -409,7 +409,7 @@ class AdminTest(BaseTest):
self.assertEquals(post.sites.count(), 1) self.assertEquals(post.sites.count(), 1)
self.user.sites.clear() self.user.sites.clear()
post.sites.clear() post.sites.clear()
post = self.reload_model(post) self.reload_model(post)
def test_admin_clear_menu(self): def test_admin_clear_menu(self):
""" """
@ -421,6 +421,7 @@ class AdminTest(BaseTest):
request = self.get_page_request(None, self.user, r'/en/page-two/') request = self.get_page_request(None, self.user, r'/en/page-two/')
first_nodes = menu_pool.get_nodes(request) first_nodes = menu_pool.get_nodes(request)
self._reset_menus()
with pause_knocks(post): with pause_knocks(post):
with self.login_user_context(self.user): with self.login_user_context(self.user):
data = dict(namespace='sample_app', app_title='app1', object_name='Blog') data = dict(namespace='sample_app', app_title='app1', object_name='Blog')
@ -431,7 +432,7 @@ class AdminTest(BaseTest):
msg_mid = MessageMiddleware() msg_mid = MessageMiddleware()
msg_mid.process_request(request) msg_mid.process_request(request)
config_admin = admin.site._registry[BlogConfig] config_admin = admin.site._registry[BlogConfig]
response = config_admin.change_view(request, str(self.app_config_1.pk)) config_admin.change_view(request, str(self.app_config_1.pk))
second_nodes = menu_pool.get_nodes(request) second_nodes = menu_pool.get_nodes(request)
self.assertNotEqual(len(first_nodes), len(second_nodes)) self.assertNotEqual(len(first_nodes), len(second_nodes))