Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Implement smart group caching #11

Merged
merged 7 commits into from
This page is out of date. Refresh to see the latest.
View
2  README
@@ -52,7 +52,7 @@ html version using the setup.py::
Changelog:
==========
-0.5dev (2012-09-24):
+0.5 (2013-03-18):
-----------------
* It is now possible to minimize the number of queries when using
View
101 authority/permissions.py
@@ -42,7 +42,7 @@ def __init__(self, user=None, group=None, *args, **kwargs):
self.group = group
super(BasePermission, self).__init__(*args, **kwargs)
- def _get_cached_perms(self):
+ def _get_user_cached_perms(self):
"""
Set up both the user and group caches.
"""
@@ -73,18 +73,46 @@ def _get_cached_perms(self):
)] = True
return user_permissions, group_permissions
- def _prime_perm_caches(self):
+ def _get_group_cached_perms(self):
+ """
+ Set group cache.
+ """
+ if not self.group:
+ return {}
+ perms = Permission.objects.filter(
+ group=self.group,
+ )
+ group_permissions = {}
+ for perm in perms:
+ group_permissions[(
+ perm.object_id,
+ perm.content_type_id,
+ perm.codename,
+ perm.approved,
+ )] = True
+ return group_permissions
+
+ def _prime_user_perm_caches(self):
"""
Prime both the user and group caches and put them on the ``self.user``.
In addition add a cache filled flag on ``self.user``.
"""
- perm_cache, group_perm_cache = self._get_cached_perms()
+ perm_cache, group_perm_cache = self._get_user_cached_perms()
self.user._authority_perm_cache = perm_cache
self.user._authority_group_perm_cache = group_perm_cache
self.user._authority_perm_cache_filled = True
+ def _prime_group_perm_caches(self):
+ """
+ Prime the group cache and put them on the ``self.group``.
+ In addition add a cache filled flag on ``self.group``.
+ """
+ perm_cache = self._get_group_cached_perms()
+ self.group._authority_perm_cache = perm_cache
+ self.group._authority_perm_cache_filled = True
+
@property
- def _perm_cache(self):
+ def _user_perm_cache(self):
"""
cached_permissions will generate the cache in a lazy fashion.
"""
@@ -102,7 +130,7 @@ def _perm_cache(self):
return self.user._authority_perm_cache
# Prime the cache.
- self._prime_perm_caches()
+ self._prime_user_perm_caches()
return self.user._authority_perm_cache
@property
@@ -111,6 +139,28 @@ def _group_perm_cache(self):
cached_permissions will generate the cache in a lazy fashion.
"""
# Check to see if the cache has been primed.
+ if not self.group:
+ return {}
+ cache_filled = getattr(
+ self.group,
+ '_authority_perm_cache_filled',
+ False,
+ )
+ if cache_filled:
+ # Don't really like the name for this, but this matches how Django
+ # does it.
+ return self.group._authority_perm_cache
+
+ # Prime the cache.
+ self._prime_group_perm_caches()
+ return self.group._authority_perm_cache
+
+ @property
+ def _user_group_perm_cache(self):
+ """
+ cached_permissions will generate the cache in a lazy fashion.
+ """
+ # Check to see if the cache has been primed.
if not self.user:
return {}
cache_filled = getattr(
@@ -122,7 +172,7 @@ def _group_perm_cache(self):
return self.user._authority_group_perm_cache
# Prime the cache.
- self._prime_perm_caches()
+ self._prime_user_perm_caches()
return self.user._authority_group_perm_cache
def invalidate_permissions_cache(self):
@@ -135,13 +185,15 @@ def invalidate_permissions_cache(self):
"""
if self.user:
self.user._authority_perm_cache_filled = False
+ if self.group:
+ self.group._authority_perm_cache_filled = False
@property
def use_smart_cache(self):
# AUTHORITY_USE_SMART_CACHE defaults to False to maintain backwards
# compatibility.
use_smart_cache = getattr(settings, 'AUTHORITY_USE_SMART_CACHE', True)
- return self.user and use_smart_cache
+ return (self.user or self.group) and use_smart_cache
def has_user_perms(self, perm, obj, approved, check_groups=True):
if not self.user:
@@ -164,12 +216,12 @@ def _user_has_perms(cached_perms):
))
# Check to see if the permission is in the cache.
- if _user_has_perms(self._perm_cache):
+ if _user_has_perms(self._user_perm_cache):
return True
# Optionally check group permissions
if check_groups:
- return _user_has_perms(self._group_perm_cache)
+ return _user_has_perms(self._user_group_perm_cache)
return False
# Actually hit the DB, no smart cache used.
@@ -187,11 +239,32 @@ def has_group_perms(self, perm, obj, approved):
"""
Check if group has the permission for the given object
"""
- if self.group:
- perms = Permission.objects.group_permissions(self.group, perm, obj,
- approved)
- return perms.filter(object_id=obj.pk)
- return False
+ if not self.group:
+ return False
+
+ if self.use_smart_cache:
+ content_type_pk = Permission.objects.get_content_type(obj).pk
+
+ def _group_has_perms(cached_perms):
+ # Check to see if the permission is in the cache.
+ return cached_perms.get((
+ obj.pk,
+ content_type_pk,
+ perm,
+ approved,
+ ))
+
+ # Check to see if the permission is in the cache.
+ return _group_has_perms(self._group_perm_cache)
+
+ # Actually hit the DB, no smart cache used.
+ return Permission.objects.group_permissions(
+ self.group,
+ perm, obj,
+ approved,
+ ).filter(
+ object_id=obj.pk,
+ ).exists()
def has_perm(self, perm, obj, check_groups=True, approved=True):
"""
View
121 authority/tests.py
@@ -2,6 +2,7 @@
from django.contrib.auth.models import Permission as DjangoPermission
from django.contrib.auth.models import User, Group
from django.test import TestCase
+from django.contrib.contenttypes.models import ContentType
import authority
from authority import permissions
@@ -181,7 +182,10 @@ def setUp(self):
# Ensure we are using the smart cache.
settings.AUTHORITY_USE_SMART_CACHE = True
- def _old_permission_check(self):
+ def tearDown(self):
+ ContentType.objects.clear_cache()
+
+ def _old_user_permission_check(self):
# This is what the old, pre-cache system would check to see if a user
# had a given permission.
return Permission.objects.user_permissions(
@@ -192,6 +196,16 @@ def _old_permission_check(self):
check_groups=True,
)
+ def _old_group_permission_check(self):
+ # This is what the old, pre-cache system would check to see if a user
+ # had a given permission.
+ return Permission.objects.group_permissions(
+ self.group,
+ 'foo',
+ self.group,
+ approved=True,
+ )
+
class PerformanceTest(SmartCachingTestCase):
"""
@@ -209,15 +223,31 @@ def test_has_user_perms(self):
# Regardless of how many times has_user_perms is called, the number of
# queries is the same.
- with self.assertNumQueries(1):
- self.user_check.has_user_perms('foo', self.user, True, False)
- self.user_check.has_user_perms('foo', self.user, True, False)
- self.user_check.has_user_perms('foo', self.user, True, False)
+ # Content type and permissions (2 queries)
+ with self.assertNumQueries(2):
+ for _ in range(5):
+ # Need to assert it so the query actually gets executed.
+ assert not self.user_check.has_user_perms(
+ 'foo',
+ self.user,
+ True,
+ False,
+ )
+
+ def test_group_has_perms(self):
+ with self.assertNumQueries(2):
+ for _ in range(5):
+ assert not self.group_check.has_group_perms(
+ 'foo',
+ self.group,
+ True,
+ )
def test_has_user_perms_check_group(self):
# Regardless of the number groups permissions, it should only take one
# query to check both users and groups.
- with self.assertNumQueries(1):
+ # Content type and permissions (2 queries)
+ with self.assertNumQueries(2):
self.user_check.has_user_perms(
'foo',
self.user,
@@ -225,18 +255,60 @@ def test_has_user_perms_check_group(self):
check_groups=True,
)
- def test_invalidate_permissions_cache(self):
+ def test_invalidate_user_permissions_cache(self):
# Show that calling invalidate_permissions_cache will cause extra
# queries.
- with self.assertNumQueries(2):
- self.user_check.has_user_perms('foo', self.user, True, False)
+ # For each time invalidate_permissions_cache gets called, you
+ # will need to do one query to get content type and one to get
+ # the permissions.
+ with self.assertNumQueries(4):
+ for _ in range(5):
+ assert not self.user_check.has_user_perms(
+ 'foo',
+ self.user,
+ True,
+ False,
+ )
# Invalidate the cache to show that a query will be generated when
# checking perms again.
self.user_check.invalidate_permissions_cache()
+ ContentType.objects.clear_cache()
+
+ # One query to re generate the cache.
+ for _ in range(5):
+ assert not self.user_check.has_user_perms(
+ 'foo',
+ self.user,
+ True,
+ False,
+ )
+
+ def test_invalidate_group_permissions_cache(self):
+ # Show that calling invalidate_permissions_cache will cause extra
+ # queries.
+ # For each time invalidate_permissions_cache gets called, you
+ # will need to do one query to get content type and one to get
+ with self.assertNumQueries(4):
+ for _ in range(5):
+ assert not self.group_check.has_group_perms(
+ 'foo',
+ self.group,
+ True,
+ )
+
+ # Invalidate the cache to show that a query will be generated when
+ # checking perms again.
+ self.group_check.invalidate_permissions_cache()
+ ContentType.objects.clear_cache()
# One query to re generate the cache.
- self.user_check.has_user_perms('foo', self.user, True, False)
+ for _ in range(5):
+ assert not self.group_check.has_group_perms(
+ 'foo',
+ self.group,
+ True,
+ )
def test_has_user_perms_check_group_multiple(self):
# Create a permission with just a group.
@@ -247,10 +319,12 @@ def test_has_user_perms_check_group_multiple(self):
group=self.group,
approved=True,
)
+ # By creating the Permission objects the Content type cache
+ # gets created.
# Check the number of queries.
with self.assertNumQueries(1):
- self.user_check.has_user_perms('foo', self.user, True, True)
+ assert self.user_check.has_user_perms('foo', self.user, True, True)
# Create a second group.
new_group = Group.objects.create(name='new_group')
@@ -269,7 +343,7 @@ def test_has_user_perms_check_group_multiple(self):
# Make sure it is the same number of queries.
with self.assertNumQueries(1):
- self.user_check.has_user_perms('foo', self.user, True, True)
+ assert self.user_check.has_user_perms('foo', self.user, True, True)
class GroupPermissionCacheTestCase(SmartCachingTestCase):
@@ -278,7 +352,7 @@ class GroupPermissionCacheTestCase(SmartCachingTestCase):
"""
def test_has_user_perms_with_groups(self):
- perms = self._old_permission_check()
+ perms = self._old_user_permission_check()
self.assertEqual([], list(perms))
# Use the new cached user perms to show that the user does not have the
@@ -301,7 +375,7 @@ def test_has_user_perms_with_groups(self):
)
# Old permission check
- perms = self._old_permission_check()
+ perms = self._old_user_permission_check()
self.assertEqual([perm], list(perms))
# Invalidate the cache.
@@ -324,27 +398,28 @@ def test_has_group_perms_no_user(self):
)
self.assertFalse(can_foo_with_group)
- self.assertEqual(self.group_check._perm_cache, {})
- self.assertEqual(self.group_check._group_perm_cache, {})
+ perms = self._old_group_permission_check()
+ self.assertEqual([], list(perms))
# Create a permission with just that group.
- Permission.objects.create(
- content_type=Permission.objects.get_content_type(User),
- object_id=self.user.pk,
+ perm = Permission.objects.create(
+ content_type=Permission.objects.get_content_type(Group),
+ object_id=self.group.pk,
codename='foo',
group=self.group,
approved=True,
)
+ # Old permission check
+ perms = self._old_group_permission_check()
+ self.assertEqual([perm], list(perms))
+
# Invalidate the cache.
self.group_check.invalidate_permissions_cache()
can_foo_with_group = self.group_check.has_group_perms(
'foo',
- self.user,
+ self.group,
approved=True,
)
self.assertTrue(can_foo_with_group)
-
- self.assertEqual(self.group_check._perm_cache, {})
- self.assertEqual(self.group_check._group_perm_cache, {})
View
4 docs/tips_tricks.txt
@@ -66,3 +66,7 @@ invalidate_permissions_cache in order to see that changes::
This is particularly useful if you are using the permission instances during a
request, where it is unlikely that the state of the ``Permission`` table will
change.
+
+Although the previous example was only passing in a ``user`` into the
+permission, smart caching is used when getting permissions in a ``group`` as
+well.
View
6 example/settings.py
@@ -19,6 +19,12 @@
}
}
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
+ }
+}
+
TIME_ZONE = 'America/Chicago'
LANGUAGE_CODE = 'en-us'
View
11 setup.py
@@ -1,13 +1,17 @@
import os
from setuptools import setup, find_packages
+
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
name='django-authority',
version='0.4',
- description="A Django app that provides generic per-object-permissions for Django's auth app.",
+ description=(
+ "A Django app that provides generic per-object-permissions "
+ "for Django's auth app."
+ ),
long_description=read('README'),
author='Jannis Leidel',
author_email='jannis@leidel.info',
@@ -22,7 +26,12 @@ def read(fname):
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
'Framework :: Django',
+ 'Framework :: Django :: 1.3',
+ 'Framework :: Django :: 1.4',
+ 'Framework :: Django :: 1.5',
],
install_requires=['django', 'django-extensions', 'django-debug-toolbar'],
package_data = {
Something went wrong with that request. Please try again.