Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Proposed fix & changes to GlobalPagePermissions #1128

Closed
wants to merge 5 commits into from

6 participants

@kezabelle

This pull request should cover both the fixing & testing of the problem in #1120; it goes slightly further than that and refactors some other usages to be consistent with the logic the admin help_text suggests, but the coverage output seems to indicate that those changes are already caught by other tests. It introduces a new manager, and a new set test case set.

I'm open to ways to improve or refactor it further, should it be deemed required.

Though git thinks there's 4 relevant commits, it looks like two of those will be merged by some strategy or other.

kezabelle added some commits
cms/tests/permmod.py
((41 lines not shown))
+ 'can_add_page': True,
+ 'can_change_page': True,
+ 'can_delete_page': False
+ }, staffuser)
+
+ gpp = GlobalPagePermission.objects.create(can_add=True, can_change=True,
+ can_delete=False, user=staffuser)
+ # we're querying here to ensure that even though we've created two users
+ # above, we should have successfully filtered to just one perm.
+ self.assertEqual(1, GlobalPagePermission.objects.with_user(staffuser).count())
+
+ # assemble some basic requirements
+ self.req = RequestFactory().get('/')
+ self.req.user = staffuser
+ # this is copying usage in CMSTestCase.
+ self.req.session = self.client.session
@ojii Collaborator
ojii added a note

not happy with this. it possibly leaks state from this test into other tests. I think the best way to do this is to create a new Client instance, instead of RequestFactory.

Is that because it's binding onto self, or copying client.session over? (Or both; looking back, there's no need for the attribute being bound to self) I was basically going for copying cms.test_utils.CMSTestCase.get_request() without pulling in that whole dependency unnecessarily, because realistically, I only want the mock request to pass to the permissions tests below. I'll try and work up a replacement soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@kezabelle

Right, I've updated the test to cover more than it previously did; it now confirms that user 1 has access to all sites, and that user 2 only has access to one of the sites, as reported by utils.permissions.has_page_add_permission et al.

It still does not use a Client instance, because I think it's the completely incorrect usage, and doesn't streamline or improve the test any by getting an extraneous response, and then patching the request on that (which is what CMSTestCase does with the RequestFactory anyway).
The only reason I want a request at all is because certain methods are looking for items in there, which is why I've continued to use the mock request. I also realised that the session requirement was simply that there be a attribute called that, so I've sidestepped contamination by just providing an empty dict.

I've also updated the comments in the test case to reflect the more complicated nature.

Oh, and it makes use of assertNumQueries so there's an obvious place for the tests to fail if the new GlobalPagePermissions manager gets more complex.

@kezabelle kezabelle Merge branch 'develop' of github.com:kezabelle/django-cms into
permissions, fixing various conflicts.

Conflicts resolved in:
	cms/menu.py
	cms/models/pagemodel.py
	cms/tests/permmod.py
d452519
@kezabelle

welp, merged in develop and fixed the conflicts with the big menu changeset, but it appears to have utterly destroyed the diff, which is interesting.

@ojii
Collaborator
@digi604
Collaborator

please rebase this pull request.

@digi604
Collaborator

I think this code needs to work with the new has_global_page_permission(request, site, **filters): function that caches the global page permissions on the request.

@kezabelle

Huh, it's only by accident I've seen the comments above, as GitHub has handily never informed me about them.
I agree it needs a bunch more work now, as things have moved on somewhat since. I'll investigate reworking it.

@digi604
Collaborator

want to get this into 2.4

@digi604
Collaborator

@kezabelle as simple moderator is now merged... i would love to have this in 2.4... could you remerge?

@digi604
Collaborator

merge? please?

@kux kux commented on the diff
cms/models/permissionmodels.py
@@ -51,7 +52,7 @@ class GlobalPagePermission(AbstractPagePermission):
can_recover_page = models.BooleanField(_("can recover pages"), default=True, help_text=_("can recover any deleted page"))
sites = models.ManyToManyField(Site, null=True, blank=True, help_text=_('If none selected, user haves granted permissions to all sites.'), verbose_name=_('sites'))
@kux
kux added a note

Not part of the actual changeset, but it would be nice to replace 'haves' with 'has'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@kux kux commented on the diff
cms/models/managers.py
@@ -251,6 +252,28 @@ def with_can_change_permissions(self, user):
permissions use page.permissions manager.
"""
return self.with_user(user).filter(can_change_permissions=True)
+
+class GlobalPagePermissionManager(BasicPagePermissionManager):
+
+ def user_has_permission(self, user, site_id, perm):
@kux
kux added a note

Since all methods of this class start with user_has_, I would expect them to return a bool rather than a queryset. (has_this, is_that usually sounds like a bool returning function)

I would suggest either:

  • return self.with_user(user).filter(this_site | all_sites).exists()
  • rename methods to get_user_permission
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@kux kux commented on the diff
cms/menu.py
((40 lines not shown))
# 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()
-
+ global_view_perms = GlobalPagePermission.objects.user_has_view_permission(
@kux
kux added a note

Do methods in GlobalPagePermission cover user groups?

Mind the ' Q(group__user=request.user)' filter in the above query which doesn't seem to be covered in GlobalPagePermissionManager.user_has_permission

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@FrankBie

according to the testcases not - if you remove it the tests fail.

@digi604
Collaborator

Anybody wants to give this PR some looooove?

@yakky yakky referenced this pull request from a commit in yakky/django-cms
@yakky yakky Forward porting of #1128 by @kezabelle. Still some tests failing. 5bda939
@yakky yakky referenced this pull request from a commit in yakky/django-cms
@yakky yakky Forward porting of #1128 by @kezabelle. Still some tests failing. ace2dbe
@yakky yakky referenced this pull request from a commit in yakky/django-cms
@yakky yakky Forward porting of #1128 by @kezabelle. Still some tests failing. 433dc40
@yakky yakky referenced this pull request from a commit in yakky/django-cms
@yakky yakky Forward porting of #1128 by @kezabelle. Still some tests failing. b71d508
@yakky yakky referenced this pull request from a commit in yakky/django-cms
@yakky yakky Forward porting of #1128 by @kezabelle. Still some tests failing. bb78a30
@yakky
Collaborator

Closed by #2701

@yakky yakky closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 22, 2011
  1. @kezabelle

    working on fixing global page permissions for issue #1120

    kezabelle authored
    Introduces a new manager, to keep the Q objects in one place.
Commits on Dec 27, 2011
  1. @kezabelle

    test case for #1120

    kezabelle authored
Commits on Jan 2, 2012
  1. @kezabelle
  2. @kezabelle
Commits on Feb 6, 2012
  1. @kezabelle

    Merge branch 'develop' of github.com:kezabelle/django-cms into

    kezabelle authored
    permissions, fixing various conflicts.
    
    Conflicts resolved in:
    	cms/menu.py
    	cms/models/pagemodel.py
    	cms/tests/permmod.py
This page is out of date. Refresh to see the latest.
View
142 cms/menu.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
from cms.apphook_pool import apphook_pool
-from cms.models.moderatormodels import (ACCESS_DESCENDANTS,
+from cms.models.moderatormodels 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
@@ -28,7 +28,7 @@ def get_visible_pages(request, pages, site=None):
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()
@@ -36,13 +36,13 @@ def get_visible_pages(request, pages, site=None):
for page in pages:
# taken from for_page as multiple at once version
page_q = Q(page__tree_id=page.tree_id) & (
- Q(page=page)
+ Q(page=page)
| (Q(page__level__lt=page.level) & (Q(grant_on=ACCESS_DESCENDANTS) | Q(grant_on=ACCESS_PAGE_AND_DESCENDANTS)))
- | (Q(page__level=page.level - 1) & (Q(grant_on=ACCESS_CHILDREN) | Q(grant_on=ACCESS_PAGE_AND_CHILDREN)))
- )
+ | (Q(page__level=page.level - 1) & (Q(grant_on=ACCESS_CHILDREN) | Q(grant_on=ACCESS_PAGE_AND_CHILDREN)))
+ )
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')
@@ -50,58 +50,56 @@ def get_visible_pages(request, pages, site=None):
# 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
+ # 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)
- # add children
- if perm.grant_on in [ACCESS_CHILDREN, ACCESS_PAGE_AND_CHILDREN]:
+ # add children
+ if perm.grant_on in [ACCESS_CHILDREN, ACCESS_PAGE_AND_CHILDREN]:
child_ids = perm.page.get_children().values_list('id', flat=True)
for id in child_ids:
restricted_pages[id].append(perm)
# add descendants
- elif perm.grant_on in [ACCESS_DESCENDANTS, ACCESS_PAGE_AND_DESCENDANTS]:
+ elif perm.grant_on in [ACCESS_DESCENDANTS, ACCESS_PAGE_AND_DESCENDANTS]:
child_ids = perm.page.get_descendants().values_list('id', flat=True)
for id in child_ids:
restricted_pages[id].append(perm)
- # anonymous
+ # anonymous
# no restriction applied at all
- if (not is_auth_user and
- is_setting_public_all and
+ if (not is_auth_user and
+ is_setting_public_all and
not restricted_pages):
- return [page.pk for page in pages]
-
-
+ return [page.pk for page in pages]
+
+
if site is None:
site = current_site(request)
-
+
# 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()
-
+ global_view_perms = GlobalPagePermission.objects.user_has_view_permission(
@kux
kux added a note

Do methods in GlobalPagePermission cover user groups?

Mind the ' Q(group__user=request.user)' filter in the above query which doesn't seem to be covered in GlobalPagePermissionManager.user_has_permission

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ request.user, site.pk).exists()
+
#no page perms edgcase - all visible
if ((is_setting_public_all or (
- is_setting_public_staff and request.user.is_staff))and
+ 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 edgcase - none visible
- elif (is_setting_public_staff and
- not request.user.is_staff and
+ 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
@@ -118,14 +116,14 @@ def has_permission_membership(page):
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
+ # 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
@@ -135,9 +133,9 @@ def has_permission_membership(page):
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
+ # or
# authenticated staff user, no restriction and public for staff
to_add = True
# check group and user memberships to restricted pages
@@ -145,7 +143,7 @@ def has_permission_membership(page):
to_add = True
elif has_global_perm():
to_add = True
- # anonymous user, no restriction
+ # anonymous user, no restriction
elif not is_restricted and is_setting_public_all:
to_add = True
# store it
@@ -156,7 +154,7 @@ def has_permission_membership(page):
def page_to_node(page, home, cut):
'''
Transform a CMS page into a navigation node.
-
+
page: the page you wish to transform
home: a reference to the "home" page (the page with tree_id=1)
cut: Should we cut page from it's parent pages? This means the node will not
@@ -167,28 +165,28 @@ def page_to_node(page, home, cut):
attr = {'soft_root':page.soft_root,
'auth_required':page.login_required,
'reverse_id':page.reverse_id,}
-
+
parent_id = page.parent_id
# Should we cut the Node from its parents?
if home and page.parent_id == home.pk and cut:
parent_id = None
-
+
# possible fix for a possible problem
#if parent_id and not page.parent.get_calculated_status():
# parent_id = None # ????
-
+
if page.limit_visibility_in_menu == None:
attr['visible_for_authenticated'] = True
attr['visible_for_anonymous'] = True
else:
attr['visible_for_authenticated'] = page.limit_visibility_in_menu == 1
attr['visible_for_anonymous'] = page.limit_visibility_in_menu == 2
-
+
if page.pk == home.pk:
attr['is_home'] = True
# Extenders can be either navigation extenders or from apphooks.
- extenders = []
+ extenders = []
if page.navigation_extenders:
extenders.append(page.navigation_extenders)
# Is this page an apphook? If so, we need to handle the apphooks's nodes
@@ -200,40 +198,40 @@ def page_to_node(page, home, cut):
app = apphook_pool.get_apphook(app_name)
for menu in app.menus:
extenders.append(menu.__name__)
-
+
if extenders:
attr['navigation_extenders'] = extenders
-
+
# Do we have a redirectURL?
attr['redirect_url'] = page.get_redirect() # save redirect URL if any
-
+
# Now finally, build the NavigationNode object and return it.
ret_node = NavigationNode(
- page.get_menu_title(),
- page.get_absolute_url(),
- page.pk,
- parent_id,
+ page.get_menu_title(),
+ page.get_absolute_url(),
+ page.pk,
+ parent_id,
attr=attr,
visible=page.in_navigation,
)
return ret_node
class CMSMenu(Menu):
-
+
def get_nodes(self, request):
page_queryset = get_page_queryset(request)
site = Site.objects.get_current()
lang = get_language_from_request(request)
-
+
filters = {
'site':site,
}
-
+
if settings.CMS_HIDE_UNTRANSLATED:
filters['title_set__language'] = lang
-
+
pages = page_queryset.published().filter(**filters).order_by("tree_id", "lft")
-
+
ids = []
nodes = []
first = True
@@ -241,7 +239,7 @@ def get_nodes(self, request):
home_children = []
home = None
actual_pages = []
-
+
# cache view perms
visible_pages = get_visible_pages(request, pages, site)
for page in pages:
@@ -287,7 +285,7 @@ def get_nodes(self, request):
break
if not ids:
break
- return nodes
+ return nodes
menu_pool.register_menu(CMSMenu)
class NavExtender(Modifier):
@@ -323,7 +321,7 @@ def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
for node in nodes:
if node.namespace == menu[0]:
removed.append(node)
- if breadcrumb:
+ if breadcrumb:
# if breadcrumb and home not in navigation add node
if breadcrumb and home and not home.visible:
home.visible = True
@@ -331,36 +329,36 @@ def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
home.selected = True
else:
home.selected = False
- # remove all nodes that are nav_extenders and not assigned
+ # remove all nodes that are nav_extenders and not assigned
for node in removed:
nodes.remove(node)
- return nodes
+ return nodes
menu_pool.register_modifier(NavExtender)
class SoftRootCutter(Modifier):
"""
Ask evildmp/superdmp if you don't understand softroots!
-
+
Softroot description from the docs:
-
+
A soft root is a page that acts as the root for a menu navigation tree.
-
+
Typically, this will be a page that is the root of a significant new
section on your site.
-
+
When the soft root feature is enabled, the navigation menu for any page
will start at the nearest soft root, rather than at the real root of
the site’s page hierarchy.
-
+
This feature is useful when your site has deep page hierarchies (and
therefore multiple levels in its navigation trees). In such a case, you
usually don’t want to present site visitors with deep menus of nested
items.
-
+
For example, you’re on the page -Introduction to Bleeding-?, so the menu
might look like this:
-
+
School of Medicine
Medical Education
Departments
@@ -386,12 +384,12 @@ class SoftRootCutter(Modifier):
Administration
Contact us
Impressum
-
+
which is frankly overwhelming.
-
+
By making -Department of Mediaeval Surgery-? a soft root, the menu
becomes much more manageable:
-
+
Department of Mediaeval Surgery
Theory
Cures
@@ -417,7 +415,7 @@ def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
selected = node
if not node.parent:
root_nodes.append(node)
-
+
# if we found a selected ...
if selected:
# and the selected is a softroot
@@ -432,19 +430,19 @@ def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
# if it's not a soft root, walk ancestors (upwards!)
nodes = self.find_ancestors_and_remove_children(selected, nodes)
return nodes
-
+
def find_and_remove_children(self, node, nodes):
for n in node.children:
if n.attr.get("soft_root", False):
self.remove_children(n, nodes)
return nodes
-
+
def remove_children(self, node, nodes):
for n in node.children:
nodes.remove(n)
self.remove_children(n, nodes)
node.children = []
-
+
def find_ancestors_and_remove_children(self, node, nodes):
"""
Check ancestors of node for soft roots
@@ -464,5 +462,5 @@ def find_ancestors_and_remove_children(self, node, nodes):
if n != node:
self.find_and_remove_children(n, nodes)
return nodes
-
+
menu_pool.register_modifier(SoftRootCutter)
View
31 cms/models/managers.py
@@ -237,7 +237,8 @@ def set_or_create(self, request, page, form, language):
class BasicPagePermissionManager(models.Manager):
"""Global page permission manager accessible under objects.
- !IMPORTANT: take care, PagePermissionManager extends this manager
+ !IMPORTANT: take care, PagePermissionManager and GlobalPagePermissionManager both
+ inherit from this manager
"""
def with_user(self, user):
"""Get all objects for given user, also takes look if user is in some
@@ -251,6 +252,28 @@ def with_can_change_permissions(self, user):
permissions use page.permissions manager.
"""
return self.with_user(user).filter(can_change_permissions=True)
+
+class GlobalPagePermissionManager(BasicPagePermissionManager):
+
+ def user_has_permission(self, user, site_id, perm):
@kux
kux added a note

Since all methods of this class start with user_has_, I would expect them to return a bool rather than a queryset. (has_this, is_that usually sounds like a bool returning function)

I would suggest either:

  • return self.with_user(user).filter(this_site | all_sites).exists()
  • rename methods to get_user_permission
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ ''' Provide a single point of entry for deciding whether any given global
+ permission exists.
+ '''
+ # if the user has add rights to this site explicitly
+ this_site = Q(**{perm: True, 'sites__in':[site_id]})
+ # if the user can add to all sites
+ all_sites = Q(**{perm: True, 'sites__isnull': True})
+ return self.with_user(user).filter(this_site | all_sites)
+
+ def user_has_add_permission(self, user, site_id):
+ return self.user_has_permission(user, site_id, 'can_add')
+
+ def user_has_change_permission(self, user, site_id):
+ return self.user_has_permission(user, site_id, 'can_change')
+
+ def user_has_view_permission(self, user, site_id):
+ return self.user_has_permission(user, site_id, 'can_view')
+
class PagePermissionManager(BasicPagePermissionManager):
"""Page permission manager accessible under objects.
@@ -470,10 +493,8 @@ def __get_id_list(self, user, site, attr):
if cached is not None:
return cached
# check global permissions
- global_permissions = GlobalPagePermission.objects.with_user(user)
- if global_permissions.filter(**{
- attr: True, 'sites__in':[site]
- }).exists():
+ global_perm = GlobalPagePermission.objects.user_has_permission(user, site, attr).exists()
+ if global_perm:
# user or his group are allowed to do `attr` action
# !IMPORTANT: page permissions must not override global permissions
return PagePermissionsPermissionManager.GRANT_ALL
View
252 cms/models/pagemodel.py
@@ -38,7 +38,7 @@ class Page(MPTTModel):
MODERATOR_APPROVED = 10
# special case - page was approved, but some of page parents are not approved yet
MODERATOR_APPROVED_WAITING_FOR_PARENTS = 11
-
+
moderator_state_choices = (
(MODERATOR_CHANGED, _('changed')),
(MODERATOR_NEED_APPROVEMENT, _('req. app.')),
@@ -46,7 +46,7 @@ class Page(MPTTModel):
(MODERATOR_APPROVED, _('approved')),
(MODERATOR_APPROVED_WAITING_FOR_PARENTS, _('app. par.')),
)
-
+
LIMIT_VISIBILITY_IN_MENU_CHOICES = (
(1,_('for logged in users only')),
(2,_('for anonymous users only')),
@@ -54,9 +54,9 @@ class Page(MPTTModel):
PUBLISHER_STATE_DEFAULT = 0
PUBLISHER_STATE_DIRTY = 1
PUBLISHER_STATE_DELETE = 2
-
+
template_choices = [(x, _(y)) for x,y in settings.CMS_TEMPLATES]
-
+
created_by = models.CharField(_("created by"), max_length=70, editable=False)
changed_by = models.CharField(_("changed by"), max_length=70, editable=False)
parent = models.ForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
@@ -69,29 +69,29 @@ class Page(MPTTModel):
reverse_id = models.CharField(_("id"), max_length=40, db_index=True, blank=True, null=True, help_text=_("An unique identifier that is used with the page_url templatetag for linking to this page"))
navigation_extenders = models.CharField(_("attached menu"), max_length=80, db_index=True, blank=True, null=True)
published = models.BooleanField(_("is published"), blank=True)
-
+
template = models.CharField(_("template"), max_length=100, choices=template_choices, help_text=_('The template used to render the content.'))
site = models.ForeignKey(Site, help_text=_('The site the page is accessible at.'), verbose_name=_("site"))
-
+
moderator_state = models.SmallIntegerField(_('moderator state'), choices=moderator_state_choices, default=MODERATOR_NEED_APPROVEMENT, blank=True)
-
+
level = models.PositiveIntegerField(db_index=True, editable=False)
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)
-
+
login_required = models.BooleanField(_("login required"), default=False)
limit_visibility_in_menu = models.SmallIntegerField(_("menu visibility"), default=None, null=True, blank=True, choices=LIMIT_VISIBILITY_IN_MENU_CHOICES, db_index=True, help_text=_("limit when this page is visible in the menu"))
-
+
# Placeholders (plugins)
placeholders = models.ManyToManyField(Placeholder, editable=False)
-
+
# Publisher fields
publisher_is_draft = models.BooleanField(default=1, editable=False, db_index=True)
publisher_public = models.OneToOneField('self', related_name='publisher_draft', null=True, editable=False)
publisher_state = models.SmallIntegerField(default=0, editable=False, db_index=True)
-
+
# Managers
objects = PageManager()
permissions = PagePermissionsPermissionManager()
@@ -104,13 +104,13 @@ class Meta:
verbose_name_plural = _('pages')
ordering = ('site','tree_id', 'lft')
app_label = 'cms'
-
+
class PublisherMeta:
exclude_fields_append = ['id', 'publisher_is_draft', 'publisher_public',
'publisher_state', 'moderator_state',
'placeholders', 'lft', 'rght', 'tree_id',
'parent']
-
+
def __unicode__(self):
title = self.get_menu_title(fallback=True)
if title is None:
@@ -126,7 +126,7 @@ def get_absolute_url(self, language=None, fallback=True):
# else
path = self.get_path(language, fallback)
return urlutils.urljoin(reverse('pages-root'), path)
-
+
def move_page(self, target, position='first-child'):
"""Called from admin interface when page is moved. Should be used on
all the places which are changing page position. Used like an interface
@@ -138,41 +138,41 @@ def move_page(self, target, position='first-child'):
and self.template == settings.CMS_TEMPLATE_INHERITANCE_MAGIC):
self.template = self.get_template()
self.move_to(target, position)
-
+
# fire signal
from cms.models.moderatormodels import PageModeratorState
self.force_moderation_action = PageModeratorState.ACTION_MOVE
import cms.signals as cms_signals
cms_signals.page_moved.send(sender=Page, instance=self) #titles get saved before moderation
self.save(change_state=True) # always save the page after move, because of publisher
-
+
# check the slugs
page_utils.check_title_slugs(self)
-
+
def copy_page(self, target, site, position='first-child',
copy_permissions=True, copy_moderation=True,
public_copy=False):
"""
copy a page [ and all its descendants to a new location ]
Doesn't checks for add page permissions anymore, this is done in PageAdmin.
-
+
Note: public_copy was added in order to enable the creation of a copy for creating the public page during
the publish operation as it sets the publisher_is_draft=False.
"""
from cms.utils.moderator import update_moderation_message
-
+
page_copy = None
-
-
+
+
if public_copy:
- # create a copy of the draft page - existing code loops through pages so added it to a list
- pages = [copy.copy(self)]
+ # create a copy of the draft page - existing code loops through pages so added it to a list
+ pages = [copy.copy(self)]
else:
pages = [self] + list(self.get_descendants().order_by('-rght'))
-
- if not public_copy:
+
+ if not public_copy:
site_reverse_ids = Page.objects.filter(site=site, reverse_id__isnull=False).values_list('reverse_id', flat=True)
-
+
if target:
target.old_pk = -1
if position == "first-child":
@@ -185,8 +185,8 @@ def copy_page(self, target, site, position='first-child',
tree = []
if tree:
tree[0].old_pk = tree[0].pk
-
-
+
+
first = True
# loop over all affected pages (self is included in descendants)
for page in pages:
@@ -229,7 +229,7 @@ def copy_page(self, target, site, position='first-child',
page.parent = None
tree.append(page)
page.site = site
-
+
# override default page settings specific for public copy
if public_copy:
page.published = True
@@ -237,13 +237,13 @@ def copy_page(self, target, site, position='first-child',
page.moderator_state = Page.MODERATOR_APPROVED
# we need to set relate this new public copy to its draft page (self)
page.publisher_public = self
-
+
# code taken from Publisher publish() overridden here as we need to save the page
# before we are able to use the page object for titles, placeholders etc.. below
# the method has been modified to return the object after saving the instance variable
page = self._publisher_save_public(page)
page_copy = page # create a copy used in the return
- else:
+ else:
# only need to save the page if it isn't public since it is saved above otherwise
page.save()
@@ -260,23 +260,23 @@ def copy_page(self, target, site, position='first-child',
moderator.pk = None
moderator.page = page
moderator.save()
-
+
# update moderation message for standard copy
if not public_copy:
update_moderation_message(page, unicode(_('Page was copied.')))
-
+
# copy titles of this page
for title in titles:
title.pk = None # setting pk = None creates a new instance
title.publisher_public_id = None
title.published = False
title.page = page
-
+
# create slug-copy for standard copy
if not public_copy:
title.slug = page_utils.get_available_slug(title)
title.save()
-
+
# copy the placeholders (and plugins on those placeholders!)
for ph in placeholders:
plugins = list(ph.cmsplugin_set.all().order_by('tree_id', '-rght'))
@@ -288,8 +288,8 @@ def copy_page(self, target, site, position='first-child',
page.placeholders.add(ph)
if plugins:
copy_plugins_to(plugins, ph)
-
-
+
+
# invalidate the menu for this site
menu_pool.clear(site_id=site.pk)
return page_copy # return the page_copy or None
@@ -298,22 +298,22 @@ def save(self, no_signals=False, change_state=True, commit=True,
force_with_moderation=False, force_state=None, **kwargs):
"""
Args:
-
+
commit: True if model should be really saved
- force_with_moderation: can be true when new object gets added under
- some existing page and this new page will require moderation;
+ force_with_moderation: can be true when new object gets added under
+ some existing page and this new page will require moderation;
this is because of how this adding works - first save, then move
"""
-
+
# Published pages should always have a publication date
publish_directly, under_moderation = False, False
if self.publisher_is_draft:
- # publisher specific stuff, but only on draft model, this is here
+ # publisher specific stuff, but only on draft model, this is here
# because page initializes publish process
-
+
if settings.CMS_MODERATOR:
under_moderation = force_with_moderation or self.pk and bool(self.get_moderator_queryset().count())
-
+
created = not bool(self.pk)
if settings.CMS_MODERATOR:
if change_state:
@@ -323,27 +323,27 @@ def save(self, no_signals=False, change_state=True, commit=True,
elif not self.requires_approvement():
# always change state to need approvement when there is some change
self.moderator_state = Page.MODERATOR_NEED_APPROVEMENT
-
+
if not under_moderation and (self.published or self.publisher_public):
- # existing page without moderator - publish it directly if
+ # existing page without moderator - publish it directly if
# published is True
publish_directly = True
-
+
elif change_state:
self.moderator_state = Page.MODERATOR_CHANGED
#publish_directly = True - no publisher, no publishing!! - we just
# use draft models in this case
-
+
if force_state is not None:
self.moderator_state = force_state
-
+
# if the page is published we set the publish date if not set yet.
if self.publication_date is None and self.published:
self.publication_date = datetime.now()
-
+
if self.reverse_id == "":
self.reverse_id = None
-
+
from cms.utils.permissions import _thread_locals
user = getattr(_thread_locals, "user", None)
if user:
@@ -352,20 +352,20 @@ def save(self, no_signals=False, change_state=True, commit=True,
self.changed_by = "script"
if not self.pk:
self.created_by = self.changed_by
-
+
if commit:
if no_signals:# ugly hack because of mptt
self.save_base(cls=self.__class__, **kwargs)
else:
super(Page, self).save(**kwargs)
-
+
#if commit and (publish_directly or created and not under_moderation):
if self.publisher_is_draft:
if self.published:
if commit and publish_directly:
-
+
self.publish()
-
+
def save_base(self, *args, **kwargs):
"""Overriden save_base. If an instance is draft, and was changed, mark
it as dirty.
@@ -386,7 +386,7 @@ def save_base(self, *args, **kwargs):
def publish(self):
"""Overrides Publisher method, because there may be some descendants, which
- are waiting for parent to publish, so publish them if possible.
+ are waiting for parent to publish, so publish them if possible.
IMPORTANT: @See utils.moderator.approve_page for publishing permissions
@@ -480,12 +480,12 @@ def delete(self):
"""Mark public instance for deletion and delete draft.
"""
placeholders = self.placeholders.all()
-
+
for ph in placeholders:
plugin = CMSPlugin.objects.filter(placeholder=ph)
plugin.delete()
ph.delete()
-
+
if self.publisher_public_id:
# mark the public instance for deletion
self.publisher_public.publisher_state = self.PUBLISHER_STATE_DELETE
@@ -494,11 +494,11 @@ def delete(self):
def delete_with_public(self):
-
+
placeholders = list(self.placeholders.all())
if self.publisher_public_id:
placeholders = placeholders + list(self.publisher_public.placeholders.all())
-
+
for ph in placeholders:
plugin = CMSPlugin.objects.filter(placeholder=ph)
plugin.delete()
@@ -506,13 +506,13 @@ def delete_with_public(self):
if self.publisher_public_id:
self.publisher_public.delete()
super(Page, self).delete()
-
+
def get_draft_object(self):
return self
def get_public_object(self):
return self.publisher_public
-
+
def get_languages(self):
"""
get the list of all existing languages for this page
@@ -522,30 +522,30 @@ def get_languages(self):
if not hasattr(self, "all_languages"):
self.all_languages = Title.objects.filter(page=self).values_list("language", flat=True).distinct()
self.all_languages = list(self.all_languages)
- self.all_languages.sort()
+ self.all_languages.sort()
return self.all_languages
-
+
def get_cached_ancestors(self, ascending=True):
if ascending:
if not hasattr(self, "ancestors_ascending"):
- self.ancestors_ascending = list(self.get_ancestors(ascending))
+ self.ancestors_ascending = list(self.get_ancestors(ascending))
return self.ancestors_ascending
else:
if not hasattr(self, "ancestors_descending"):
self.ancestors_descending = list(self.get_ancestors(ascending))
return self.ancestors_descending
-
+
def get_title_obj(self, language=None, fallback=True, version_id=None, force_reload=False):
- """Helper function for accessing wanted / current title.
+ """Helper function for accessing wanted / current title.
If wanted title doesn't exists, EmptyTitle instance will be returned.
"""
-
+
language = self._get_title_cache(language, fallback, version_id, force_reload)
if language in self.title_cache:
return self.title_cache[language]
from cms.models.titlemodels import EmptyTitle
return EmptyTitle()
-
+
def get_title_obj_attribute(self, attrname, language=None, fallback=True, version_id=None, force_reload=False):
"""Helper function for getting attribute or None from wanted/current title.
"""
@@ -555,7 +555,7 @@ def get_title_obj_attribute(self, attrname, language=None, fallback=True, versio
return attribute
except AttributeError:
return None
-
+
def get_path(self, language=None, fallback=True, version_id=None, force_reload=False):
"""
get the path of the page depending on the given language
@@ -567,13 +567,13 @@ def get_slug(self, language=None, fallback=True, version_id=None, force_reload=F
get the slug of the page depending on the given language
"""
return self.get_title_obj_attribute("slug", language, fallback, version_id, force_reload)
-
+
def get_title(self, language=None, fallback=True, version_id=None, force_reload=False):
"""
get the title of the page depending on the given language
"""
return self.get_title_obj_attribute("title", language, fallback, version_id, force_reload)
-
+
def get_menu_title(self, language=None, fallback=True, version_id=None, force_reload=False):
"""
get the menu title of the page depending on the given language
@@ -582,7 +582,7 @@ def get_menu_title(self, language=None, fallback=True, version_id=None, force_re
if not menu_title:
return self.get_title(language, True, version_id, force_reload)
return menu_title
-
+
def get_page_title(self, language=None, fallback=True, version_id=None, force_reload=False):
"""
get the page title of the page depending on the given language
@@ -603,19 +603,19 @@ def get_meta_keywords(self, language=None, fallback=True, version_id=None, force
get content for the keywords meta tag for the page depending on the given language
"""
return self.get_title_obj_attribute("meta_keywords", language, fallback, version_id, force_reload)
-
+
def get_application_urls(self, language=None, fallback=True, version_id=None, force_reload=False):
"""
get application urls conf for application hook
"""
return self.get_title_obj_attribute("application_urls", language, fallback, version_id, force_reload)
-
+
def get_redirect(self, language=None, fallback=True, version_id=None, force_reload=False):
"""
get redirect
"""
return self.get_title_obj_attribute("redirect", language, fallback, version_id, force_reload)
-
+
def _get_title_cache(self, language, fallback, version_id, force_reload):
if not language:
language = get_language()
@@ -628,8 +628,8 @@ def _get_title_cache(self, language, fallback, version_id, force_reload):
fallback_langs = i18n.get_fallback_languages(language)
for lang in fallback_langs:
if lang in self.title_cache:
- return lang
- load = True
+ return lang
+ load = True
if load:
from cms.models.titlemodels import Title
if version_id:
@@ -643,10 +643,10 @@ def _get_title_cache(self, language, fallback, version_id, force_reload):
else:
title = Title.objects.get_title(self, language, language_fallback=fallback)
if title:
- self.title_cache[title.language] = title
+ self.title_cache[title.language] = title
language = title.language
return language
-
+
def get_template(self):
"""
get the template of this page if defined or if closer parent if
@@ -664,7 +664,7 @@ def get_template(self):
if not template:
template = settings.CMS_TEMPLATES[0][0]
return template
-
+
def get_template_name(self):
"""
get the textual name (2nd parameter in settings.CMS_TEMPLATES)
@@ -674,30 +674,26 @@ def get_template_name(self):
template = self.get_template()
for t in settings.CMS_TEMPLATES:
if t[0] == template:
- return t[1]
+ return t[1]
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 and self.publisher_public:
return self.publisher_public.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()
+ global_view_perms = GlobalPagePermission.objects.user_has_view_permission(
+ request.user, current_site(request)).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
@@ -709,9 +705,9 @@ def has_view_permission(self, request):
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")
+ generic_perm = self.has_generic_permission(request, "view")
return (user_perm or generic_perm)
-
+
else:
#anonymous user
@@ -727,44 +723,44 @@ def has_view_permission(self, request):
codename = '%s.view_%s' % (opts.app_label, opts.object_name.lower())
return (request.user.has_perm(codename) or
self.has_generic_permission(request, "view"))
-
+
def has_change_permission(self, request):
opts = self._meta
if request.user.is_superuser:
return True
return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) and \
self.has_generic_permission(request, "change")
-
+
def has_delete_permission(self, request):
opts = self._meta
if request.user.is_superuser:
return True
return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission()) and \
self.has_generic_permission(request, "delete")
-
+
def has_publish_permission(self, request):
return self.has_generic_permission(request, "publish")
-
+
def has_advanced_settings_permission(self, request):
return self.has_generic_permission(request, "advanced_settings")
-
+
def has_change_permissions_permission(self, request):
"""
Has user ability to change permissions for current page?
"""
return self.has_generic_permission(request, "change_permissions")
-
+
def has_add_permission(self, request):
"""
Has user ability to add page under current page?
"""
return self.has_generic_permission(request, "add")
-
+
def has_move_page_permission(self, request):
"""Has user ability to move current page?
"""
return self.has_generic_permission(request, "move_page")
-
+
def has_moderate_permission(self, request):
"""
Has user ability to moderate current page? If moderation isn't
@@ -773,7 +769,7 @@ def has_moderate_permission(self, request):
if not settings.CMS_MODERATOR:
return False
return self.has_generic_permission(request, "moderate")
-
+
def has_generic_permission(self, request, perm_type):
"""
Return true if the current user has permission on the page.
@@ -789,7 +785,7 @@ def has_generic_permission(self, request, perm_type):
if getattr(self, att_name):
self.permission_edit_cache = True
return getattr(self, att_name)
-
+
def is_home(self):
if self.parent_id:
return False
@@ -799,66 +795,66 @@ def is_home(self):
except NoHomeFound:
pass
return False
-
+
def get_home_pk_cache(self):
attr = "%s_home_pk_cache_%s" % (self.publisher_is_draft and "draft" or "public", self.site_id)
if not hasattr(self, attr):
setattr(self, attr, self.get_object_queryset().get_home(self.site).pk)
return getattr(self, attr)
-
+
def set_home_pk_cache(self, value):
attr = "%s_home_pk_cache_%s" % (self.publisher_is_draft and "draft" or "public", self.site_id)
setattr(self, attr, value)
home_pk_cache = property(get_home_pk_cache, set_home_pk_cache)
-
+
def get_media_path(self, filename):
"""
Returns path (relative to MEDIA_ROOT/MEDIA_URL) to directory for storing page-scope files.
This allows multiple pages to contain files with identical names without namespace issues.
- Plugins such as Picture can use this method to initialise the 'upload_to' parameter for
+ Plugins such as Picture can use this method to initialise the 'upload_to' parameter for
File-based fields. For example:
image = models.ImageField(_("image"), upload_to=CMSPlugin.get_media_path)
where CMSPlugin.get_media_path calls self.page.get_media_path
-
+
This location can be customised using the CMS_PAGE_MEDIA_PATH setting
"""
return join(settings.CMS_PAGE_MEDIA_PATH, "%d" % self.id, filename)
-
+
def last_page_states(self):
"""Returns last five page states, if they exist, optimized, calls sql
query only if some states available
"""
- # TODO: optimize SQL... 1 query per page
+ # TODO: optimize SQL... 1 query per page
if settings.CMS_MODERATOR:
has_moderator_state = getattr(self, '_has_moderator_state_chache', None)
if has_moderator_state == False:
return self.pagemoderatorstate_set.none()
return self.pagemoderatorstate_set.all().order_by('created',)[:5]
return self.pagemoderatorstate_set.none()
-
+
def get_moderator_queryset(self):
- """Returns ordered set of all PageModerator instances, which should
+ """Returns ordered set of all PageModerator instances, which should
moderate this page
"""
from cms.models.moderatormodels import PageModerator
if not settings.CMS_MODERATOR or not self.tree_id:
return PageModerator.objects.get_empty_query_set()
-
+
q = Q(page__tree_id=self.tree_id, page__level__lt=self.level, moderate_descendants=True) | \
Q(page__tree_id=self.tree_id, page__level=self.level - 1, moderate_children=True) | \
Q(page__pk=self.pk, moderate_page=True)
-
+
return PageModerator.objects.distinct().filter(q).order_by('page__level')
-
+
def is_under_moderation(self):
return bool(self.get_moderator_queryset().count())
-
+
def is_approved(self):
"""Returns true, if page is approved and published, or approved, but
parents are missing..
"""
return self.moderator_state in (Page.MODERATOR_APPROVED, Page.MODERATOR_APPROVED_WAITING_FOR_PARENTS)
-
+
def is_public_published(self):
"""Returns true if public model is published.
"""
@@ -870,16 +866,16 @@ def is_public_published(self):
return self.publisher_public.published
#return is_public_published(self)
return False
-
+
def reload(self):
"""
Reload a page from the database
"""
return Page.objects.get(pk=self.pk)
-
+
def requires_approvement(self):
return self.moderator_state in (Page.MODERATOR_NEED_APPROVEMENT, Page.MODERATOR_NEED_DELETE_APPROVEMENT)
-
+
def get_moderation_value(self, user):
"""Returns page moderation value for given user, moderation value is
sum of moderations.
@@ -891,13 +887,13 @@ def get_moderation_value(self, user):
page_moderator = self.pagemoderator_set.get(user=user)
except ObjectDoesNotExist:
return 0
-
+
moderation_value = page_moderator.get_decimal()
-
+
self._moderation_value_cahce = moderation_value
self._moderation_value_cache_for_user_id = user
-
- return moderation_value
+
+ return moderation_value
def get_object_queryset(self):
"""Returns smart queryset depending on object type - draft / public
@@ -976,7 +972,7 @@ def get_previous_filtered_sibling(self, **filters):
'%s__lt' % opts.right_attr: getattr(self, opts.left_attr),
})
order_by = '-%s' % opts.right_attr
-
+
# publisher stuff
filters.update({
'publisher_is_draft': self.publisher_is_draft
@@ -985,7 +981,7 @@ def get_previous_filtered_sibling(self, **filters):
filters.update({
'site__id': self.site_id
})
-
+
sibling = None
try:
sibling = self._tree_manager.filter(**filters).order_by(order_by)[0]
@@ -1052,7 +1048,7 @@ def _publisher_save_public(self, obj):
# or none structural change, just save
obj.save()
return obj
-
+
def rescan_placeholders(self):
"""
Rescan and if necessary create placeholders in the current template.
@@ -1072,7 +1068,7 @@ def rescan_placeholders(self):
def _reversion():
exclude_fields = ['publisher_is_draft', 'publisher_public', 'publisher_state']
-
+
reversion_register(
Page,
follow=["title_set", "placeholders", "pagepermission_set"],
View
5 cms/models/permissionmodels.py
@@ -6,7 +6,8 @@
from django.contrib.sites.models import Site
from cms.models import Page, ACCESS_CHOICES, ACCESS_PAGE_AND_DESCENDANTS
-from cms.models.managers import BasicPagePermissionManager, PagePermissionManager
+from cms.models.managers import (BasicPagePermissionManager, PagePermissionManager,
+ GlobalPagePermissionManager)
from cms.utils.helpers import reversion_register
class AbstractPagePermission(models.Model):
@@ -51,7 +52,7 @@ class GlobalPagePermission(AbstractPagePermission):
can_recover_page = models.BooleanField(_("can recover pages"), default=True, help_text=_("can recover any deleted page"))
sites = models.ManyToManyField(Site, null=True, blank=True, help_text=_('If none selected, user haves granted permissions to all sites.'), verbose_name=_('sites'))
@kux
kux added a note

Not part of the actual changeset, but it would be nice to replace 'haves' with 'has'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
- objects = BasicPagePermissionManager()
+ objects = GlobalPagePermissionManager()
class Meta:
verbose_name = _('Page global permission')
View
518 cms/tests/permmod.py
@@ -1,17 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
-from cms.api import (create_page, publish_page, approve_page, add_plugin,
+from cms.admin.forms import save_permissions
+from cms.api import (create_page, publish_page, approve_page, add_plugin,
create_page_user, assign_user_to_page)
from cms.models import Page, CMSPlugin
-from cms.models.moderatormodels import (ACCESS_DESCENDANTS,
+from cms.models.moderatormodels import (ACCESS_DESCENDANTS,
ACCESS_PAGE_AND_DESCENDANTS)
from cms.models.permissionmodels import PagePermission, GlobalPagePermission
-from cms.test_utils.testcases import (URL_CMS_PAGE_ADD, URL_CMS_PLUGIN_REMOVE,
+from cms.test_utils.testcases import (URL_CMS_PAGE_ADD, URL_CMS_PLUGIN_REMOVE,
SettingsOverrideTestCase, URL_CMS_PLUGIN_ADD, CMSTestCase)
from cms.test_utils.util.context_managers import SettingsOverride
+from cms.test_utils.util.request_factory import RequestFactory
from cms.utils.page_resolver import get_page_from_path
-from cms.utils.permissions import has_generic_permission
-
+from cms.utils.permissions import (has_page_add_permission, has_page_change_permission,
+ has_generic_permission)
+from django.contrib.admin.sites import site
from django.contrib.auth.models import User, Permission, AnonymousUser, Group
from django.contrib.sites.models import Site
from django.core.management import call_command
@@ -21,41 +24,41 @@ class PermissionModeratorTests(SettingsOverrideTestCase):
"""Permissions and moderator together
Fixtures contains 3 users and 1 published page and some other stuff
-
+
Users:
1. `super`: superuser
2. `master`: user with permissions to all applications
3. `slave`: user assigned to page `slave-home`
-
+
Pages:
1. `home`:
- published page
- master can do anything on its subpages, but not on home!
-
+
2. `master`:
- published page
- crated by super
- `master` can do anything on it and its descendants
- subpages:
-
+
3. `slave-home`:
- not published
- assigned slave user which can add/change/delete/
move/publish/moderate this page and its descendants
- `master` user want to moderate this page and all descendants
-
+
4. `pageA`:
- created by super
- - master can add/change/delete on it and descendants
+ - master can add/change/delete on it and descendants
"""
settings_overrides = {
'CMS_PERMISSION': True,
'CMS_MODERATOR': True,
}
-
+
def setUp(self):
# create super user
- self.user_super = User(username="super", is_staff=True, is_active=True,
+ self.user_super = User(username="super", is_staff=True, is_active=True,
is_superuser=True)
self.user_super.set_password("super")
self.user_super.save()
@@ -63,16 +66,16 @@ def setUp(self):
self.user_staff = User(username="staff", is_staff=True, is_active=True)
self.user_staff.set_password("staff")
self.user_staff.save()
-
+
with self.login_user_context(self.user_super):
-
+
self._home_page = create_page("home", "nav_playground.html", "en",
created_by=self.user_super)
-
+
# master page & master user
-
+
self._master_page = create_page("master", "nav_playground.html", "en")
-
+
# create master user
master = User(username="master", email="master@django-cms.org", is_staff=True, is_active=True)
master.set_password('master')
@@ -80,38 +83,38 @@ def setUp(self):
master.user_permissions.add(Permission.objects.get(codename='add_text'))
master.user_permissions.add(Permission.objects.get(codename='delete_text'))
master.user_permissions.add(Permission.objects.get(codename='change_text'))
-
+
self.user_master = create_page_user(self.user_super, master, grant_all=True)
# create non global, non staff user
self.user_non_global = User(username="nonglobal", is_active=True)
self.user_non_global.set_password("nonglobal")
self.user_non_global.save()
-
+
# assign master user under home page
assign_user_to_page(self.home_page, self.user_master,
grant_on=ACCESS_DESCENDANTS, grant_all=True)
-
+
# and to master page
assign_user_to_page(self.master_page, self.user_master,
grant_on=ACCESS_PAGE_AND_DESCENDANTS, grant_all=True)
-
+
# slave page & slave user
-
+
self._slave_page = create_page("slave-home", "nav_playground.html", "en",
parent=self.master_page, created_by=self.user_super)
-
+
slave = User(username='slave', email='slave@django-cms.org', is_staff=True)
slave.set_password('slave')
slave.save()
slave.user_permissions.add(Permission.objects.get(codename='add_text'))
slave.user_permissions.add(Permission.objects.get(codename='delete_text'))
slave.user_permissions.add(Permission.objects.get(codename='change_text'))
-
+
self.user_slave = create_page_user(self.user_super, slave, can_add_page=True,
can_change_page=True, can_delete_page=True)
-
+
assign_user_to_page(self.slave_page, self.user_slave, grant_all=True)
-
+
# create page_b
page_b = create_page("pageB", "nav_playground.html", "en", created_by=self.user_super)
# Normal user
@@ -123,34 +126,34 @@ def setUp(self):
assign_user_to_page(page_b, self.user_normal, can_view=True)
self.user_normal = self.reload(self.user_normal)
# create page_a - sample page from master
-
+
page_a = create_page("pageA", "nav_playground.html", "en",
created_by=self.user_super)
- assign_user_to_page(page_a, self.user_master,
- can_add=True, can_change=True, can_delete=True, can_publish=True,
+ assign_user_to_page(page_a, self.user_master,
+ can_add=True, can_change=True, can_delete=True, can_publish=True,
can_move_page=True, can_moderate=True)
# publish after creating all drafts
publish_page(self.home_page, self.user_super)
-
+
publish_page(self.master_page, self.user_super)
-
+
self.page_b = publish_page(page_b, self.user_super)
# logg in as master, and request moderation for slave page and descendants
self.request_moderation(self.slave_page, 7)
-
+
@property
def master_page(self):
return self.reload(self._master_page)
-
+
@property
def slave_page(self):
return self.reload(self._slave_page)
-
+
@property
def home_page(self):
return self.reload(self._home_page)
-
+
def _add_plugin(self, user, page):
"""
Add a plugin using the test client to check for permissions.
@@ -173,29 +176,29 @@ def test_super_can_add_page_to_root(self):
with self.login_user_context(self.user_super):
response = self.client.get(URL_CMS_PAGE_ADD)
self.assertEqual(response.status_code, 200)
-
+
def test_master_can_add_page_to_root(self):
with self.login_user_context(self.user_master):
response = self.client.get(URL_CMS_PAGE_ADD)
self.assertEqual(response.status_code, 403)
-
+
def test_slave_can_add_page_to_root(self):
with self.login_user_context(self.user_slave):
response = self.client.get(URL_CMS_PAGE_ADD)
self.assertEqual(response.status_code, 403)
-
+
def test_moderation_on_slave_home(self):
self.assertEqual(self.slave_page.get_moderator_queryset().count(), 1)
-
+
def test_slave_can_add_page_under_slave_home(self):
with self.login_user_context(self.user_slave):
# move to admin.py?
# url = URL_CMS_PAGE_ADD + "?target=%d&position=last-child" % slave_page.pk
-
+
# can he even access it over get?
# response = self.client.get(url)
# self.assertEqual(response.status_code, 200)
-
+
# add page
page = create_page("page", "nav_playground.html", "en",
parent=self.slave_page, created_by=self.user_slave)
@@ -204,19 +207,19 @@ def test_slave_can_add_page_under_slave_home(self):
# removed test cases since Title object does not inherit from Publisher anymore
#self.assertObjectExist(Title.objects, slug=page_data['slug'])
#self.assertObjectDoesNotExist(Title.objects.public(), slug=page_data['slug'])
-
+
# moderators and approvement ok?
self.assertEqual(page.get_moderator_queryset().count(), 1)
self.assertEqual(page.moderator_state, Page.MODERATOR_CHANGED)
-
+
# must not have public object yet
self.assertFalse(page.publisher_public)
-
+
self.assertTrue(has_generic_permission(page.pk, self.user_slave, "publish", 1))
-
- # publish as slave, published as user_master before
+
+ # publish as slave, published as user_master before
publish_page(page, self.user_slave)
-
+
# user_slave is moderator for this page
# approve / publish as user_slave
# user master should be able to approve aswell
@@ -226,32 +229,32 @@ def test_page_added_by_slave_can_be_published_approved_by_user_master(self):
# add page
page = create_page("page", "nav_playground.html", "en",
parent=self.slave_page, created_by=self.user_slave)
- # same as test_slave_can_add_page_under_slave_home
+ # same as test_slave_can_add_page_under_slave_home
self.assertEqual(page.get_moderator_queryset().count(), 1)
self.assertEqual(page.moderator_state, Page.MODERATOR_CHANGED)
-
+
# must not have public object yet
self.assertFalse(page.publisher_public)
-
+
self.assertTrue(has_generic_permission(page.pk, self.user_master, "publish", page.site.pk))
# should be True user_master should have publish permissions for childred aswell
# don't test for published since publishing must be approved
publish_page(page, self.user_master)
-
+
# user_master is moderator for top level page / but can't approve descendants?
# approve / publish as user_master
# user master should be able to approve descendants
- page = approve_page(page, self.user_master)
-
+ page = approve_page(page, self.user_master)
+
def test_super_can_add_plugin(self):
self._add_plugin(self.user_super, page=self.slave_page)
-
+
def test_master_can_add_plugin(self):
self._add_plugin(self.user_master, page=self.slave_page)
-
+
def test_slave_can_add_plugin(self):
self._add_plugin(self.user_slave, page=self.slave_page)
-
+
def test_same_order(self):
# create 4 pages
slugs = []
@@ -260,74 +263,74 @@ def test_same_order(self):
parent=self.home_page)
slug = page.title_set.drafts()[0].slug
slugs.append(slug)
-
+
# approve last 2 pages in reverse order
for slug in reversed(slugs[2:]):
page = self.assertObjectExist(Page.objects.drafts(), title_set__slug=slug)
page = publish_page(page, self.user_master, True)
self.check_published_page_attributes(page)
-
+
def test_create_copy_publish(self):
# create new page to copy
page = create_page("page", "nav_playground.html", "en",
parent=self.slave_page)
-
+
# copy it under home page...
# TODO: Use page.copy_page here
with self.login_user_context(self.user_master):
copied_page = self.copy_page(page, self.home_page)
-
+
page = publish_page(copied_page, self.user_master, True)
self.check_published_page_attributes(page)
-
-
+
+
def test_create_publish_copy(self):
# create new page to copy
page = create_page("page", "nav_playground.html", "en",
parent=self.home_page)
-
+
page = publish_page(page, self.user_master, True)
-
+
# copy it under master page...
# TODO: Use page.copy_page here
with self.login_user_context(self.user_master):
copied_page = self.copy_page(page, self.master_page)
-
+
self.check_published_page_attributes(page)
copied_page = publish_page(copied_page, self.user_master, True)
self.check_published_page_attributes(copied_page)
-
-
+
+
def test_subtree_needs_approvement(self):
# create page under slave_page
page = create_page("parent", "nav_playground.html", "en",
parent=self.home_page)
self.assertFalse(page.publisher_public)
-
+
# create subpage uner page
subpage = create_page("subpage", "nav_playground.html", "en", parent=page)
self.assertFalse(subpage.publisher_public)
-
- # publish both of them in reverse order
- subpage = publish_page(subpage, self.user_master, True)
-
- # subpage should not be published, because parent is not published
+
+ # publish both of them in reverse order
+ subpage = publish_page(subpage, self.user_master, True)
+
+ # subpage should not be published, because parent is not published
# yet, should be marked as `publish when parent`
- self.assertFalse(subpage.publisher_public)
-
+ self.assertFalse(subpage.publisher_public)
+
# pagemoderator state must be set
self.assertEqual(subpage.moderator_state, Page.MODERATOR_APPROVED_WAITING_FOR_PARENTS)
-
+
# publish page (parent of subage), so subpage must be published also
page = publish_page(page, self.user_master, True)
self.assertNotEqual(page.publisher_public, None)
-
+
# reload subpage, it was probably changed
subpage = self.reload_page(subpage)
-
+
# parent was published, so subpage must be also published..
- self.assertNotEqual(subpage.publisher_public, None)
-
+ self.assertNotEqual(subpage.publisher_public, None)
+
#check attributes
self.check_published_page_attributes(page)
self.check_published_page_attributes(subpage)
@@ -337,93 +340,93 @@ def test_subtree_with_super(self):
# create page under root
page = create_page("page", "nav_playground.html", "en")
self.assertFalse(page.publisher_public)
-
+
# create subpage under page
subpage = create_page("subpage", "nav_playground.html", "en",
parent=page)
self.assertFalse(subpage.publisher_public)
-
+
# tree id must be the same
self.assertEqual(page.tree_id, subpage.tree_id)
-
- # publish both of them
+
+ # publish both of them
page = self.reload(page)
page = publish_page(page, self.user_super, True)
# reload subpage, there were an tree_id change
subpage = self.reload_page(subpage)
self.assertEqual(page.tree_id, subpage.tree_id)
-
+
subpage = publish_page(subpage, self.user_super, True)
# tree id must stay the same
self.assertEqual(page.tree_id, subpage.tree_id)
-
+
# published pages must also have the same tree_id
self.assertEqual(page.publisher_public.tree_id, subpage.publisher_public.tree_id)
-
+
#check attributes
- self.check_published_page_attributes(page)
+ self.check_published_page_attributes(page)
self.check_published_page_attributes(subpage)
-
-
+
+
def test_super_add_page_to_root(self):
- """Create page which is not under moderation in root, and check if
+ """Create page which is not under moderation in root, and check if
some properties are correct.
"""
# create page under root
page = create_page("page", "nav_playground.html", "en")
-
+
# public must not exist
self.assertFalse(page.publisher_public)
-
+
# moderator_state must be changed
self.assertEqual(page.moderator_state, Page.MODERATOR_CHANGED)
-
-
+
+
def test_moderator_flags(self):
"""Add page under slave_home and check its flag
"""
page = create_page("page", "nav_playground.html", "en",
parent=self.slave_page)
-
+
# moderator_state must be changed
self.assertEqual(page.moderator_state, Page.MODERATOR_CHANGED)
-
+
# check publish box
page = publish_page(page, self.user_slave)
-
+
# page should request approvement now
self.assertEqual(page.moderator_state, Page.MODERATOR_NEED_APPROVEMENT)
-
+
# approve it by master
- # approve this page - but it doesn't get published yet, because
+ # approve this page - but it doesn't get published yet, because
# slave home is not published
page = approve_page(page, self.user_master)
-
+
# public page must not exist because of parent
self.assertFalse(page.publisher_public)
-
+
# waiting for parents
self.assertEqual(page.moderator_state, Page.MODERATOR_APPROVED_WAITING_FOR_PARENTS)
-
+
# publish slave page
slave_page = publish_page(self.slave_page, self.user_master)
-
+
self.assertFalse(page.publisher_public)
self.assertFalse(slave_page.publisher_public)
-
+
# they must be approved first
slave_page = approve_page(slave_page, self.user_master)
-
+
# master is approved
self.assertEqual(slave_page.moderator_state, Page.MODERATOR_APPROVED)
-
+
# reload page
page = self.reload_page(page)
-
+
# page must be approved also now
self.assertEqual(page.moderator_state, Page.MODERATOR_APPROVED)
-
+
def test_plugins_get_published(self):
# create page under root
page = create_page("page", "nav_playground.html", "en")
@@ -438,33 +441,33 @@ def test_remove_plugin_page_under_moderation(self):
# login as slave and create page
page = create_page("page", "nav_playground.html", "en", parent=self.slave_page)
self.assertEqual(page.get_moderator_queryset().count(), 1)
-
+
# add plugin
placeholder = page.placeholders.all()[0]
plugin = add_plugin(placeholder, "TextPlugin", "en", body="test")
-
+
self.assertEqual(page.moderator_state, Page.MODERATOR_CHANGED)
# publish page
page = self.reload(page)
page = publish_page(page, self.user_slave)
-
+
# only the draft plugin should exist
self.assertEqual(CMSPlugin.objects.all().count(), 1)
-
+
# page should require approval
self.assertEqual(page.moderator_state, Page.MODERATOR_NEED_APPROVEMENT)
-
+
# master approves and publishes the page
# first approve slave-home
slave_page = self.reload(self.slave_page)
publish_page(slave_page, self.user_master, approve=True)
page = self.reload(page)
page = publish_page(page, self.user_master, approve=True)
-
+
# draft and public plugins should now exist
self.assertEqual(CMSPlugin.objects.all().count(), 2)
-
+
# login as slave and delete the plugin - should require moderation
with self.login_user_context(self.user_slave):
plugin_data = {
@@ -473,20 +476,20 @@ def test_remove_plugin_page_under_moderation(self):
remove_url = URL_CMS_PLUGIN_REMOVE
response = self.client.post(remove_url, plugin_data)
self.assertEquals(response.status_code, 200)
-
+
# there should only be a public plugin - since the draft has been deleted
self.assertEquals(CMSPlugin.objects.all().count(), 1)
-
+
# reload the page as it's moderator value should have been set in pageadmin.remove_plugin
self.assertEqual(page.moderator_state, Page.MODERATOR_APPROVED)
page = self.reload_page(page)
-
+
self.assertEqual(page.moderator_state, Page.MODERATOR_NEED_APPROVEMENT)
-
+
# login as super user and approve/publish the page
page = publish_page(page, self.user_super, approve=True)
self.assertEqual(page.moderator_state, Page.MODERATOR_APPROVED)
-
+
# there should now be 0 plugins
self.assertEquals(CMSPlugin.objects.all().count(), 0)
@@ -515,7 +518,7 @@ def test_staff_can_view(self):
self.assertEquals(login_user_id,user.id)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
-
+
def test_user_normal_can_view(self):
url = self.page_b.get_absolute_url(language='en')
all_view_perms = PagePermission.objects.filter(can_view=True)
@@ -529,7 +532,7 @@ def test_user_normal_can_view(self):
with self.login_user_context(self.user_normal):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
-
+
# verifiy that the user_non_global has not access to this page
has_perm = False
for perm in all_view_perms:
@@ -556,9 +559,9 @@ def test_user_globalpermission(self):
published=True)
global_page = publish_page(global_page, user_global, approve=True)
# it's allowed for the normal user to view the page
- assign_user_to_page(global_page, user_global,
+ assign_user_to_page(global_page, user_global,
global_permission=True, can_view=True)
-
+
url = global_page.get_absolute_url('en')
all_view_perms = PagePermission.objects.filter(can_view=True)
has_perm = False
@@ -566,11 +569,11 @@ def test_user_globalpermission(self):
if perm.page == self.page_b and perm.user == user_global:
has_perm = True
self.assertEqual(has_perm, False)
-
+
global_page_perm_q = Q(user=user_global) & Q(can_view=True)
global_view_perms = GlobalPagePermission.objects.filter(global_page_perm_q).exists()
self.assertEqual(global_view_perms, True)
-
+
# user_global
with self.login_user_context(user_global):
response = self.client.get(url)
@@ -585,7 +588,7 @@ def test_user_globalpermission(self):
global_page_perm_q = Q(user=self.user_non_global) & Q(can_view=True)
global_view_perms = GlobalPagePermission.objects.filter(global_page_perm_q).exists()
self.assertEqual(global_view_perms, False)
-
+
with self.login_user_context(self.user_non_global):
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
@@ -603,71 +606,71 @@ def test_anonymous_user_public_for_none(self):
with SettingsOverride(CMS_PUBLIC_FOR=None):
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
-
+
class