Skip to content

Commit

Permalink
Rewrite loading of view permissions to use caching and less complex q…
Browse files Browse the repository at this point in the history
…ueries..
  • Loading branch information
adaptivelogic committed Dec 12, 2012
1 parent f1f0bb6 commit 46bd3b6
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 422 deletions.
7 changes: 4 additions & 3 deletions cms/admin/change_list.py
Expand Up @@ -3,7 +3,7 @@
from collections import defaultdict
from cms.exceptions import NoHomeFound
from cms.models import Title, Page, PageModeratorState
from cms.utils.permissions import get_user_sites_queryset
from cms.utils.permissions import get_user_sites_queryset, load_view_restrictions
from django.conf import settings
from django.contrib.admin.views.main import ChangeList, ALL_VAR, IS_POPUP_VAR, \
ORDER_TYPE_VAR, ORDER_VAR, SEARCH_VAR
Expand Down Expand Up @@ -101,8 +101,7 @@ def set_items(self, request):
# tree using a stack now)
pages = self.get_query_set(request).drafts().order_by('tree_id', 'lft').select_related()


# Get lists of page IDs for which the current user has
# Get lists of page IDs for which the current user has
# "permission to..." on the current site.
perm_edit_ids = Page.permissions.get_change_id_list(request.user, site)
perm_publish_ids = Page.permissions.get_publish_id_list(request.user, site)
Expand All @@ -114,6 +113,8 @@ def set_items(self, request):
#pages = pages.filter(pk__in=perm_change_list_ids)

root_pages = []
# Cache view restrictions for the is_restricted template tag
load_view_restrictions(request, pages)
pages = list(pages)
all_pages = pages[:] # That is, basically, a copy.
try:
Expand Down
146 changes: 5 additions & 141 deletions cms/menu.py
@@ -1,160 +1,24 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
from cms.apphook_pool import apphook_pool
from cms.models.permissionmodels import (ACCESS_DESCENDANTS,
ACCESS_PAGE_AND_DESCENDANTS, ACCESS_CHILDREN, ACCESS_PAGE_AND_CHILDREN, ACCESS_PAGE)
from cms.models.permissionmodels import PagePermission, GlobalPagePermission
from cms.models.titlemodels import Title
from cms.utils import get_language_from_request
from cms.utils.i18n import get_fallback_languages, hide_untranslated
from cms.utils.page_resolver import get_page_queryset
from cms.utils.moderator import get_title_queryset
from cms.utils import permissions
from cms.utils.plugins import current_site
from menus.base import Menu, NavigationNode, Modifier
from menus.menu_pool import menu_pool

from django.conf import settings
from django.contrib.sites.models import Site
from django.db.models.query_utils import Q
from django.utils.translation import get_language


def get_visible_pages(request, pages, site=None):
"""
This code is basically a many-pages-at-once version of
Page.has_view_permission.
pages contains all published pages
check if there is ANY restriction
that needs a permission page visibility calculation
"""
is_setting_public_all = settings.CMS_PUBLIC_FOR == 'all'
is_setting_public_staff = settings.CMS_PUBLIC_FOR == 'staff'
is_auth_user = request.user.is_authenticated()

visible_page_ids = []
restricted_pages = defaultdict(list)
pages_perms_q = Q()

for page in pages:
# taken from for_page as multiple at once version
page_q = Q(page__tree_id=page.tree_id) & (
Q(page__level__lte=page.level)
)
pages_perms_q |= page_q

pages_perms_q &= Q(can_view=True)
page_permissions = PagePermission.objects.filter(pages_perms_q).select_related('page', 'group__users')

for perm in page_permissions:
# collect the pages that are affected by permissions
if perm is not None and perm not in restricted_pages[perm.page.pk]:
# affective restricted pages gathering
# using mptt functions
# add the page with the perm itself
if perm.grant_on in [ACCESS_PAGE, ACCESS_PAGE_AND_CHILDREN ,ACCESS_PAGE_AND_DESCENDANTS]:
restricted_pages[perm.page.pk].append(perm)
restricted_pages[perm.page.publisher_public_id].append(perm)
# add children
if perm.grant_on in [ACCESS_CHILDREN, ACCESS_PAGE_AND_CHILDREN]:
child_ids = perm.page.get_children().values_list('id', 'publisher_public_id')
for id, public_id in child_ids:
restricted_pages[id].append(perm)
restricted_pages[public_id].append(perm)
# add descendants
elif perm.grant_on in [ACCESS_DESCENDANTS, ACCESS_PAGE_AND_DESCENDANTS]:
child_ids = perm.page.get_descendants().values_list('id', 'publisher_public_id')
for id, public_id in child_ids:
restricted_pages[id].append(perm)
restricted_pages[public_id].append(perm)

# anonymous
# no restriction applied at all
if (not is_auth_user and
is_setting_public_all and
not restricted_pages):
return [page.pk for page in pages]


if site is None:
site = current_site(request)
"""Returns the IDs of all visible pages"""
pages = permissions.get_visible_pages(request, pages, site)
return [page.pk for page in pages]

# authenticated user and global permission
if is_auth_user:
global_page_perm_q = Q(
Q(user=request.user) | Q(group__user=request.user)
) & Q(can_view=True) & Q(Q(sites__in=[site.pk]) | Q(sites__isnull=True))
global_view_perms = GlobalPagePermission.objects.filter(global_page_perm_q).exists()

#no page perms edge case - all visible
if ((is_setting_public_all or (
is_setting_public_staff and request.user.is_staff))and
not restricted_pages and
not global_view_perms):
return [page.pk for page in pages]
#no page perms edge case - none visible
elif (is_setting_public_staff and
not request.user.is_staff and
not restricted_pages and
not global_view_perms):
return []


def has_global_perm():
if has_global_perm.cache < 0:
has_global_perm.cache = 1 if request.user.has_perm('cms.view_page') else 0
return bool(has_global_perm.cache)
has_global_perm.cache = -1

def has_permission_membership(page):
"""
PagePermission user group membership tests
"""
user_pk = request.user.pk
page_pk = page.pk
has_perm = False
for perm in restricted_pages[page_pk]:
if perm.user_id == user_pk:
has_perm = True
if not perm.group_id:
continue
group_user_ids = perm.group.user_set.values_list('pk', flat=True)
if user_pk in group_user_ids:
has_perm = True
return has_perm

for page in pages:
to_add = False
# default to false, showing a restricted page is bad
# explicitly check all the conditions
# of settings and permissions
is_restricted = page.pk in restricted_pages
# restricted_pages contains as key any page.pk that is
# affected by a permission grant_on
if is_auth_user:
# a global permission was given to the request's user
if global_view_perms:
to_add = True
# setting based handling of unrestricted pages
elif not is_restricted and (
is_setting_public_all or (
is_setting_public_staff and request.user.is_staff)
):
# authenticated user, no restriction and public for all
# or
# authenticated staff user, no restriction and public for staff
to_add = True
# check group and user memberships to restricted pages
elif is_restricted and has_permission_membership(page):
to_add = True
elif has_global_perm():
to_add = True
# anonymous user, no restriction
elif not is_restricted and is_setting_public_all:
to_add = True
# store it
if to_add:
visible_page_ids.append(page.pk)
return visible_page_ids

def page_to_node(page, home, cut):
"""
Expand Down Expand Up @@ -230,7 +94,7 @@ class CMSMenu(Menu):

def get_nodes(self, request):
page_queryset = get_page_queryset(request)
site = Site.objects.get_current()
site = current_site(request)
lang = get_language_from_request(request)

filters = {
Expand Down
60 changes: 10 additions & 50 deletions cms/models/pagemodel.py
Expand Up @@ -6,15 +6,14 @@
from cms.models.placeholdermodel import Placeholder
from cms.models.pluginmodel import CMSPlugin
from cms.publisher.errors import MpttPublisherCantPublish
from cms.utils import i18n, urlutils, page as page_utils
from cms.utils import i18n, page as page_utils
from cms.utils import timezone
from cms.utils.copy_plugins import copy_plugins_to
from cms.utils.helpers import reversion_register
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.translation import get_language, ugettext_lazy as _
from menus.menu_pool import menu_pool
Expand Down Expand Up @@ -615,6 +614,8 @@ def get_languages(self):
self.all_languages = map(str, self.all_languages)
return self.all_languages

### MPTT properties cache

def get_cached_ancestors(self, ascending=True):
if ascending:
if not hasattr(self, "ancestors_ascending"):
Expand All @@ -625,6 +626,11 @@ def get_cached_ancestors(self, ascending=True):
self.ancestors_descending = list(self.get_ancestors(ascending))
return self.ancestors_descending

def get_cached_descendants(self):
if not hasattr(self, "_cached_descendants"):
self._cached_descendants = list(self.get_descendants())
return self._cached_descendants

### Title object access

def get_title_obj(self, language=None, fallback=True, version_id=None, force_reload=False):
Expand Down Expand Up @@ -773,54 +779,8 @@ def get_template_name(self):
return _("default")

def has_view_permission(self, request):
from cms.models.permissionmodels import PagePermission, GlobalPagePermission
from cms.utils.plugins import current_site

if not self.publisher_is_draft:
return self.publisher_draft.has_view_permission(request)
# does any restriction exist?
# inherited and direct
is_restricted = PagePermission.objects.for_page(page=self).filter(can_view=True).exists()
if request.user.is_authenticated():
site = current_site(request)
global_perms_q = Q(can_view=True) & Q(
Q(sites__in=[site]) | Q(sites__isnull=True)
)
global_view_perms = GlobalPagePermission.objects.with_user(
request.user).filter(global_perms_q).exists()

# a global permission was given to the request's user
if global_view_perms:
return True

elif not is_restricted:
if ((settings.CMS_PUBLIC_FOR == 'all') or
(settings.CMS_PUBLIC_FOR == 'staff' and
request.user.is_staff)):
return True

# a restricted page and an authenticated user
elif is_restricted:
opts = self._meta
codename = '%s.view_%s' % (opts.app_label, opts.object_name.lower())
user_perm = request.user.has_perm(codename)
generic_perm = self.has_generic_permission(request, "view")
return (user_perm or generic_perm)

else:
#anonymous user
if is_restricted or not settings.CMS_PUBLIC_FOR == 'all':
# anyonymous user, page has restriction and global access is permitted
return False
else:
# anonymous user, no restriction saved in database
return True
# Authenticated user
# Django wide auth perms "can_view" or cms auth perms "can_view"
opts = self._meta
codename = '%s.view_%s' % (opts.app_label, opts.object_name.lower())
return (request.user.has_perm(codename) or
self.has_generic_permission(request, "view"))
from cms.utils.permissions import has_view_permission
return has_view_permission(request, self)

def has_change_permission(self, request):
opts = self._meta
Expand Down

0 comments on commit 46bd3b6

Please sign in to comment.