[#2698] Simplification de l'API: prédicats
authorEric Mc Sween <eric.mcsween@auf.org>
Mon, 27 Feb 2012 17:09:58 +0000 (12:09 -0500)
committerEric Mc Sween <eric.mcsween@auf.org>
Mon, 27 Feb 2012 17:09:58 +0000 (12:09 -0500)
14 files changed:
.gitignore
auf/django/permissions/__init__.py
auf/django/permissions/backends.py [deleted file]
auf/django/permissions/predicates.py [new file with mode: 0644]
auf/django/permissions/tests.py [deleted file]
bootstrap.py [new file with mode: 0644]
buildout.cfg [new file with mode: 0644]
runtests.py [deleted file]
setup.py
tests/__init__.py [new file with mode: 0644]
tests/food/__init__.py [new file with mode: 0644]
tests/food/models.py [new file with mode: 0644]
tests/food/tests.py [new file with mode: 0644]
tests/settings.py [new file with mode: 0644]

index 731f067..79d9204 100644 (file)
@@ -1,4 +1,9 @@
 *.egg-info
 *.pyc
+/.installed.cfg
+/bin
 /build
+/develop-eggs
+/eggs
 /dist
+/parts
index 05ae6c1..480f28c 100644 (file)
+# encoding: utf-8
+
 from collections import defaultdict
 
-class Rules(object):
 
-    def __init__(self):
-        self.tests = defaultdict(list)
-        self.qs = defaultdict(list)
-        self.test_qs = defaultdict(list)
-
-    def clear(self):
-        self.tests.clear()
-        self.qs.clear()
-        self.test_qs.clear()
-
-    def add(self, perm, cls=None, test=None, q=None):
-        if test:
-            self.tests[(perm, cls)].append(test)
-        if q:
-            self.qs[(perm, cls)].append(q)
-            if not test:
-                self.test_qs[(perm, cls)].append(q)
-
-    def test(self, user, perm, obj=None):
-        if obj is None:
-            tests = self.tests[(perm, None)]
-            return any(test(user) for test in tests)
-
-        cls = type(obj)
-        tests = self.tests[(perm, cls)]
-        if any(test(user, obj) for test in tests):
-            return True
-        qs = self.test_qs[(perm, cls)]
-        if qs:
-            q = reduce(lambda x, y: x | y, (q(user) for q in qs))
-            if cls._default_manager.filter(q).filter(pk=obj.pk).exists():
-                return True
-        return False
-
-    def get_q(self, user, perm, cls):
-        qs = self.qs[(perm, cls)]
-        return reduce(lambda x, y: x | y, (q(user) for q in qs)) if qs else None
+class Predicate(object):
+    """
+    Wrapper pour une fonction ``f(user, obj, cls)``.
 
-allow_rules = Rules()
-deny_rules = Rules()
+    Le paramètre ``user`` est l'utilisateur pour lequel la permission est testée.
 
-def allow(perm, cls=None, test=None, q=None):
-    allow_rules.add(perm, cls, test, q)
+    Si le prédicat est testé sur un seul objet, cet objet est passé dans
+    le paramètre ``obj`` et le paramètre ``cls`` est ``None``. Inversement,
+    si le prédicat sert à filtrer un queryset, le modèle du queryset est
+    passé dans le paramètre ``cls`` et le paramètre ``obj`` est ``None``.
 
-def deny(perm, cls=None, test=None, q=None):
-    deny_rules.add(perm, cls, test, q)
+    La fonction peut retourner un booléen ou un objet ``Q``. Un booléen
+    indique si le test a passé ou pas. Un objet ``Q`` indique les filtres à
+    appliquer pour ne garder que les objets qui passent le test. Si un objet
+    ``Q`` est retourné lors d'un test sur un seul objet, le test retournera
+    un booléen indiquant si l'objet satisfait au filtre.
 
-def user_has_perm(user, perm, obj=None):
-    return not deny_rules.test(user, perm, obj) and allow_rules.test(user, perm, obj)
+    Les prédicats peuvent être combinés à l'aide des opérateurs booléens
+    ``&``, ``|`` et ``~``.
+    """
 
-def filter(user, perm, queryset):
-    deny_q = deny_rules.get_q(user, perm, queryset.model)
-    if deny_q:
-        queryset = queryset.exclude(deny_q)
-    allow_q = allow_rules.get_q(user, perm, queryset.model)
-    if allow_q:
-        queryset = queryset.filter(allow_q)
-    else:
-        queryset = queryset.none()
-    return queryset
+    def __init__(self, func_or_value):
+        """
+        On peut initialiser un prédicat avec une fonction ayant la signature
+        ``f(user, obj, cls)`` ou avec une valeur constante.
+        """
+        if callable(func_or_value):
+            self.func = func_or_value
+        else:
+            self.func = lambda user, obj, cls: func_or_value
+
+    def __call__(self, user, obj=None, cls=None):
+        """
+        Appelle la fonction encapsulée.
+        """
+        return self.func(user, obj, cls)
+
+    def __and__(self, other):
+        def func(user, obj, cls):
+            my_result = self(user, obj, cls)
+            if my_result is False:
+                return False
+            other_result = other(user, obj, cls)
+            if my_result is True:
+                return other_result
+            elif other_result is True:
+                return my_result
+            else:
+                return my_result & other_result
+        return Predicate(func)
+
+    def __or__(self, other):
+        def func(user, obj, cls):
+            my_result = self(user, obj, cls)
+            if my_result is True:
+                return True
+            other_result = other(user, obj, cls)
+            if my_result is False:
+                return other_result
+            elif other_result is False:
+                return my_result
+            else:
+                return my_result | other_result
+        return Predicate(func)
+
+    def __invert__(self):
+        def func(user, obj, cls):
+            result = self(user, obj, cls)
+            if isinstance(result, bool):
+                return not result
+            else:
+                return ~result
+        return Predicate(func)
 
-def clear_rules():
-    allow_rules.clear()
-    deny_rules.clear()
 
-def autodiscover():
-    """
-    Auto-discover INSTALLED_APPS permissions.py modules and fail silently when
-    not present. This forces an import on them to register permission rules.
-    This is freely adapted from django.contrib.admin.autodiscover()
-    """
+class Rules(object):
 
-    import copy
-    from django.conf import settings
-    from django.utils.importlib import import_module
-    from django.utils.module_loading import module_has_submodule
-
-    for app in settings.INSTALLED_APPS:
-        mod = import_module(app)
-        # Attempt to import the app's permissions module.
-        try:
-            import_module('%s.permissions' % app)
-        except:
-            # Decide whether to bubble up this error. If the app just
-            # doesn't have a permissions module, we can ignore the error
-            # attempting to import it, otherwise we want it to bubble up.
-            if module_has_submodule(mod, 'permissions'):
-                raise
+    def __init__(self, allow_default=None, deny_default=None):
+        self.allow_rules = defaultdict(lambda: allow_default or Predicate(False))
+        self.deny_rules = defaultdict(lambda: deny_default or Predicate(False))
+
+    def allow(self, perm, cls, predicate):
+        if not isinstance(predicate, Predicate):
+            raise TypeError("the third argument to allow() must be a Predicate")
+        self.allow_rules[(perm, cls)] |= predicate
+
+    def deny(self, perm, cls, predicate):
+        if not isinstance(predicate, Predicate):
+            raise TypeError("the third argument to deny() must be a Predicate")
+        self.deny_rules[(perm, cls)] |= predicate
+
+    def predicate_for_perm(self, perm, cls):
+        return self.allow_rules[(perm, cls)] & ~self.deny_rules[(perm, cls)]
+
+    def user_has_perm(self, user, perm, obj):
+        result = self.predicate_for_perm(perm, obj.__class__)(user, obj)
+        if isinstance(result, bool):
+            return result
+        else:
+            return obj._default_manager.filter(pk=obj.pk).filter(result).exists()
+
+    def filter_queryset(self, user, perm, queryset):
+        result = self.predicate_for_perm(perm, queryset.model)(user, cls=queryset.model)
+        if result is True:
+            return queryset
+        elif result is False:
+            return queryset.none()
+        else:
+            return queryset.filter(result)
+
+    def clear():
+        self.allow_rules.clear()
+        self.deny_rules.clear()
+
+
+class AuthenticationBackend(object):
+    supports_anonymous_user = True
+    supports_inactive_user = True
+    supports_object_permissions = True
+    rules = None
+
+    def has_perm(self, user, perm, obj=None):
+        if self.rules is None or obj is None:
+            return False
+        return self.rules.user_has_perm(user, perm, obj)
+
+    def authenticate(self, username=None, password=None):
+        # We don't authenticate
+        return None
+
+    def get_user(self, user_id):
+        # We don't authenticate
+        return None
diff --git a/auf/django/permissions/backends.py b/auf/django/permissions/backends.py
deleted file mode 100644 (file)
index 3a5c437..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-from auf.django.permissions import user_has_perm
-
-class AuthenticationBackend(object):
-    supports_anonymous_user = True
-    supports_inactive_user = True
-    supports_object_permissions = True
-
-    def has_perm(self, user, perm, obj=None):
-        return user_has_perm(user, perm, obj)
-
-    def authenticate(self, username=None, password=None):
-        # We don't authenticate
-        return None
-
-    def get_user(self, user_id):
-        # We don't authenticate
-        return None
diff --git a/auf/django/permissions/predicates.py b/auf/django/permissions/predicates.py
new file mode 100644 (file)
index 0000000..3e66e70
--- /dev/null
@@ -0,0 +1,18 @@
+# encoding: utf-8
+
+"""
+Builtin predicates.
+"""
+
+from auf.django.permissions import Predicate, predicate_for_perm, predicate_generator
+
+
+def has_global_perm(perm):
+    def p(user, obj, cls):
+        return user.has_perm(perm)
+    return Predicate(p)
+
+def has_object_perm(perm):
+    def p(user, obj, cls):
+        return predicate_for_perm(perm, model or obj.__class__)(user, obj, model)
+    return Predicate(p)
diff --git a/auf/django/permissions/tests.py b/auf/django/permissions/tests.py
deleted file mode 100644 (file)
index 8537d7f..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-import re
-
-from django.contrib.auth.models import User
-from django.db import models
-from django.db.models import Q
-from django.http import HttpRequest
-from django.template import Template, RequestContext
-from django.utils import unittest
-from django.utils.text import normalize_newlines
-
-import auf.django.permissions as perms
-
-
-class Food(models.Model):
-    name = models.CharField(max_length=100)
-    allergic_users = models.ManyToManyField(User, related_name='allergies')
-
-    def __repr__(self):
-        return "<Food %r>" % self.name
-    
-
-class FoodTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self.alice = User.objects.create_user('alice', 'alice@example.com')
-        self.bob = User.objects.create_user('bob', 'bob@example.com')
-        self.apple = Food.objects.create(name='apple')
-        self.banana = Food.objects.create(name='banana')
-        self.banana.allergic_users.add(self.alice)
-
-    def tearDown(self):
-        self.alice.delete()
-        self.bob.delete()
-        self.apple.delete()
-        self.banana.delete()
-        perms.clear_rules()
-
-
-class RulesTestCase(FoodTestCase):
-
-    def test_global_perms(self):
-        perms.allow('sing', test=lambda user: user is self.alice)
-        self.assertTrue(self.alice.has_perm('sing'))
-        self.assertFalse(self.alice.has_perm('dance'))
-
-    def test_global_deny(self):
-        perms.allow('eat', test=lambda user: True)
-        perms.deny('eat', test=lambda user: user is self.bob)
-        self.assertTrue(self.alice.has_perm('eat'))
-        self.assertFalse(self.bob.has_perm('eat'))
-
-    def test_object_perms(self):
-        perms.allow('eat', Food, test=lambda user, obj: obj not in user.allergies.all())
-        self.assertTrue(self.alice.has_perm('eat', self.apple))
-        self.assertFalse(self.alice.has_perm('eat', self.banana))
-
-    def test_object_deny(self):
-        perms.allow('eat', Food, test=lambda user, obj: True)
-        perms.deny('eat', Food, test=lambda user, obj: obj in user.allergies.all())
-        self.assertTrue(self.alice.has_perm('eat', self.apple))
-        self.assertFalse(self.alice.has_perm('eat', self.banana))
-
-    def test_no_rules(self):
-        self.assertFalse(self.alice.has_perm('climb'))
-        perms.allow('eat', Food, test=lambda user, obj: True)
-        self.assertTrue(self.alice.has_perm('eat', self.apple))
-        self.assertFalse(self.alice.has_perm('eat', self.bob))
-
-    def test_q_rules(self):
-        perms.allow('eat', Food, q=lambda user: ~Q(allergic_users=user))
-        self.assertListEqual(list(perms.filter(self.alice, 'eat', Food.objects.all())),
-                             [self.apple])
-        self.assertListEqual(list(perms.filter(self.bob, 'eat', Food.objects.all())),
-                             [self.apple, self.banana])
-        self.assertTrue(self.alice.has_perm('eat', self.apple))
-        self.assertFalse(self.alice.has_perm('eat', self.banana))
-
-class TemplateTagsTestCase(FoodTestCase):
-
-    def setUp(self):
-        FoodTestCase.setUp(self)
-        perms.allow('eat', Food, 
-                    test=lambda user, obj: obj not in user.allergies.all(),
-                    q=lambda user: ~Q(allergic_users=user))
-        perms.allow('throw', Food, test=lambda user, obj: True)
-
-    def test_ifhasperm(self):
-        template = Template("""{% load permissions %}
-{% for fruit in food %}
-{% ifhasperm "eat" fruit %}
-Eat the {{ fruit.name }}
-{% else %}
-Don't eat the {{ fruit.name }}
-{% endifhasperm %}
-{% endfor %}""")
-        request = HttpRequest()
-        request.user = self.alice
-        context = RequestContext(request, {'food': Food.objects.all()})
-        self.assertRegexpMatches(template.render(context).strip(),
-                                 r'\s+'.join([
-                                     "Eat the apple",
-                                     "Don't eat the banana"
-                                 ])
-                                )
-
-    def test_withperms(self):
-        template = Template("""{% load permissions %}
-{% for fruit in food %}
-{% withperms fruit as fruit_perms %}
-Eat {{ fruit.name }}: {{ fruit_perms.eat|yesno }}
-Throw {{ fruit.name }}: {{ fruit_perms.throw|yesno }}
-Smoke {{ fruit.name }}: {{ fruit_perms.smoke|yesno }}
-{% endwithperms %}
-{% endfor %}""")
-        request = HttpRequest()
-        request.user = self.alice
-        context = RequestContext(request, {'food': Food.objects.all()})
-        self.assertRegexpMatches(template.render(context).strip(),
-                                 r'\s+'.join([
-                                     "Eat apple: yes",
-                                     "Throw apple: yes",
-                                     "Smoke apple: no",
-                                     "Eat banana: no",
-                                     "Throw banana: yes",
-                                     "Smoke banana: no"
-                                 ])
-                                )
diff --git a/bootstrap.py b/bootstrap.py
new file mode 100644 (file)
index 0000000..7728587
--- /dev/null
@@ -0,0 +1,77 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id$
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+is_jython = sys.platform.startswith('java')
+
+try:
+    import pkg_resources
+except ImportError:
+    ez = {}
+    exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+                         ).read() in ez
+    ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+    import pkg_resources
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    def quote (c):
+        return c
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+ws  = pkg_resources.working_set
+
+if is_jython:
+    import subprocess
+    
+    assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', 
+           quote(tmpeggs), 'zc.buildout'], 
+           env=dict(os.environ,
+               PYTHONPATH=
+               ws.find(pkg_resources.Requirement.parse('setuptools')).location
+               ),
+           ).wait() == 0
+
+else:
+    assert os.spawnle(
+        os.P_WAIT, sys.executable, quote (sys.executable),
+        '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout',
+        dict(os.environ,
+            PYTHONPATH=
+            ws.find(pkg_resources.Requirement.parse('setuptools')).location
+            ),
+        ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)
diff --git a/buildout.cfg b/buildout.cfg
new file mode 100644 (file)
index 0000000..0cf9814
--- /dev/null
@@ -0,0 +1,18 @@
+[buildout]
+unzip = true
+parts = django
+find-links =
+    http://pypi.auf.org/simple/auf.recipe.django/
+develop = .
+eggs =
+    auf.django.permissions
+versions = versions
+
+[django]
+recipe = auf.recipe.django
+project = tests
+settings = settings
+eggs = ${buildout:eggs}
+
+[versions]
+auf.recipe.django = 1.2
diff --git a/runtests.py b/runtests.py
deleted file mode 100755 (executable)
index 8cd3d66..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/python2
-
-from django.conf import settings
-from django.core.management import call_command
-
-settings.configure(
-    DATABASES={
-        'default': {
-            'ENGINE': 'django.db.backends.sqlite3',
-            'NAME': ':memory:'
-        }
-    },
-    INSTALLED_APPS=(
-        'django.contrib.auth', 
-        'django.contrib.contenttypes',
-        'auf.django.permissions'
-    ),
-    AUTHENTICATION_BACKENDS=(
-        'auf.django.permissions.backends.AuthenticationBackend',
-    ),
-    TEMPLATE_LOADERS=(
-        'django.template.loaders.app_directories.Loader',
-    ),
-    TEMPLATE_CONTEXT_PROCESSORS=(
-        'django.core.context_processors.request',
-    )
-)
-call_command('test', 'permissions')
index d726964..9ae5e8b 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -12,5 +12,8 @@ setup(name=name,
       author_email='eric.mcsween@auf.org',
       url='http://pypi.auf.org/%s' % name,
       license='GPL',
-      packages=find_packages(),
+      packages=find_packages(exclude=['tests', 'tests.*']),
+      namespace_packages=['auf', 'auf.django'],
+      include_package_data=True,
+      zip_safe=False,
      )
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/food/__init__.py b/tests/food/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/food/models.py b/tests/food/models.py
new file mode 100644 (file)
index 0000000..99906ba
--- /dev/null
@@ -0,0 +1,10 @@
+from django.contrib.auth.models import User
+from django.db import models
+
+
+class Food(models.Model):
+    name = models.CharField(max_length=100)
+    allergic_users = models.ManyToManyField(User, related_name='allergies')
+
+    def __repr__(self):
+        return "<Food %r>" % self.name
diff --git a/tests/food/tests.py b/tests/food/tests.py
new file mode 100644 (file)
index 0000000..beffe93
--- /dev/null
@@ -0,0 +1,115 @@
+from __future__ import absolute_import
+
+from django.contrib.auth.models import User
+from django.db.models import Q
+from django.http import HttpRequest
+from django.template import Template, RequestContext
+from django.utils import unittest
+from django.utils.text import normalize_newlines
+
+from auf.django.permissions import Rules, Predicate, AuthenticationBackend
+
+from tests.food.models import Food
+
+@Predicate
+def is_allergic(user, obj, cls):
+    return Q(allergic_users=user)
+
+
+class FoodTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.alice = User.objects.create_user('alice', 'alice@example.com')
+        self.bob = User.objects.create_user('bob', 'bob@example.com')
+        self.apple = Food.objects.create(name='apple')
+        self.banana = Food.objects.create(name='banana')
+        self.banana.allergic_users.add(self.alice)
+        self.rules = Rules()
+        AuthenticationBackend.rules = self.rules
+
+    def tearDown(self):
+        self.alice.delete()
+        self.bob.delete()
+        self.apple.delete()
+        self.banana.delete()
+
+
+class RulesTestCase(FoodTestCase):
+
+    def test_object_perms(self):
+        self.rules.allow('eat', Food, ~is_allergic)
+        self.assertTrue(self.alice.has_perm('eat', self.apple))
+        self.assertFalse(self.alice.has_perm('eat', self.banana))
+
+    def test_object_deny(self):
+        self.rules.allow('eat', Food, Predicate(True))
+        self.rules.deny('eat', Food, is_allergic)
+        self.assertTrue(self.alice.has_perm('eat', self.apple))
+        self.assertFalse(self.alice.has_perm('eat', self.banana))
+
+    def test_no_rules(self):
+        self.assertFalse(self.alice.has_perm('climb'))
+        self.rules.allow('eat', Food, Predicate(True))
+        self.assertTrue(self.alice.has_perm('eat', self.apple))
+        self.assertFalse(self.alice.has_perm('eat', self.bob))
+
+    def test_q_rules(self):
+        self.rules.allow('eat', Food, ~is_allergic)
+        self.assertListEqual(list(self.rules.filter_queryset(self.alice, 'eat', Food.objects.all())),
+                             [self.apple])
+        self.assertListEqual(list(self.rules.filter_queryset(self.bob, 'eat', Food.objects.all())),
+                             [self.apple, self.banana])
+        self.assertTrue(self.alice.has_perm('eat', self.apple))
+        self.assertFalse(self.alice.has_perm('eat', self.banana))
+
+
+class TemplateTagsTestCase(FoodTestCase):
+
+    def setUp(self):
+        FoodTestCase.setUp(self)
+        self.rules.allow('eat', Food, ~is_allergic)
+        self.rules.allow('throw', Food, Predicate(True))
+
+    def test_ifhasperm(self):
+        template = Template("""{% load permissions %}
+{% for fruit in food %}
+{% ifhasperm "eat" fruit %}
+Eat the {{ fruit.name }}
+{% else %}
+Don't eat the {{ fruit.name }}
+{% endifhasperm %}
+{% endfor %}""")
+        request = HttpRequest()
+        request.user = self.alice
+        context = RequestContext(request, {'food': Food.objects.all()})
+        self.assertRegexpMatches(template.render(context).strip(),
+                                 r'\s+'.join([
+                                     "Eat the apple",
+                                     "Don't eat the banana"
+                                 ])
+                                )
+
+    def test_withperms(self):
+        template = Template("""{% load permissions %}
+{% for fruit in food %}
+{% withperms fruit as fruit_perms %}
+Eat {{ fruit.name }}: {{ fruit_perms.eat|yesno }}
+Throw {{ fruit.name }}: {{ fruit_perms.throw|yesno }}
+Smoke {{ fruit.name }}: {{ fruit_perms.smoke|yesno }}
+{% endwithperms %}
+{% endfor %}""")
+        request = HttpRequest()
+        request.user = self.alice
+        context = RequestContext(request, {'food': Food.objects.all()})
+        self.assertRegexpMatches(template.render(context).strip(),
+                                 r'\s+'.join([
+                                     "Eat apple: yes",
+                                     "Throw apple: yes",
+                                     "Smoke apple: no",
+                                     "Eat banana: no",
+                                     "Throw banana: yes",
+                                     "Smoke banana: no"
+                                 ])
+                                )
+
+
diff --git a/tests/settings.py b/tests/settings.py
new file mode 100644 (file)
index 0000000..2002da0
--- /dev/null
@@ -0,0 +1,25 @@
+DATABASES={
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': ':memory:',
+    }
+}
+
+INSTALLED_APPS=(
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'auf.django.permissions',
+    'tests.food',
+)
+
+AUTHENTICATION_BACKENDS=(
+    'auf.django.permissions.AuthenticationBackend',
+)
+
+TEMPLATE_LOADERS=(
+    'django.template.loaders.app_directories.Loader',
+)
+
+TEMPLATE_CONTEXT_PROCESSORS=(
+    'django.core.context_processors.request',
+)