Commits

phuihock committed 38bf520

initial import

Comments (0)

Files changed (10)

+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation 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.
+"""
+
+import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess
+from optparse import OptionParser
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    quote = str
+
+# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
+stdout, stderr = subprocess.Popen(
+    [sys.executable, '-Sc',
+     'try:\n'
+     '    import ConfigParser\n'
+     'except ImportError:\n'
+     '    print 1\n'
+     'else:\n'
+     '    print 0\n'],
+    stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+has_broken_dash_S = bool(int(stdout.strip()))
+
+# In order to be more robust in the face of system Pythons, we want to
+# run without site-packages loaded.  This is somewhat tricky, in
+# particular because Python 2.6's distutils imports site, so starting
+# with the -S flag is not sufficient.  However, we'll start with that:
+if not has_broken_dash_S and 'site' in sys.modules:
+    # We will restart with python -S.
+    args = sys.argv[:]
+    args[0:0] = [sys.executable, '-S']
+    args = map(quote, args)
+    os.execv(sys.executable, args)
+# Now we are running with -S.  We'll get the clean sys.path, import site
+# because distutils will do it later, and then reset the path and clean
+# out any namespace packages from site-packages that might have been
+# loaded by .pth files.
+clean_path = sys.path[:]
+import site
+sys.path[:] = clean_path
+for k, v in sys.modules.items():
+    if k in ('setuptools', 'pkg_resources') or (
+        hasattr(v, '__path__') and
+        len(v.__path__)==1 and
+        not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))):
+        # This is a namespace package.  Remove it.
+        sys.modules.pop(k)
+
+is_jython = sys.platform.startswith('java')
+
+setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
+distribute_source = 'http://python-distribute.org/distribute_setup.py'
+
+# parsing arguments
+def normalize_to_url(option, opt_str, value, parser):
+    if value:
+        if '://' not in value: # It doesn't smell like a URL.
+            value = 'file://%s' % (
+                urllib.pathname2url(
+                    os.path.abspath(os.path.expanduser(value))),)
+        if opt_str == '--download-base' and not value.endswith('/'):
+            # Download base needs a trailing slash to make the world happy.
+            value += '/'
+    else:
+        value = None
+    name = opt_str[2:].replace('-', '_')
+    setattr(parser.values, name, value)
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --setup-source and --download-base to point to
+local resources, you can keep this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", dest="version",
+                          help="use a specific zc.buildout version")
+parser.add_option("-d", "--distribute",
+                   action="store_true", dest="use_distribute", default=False,
+                   help="Use Distribute rather than Setuptools.")
+parser.add_option("--setup-source", action="callback", dest="setup_source",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or file location for the setup file. "
+                        "If you use Setuptools, this will default to " +
+                        setuptools_source + "; if you use Distribute, this "
+                        "will default to " + distribute_source +"."))
+parser.add_option("--download-base", action="callback", dest="download_base",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or directory for downloading "
+                        "zc.buildout and either Setuptools or Distribute. "
+                        "Defaults to PyPI."))
+parser.add_option("--eggs",
+                  help=("Specify a directory for storing eggs.  Defaults to "
+                        "a temporary directory that is deleted when the "
+                        "bootstrap script completes."))
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true", default=False,
+                  help=("Normally, if you do not specify a --version, the "
+                        "bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", None, action="store", dest="config_file",
+                   help=("Specify the path to the buildout configuration "
+                         "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout's main function
+if options.config_file is not None:
+    args += ['-c', options.config_file]
+
+if options.eggs:
+    eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
+else:
+    eggs_dir = tempfile.mkdtemp()
+
+if options.setup_source is None:
+    if options.use_distribute:
+        options.setup_source = distribute_source
+    else:
+        options.setup_source = setuptools_source
+
+if options.accept_buildout_test_releases:
+    args.append('buildout:accept-buildout-test-releases=true')
+args.append('bootstrap')
+
+try:
+    import pkg_resources
+    import setuptools # A flag.  Sometimes pkg_resources is installed alone.
+    if not hasattr(pkg_resources, '_distribute'):
+        raise ImportError
+except ImportError:
+    ez_code = urllib2.urlopen(
+        options.setup_source).read().replace('\r\n', '\n')
+    ez = {}
+    exec ez_code in ez
+    setup_args = dict(to_dir=eggs_dir, download_delay=0)
+    if options.download_base:
+        setup_args['download_base'] = options.download_base
+    if options.use_distribute:
+        setup_args['no_fake'] = True
+    ez['use_setuptools'](**setup_args)
+    if 'pkg_resources' in sys.modules:
+        reload(sys.modules['pkg_resources'])
+    import pkg_resources
+    # This does not (always?) update the default working set.  We will
+    # do it.
+    for path in sys.path:
+        if path not in pkg_resources.working_set.entries:
+            pkg_resources.working_set.add_entry(path)
+
+cmd = [quote(sys.executable),
+       '-c',
+       quote('from setuptools.command.easy_install import main; main()'),
+       '-mqNxd',
+       quote(eggs_dir)]
+
+if not has_broken_dash_S:
+    cmd.insert(1, '-S')
+
+find_links = options.download_base
+if not find_links:
+    find_links = os.environ.get('bootstrap-testing-find-links')
+if find_links:
+    cmd.extend(['-f', quote(find_links)])
+
+if options.use_distribute:
+    setup_requirement = 'distribute'
+else:
+    setup_requirement = 'setuptools'
+ws = pkg_resources.working_set
+setup_requirement_path = ws.find(
+    pkg_resources.Requirement.parse(setup_requirement)).location
+env = dict(
+    os.environ,
+    PYTHONPATH=setup_requirement_path)
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+    def _final_version(parsed_version):
+        for part in parsed_version:
+            if (part[:1] == '*') and (part not in _final_parts):
+                return False
+        return True
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setup_requirement_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+if is_jython:
+    import subprocess
+    exitcode = subprocess.Popen(cmd, env=env).wait()
+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
+    exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
+if exitcode != 0:
+    sys.stdout.flush()
+    sys.stderr.flush()
+    print ("An error occurred when trying to install zc.buildout. "
+           "Look above this message for any errors that "
+           "were output by easy_install.")
+    sys.exit(exitcode)
+
+ws.add_entry(eggs_dir)
+ws.require(requirement)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+if not options.eggs: # clean up temporary egg directory
+    shutil.rmtree(eggs_dir)
+[buildout]
+develop = .
+parts = django
+download-cache = cache
+versions = versions
+
+[django]
+recipe = djangorecipe
+version = 1.2.5
+project = pobject
+download-cache = ${buildout:download-cache}
+eggs = 
+    django_exceptional_middleware
+
+[versions]
+django_exceptional_middleware = 0.5

pobject/__init__.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import logging
+logging.basicConfig(
+    #level = logging.DEBUG,
+)
+
+class PermissionExprException(Exception):
+    def __init__(self, e, checker):
+        super(PermissionExprException, self).__init__(checker, str(e))
+
+
+class P(object):
+    """AndExpr 'checker' wrapper that can be AND, OR or XOR. This is inspired by Q object in Django.
+
+    Given 'checker' a <op> 'checker' b,
+        if <op> is &, both checks must succeed
+        if <op> is |, either one must succeed
+        if <op> is ^, one must succeed and the other one must fail
+
+    Order or evaluation is from left to right (top to bottom).
+
+    It can also be used to execute 'checks' conditionally based on the type of the current request method. If request method
+    matches the given method or when method is not provided, checker must succeed.
+
+    For example,
+        P(is_owner(...), "POST")
+        is_owner is executed only if the request method is POST.
+
+    """
+    class BaseExpr(object):
+        def __init__(self, p):
+            self.p = p
+            self.p.is_in_expr = True
+
+    class AndExpr(BaseExpr):
+        def __call__(self, last_rval, request, *args, **kwargs):
+            if last_rval:
+                logging.debug('AND')
+                return self.p.is_successful(request, *args, **kwargs)
+            return False
+
+
+    class OrExpr(BaseExpr):
+        def __call__(self, last_rval, request, *args, **kwargs):
+            if not last_rval:
+                logging.debug('OR')
+                return self.p.is_successful(request, *args, **kwargs)
+            return True
+
+
+    class XorExpr(BaseExpr):
+        def __call__(self, last_rval, request, *args, **kwargs):
+            logging.debug('XOR')
+            this_rval = self.p.is_successful(request, *args, **kwargs)
+            return last_rval != this_rval
+
+
+    def __init__(self, *checkers, **kwargs):
+        self.checkers = checkers
+        self.method = kwargs.pop('method', None)
+        self.is_in_expr = False
+        self.next_expr = None
+
+
+    def chain_expr(self, expr):
+        p = self
+        while p:
+            if p.next_expr:
+                p = p.next_expr.p
+            else:
+                break
+        p.next_expr = expr
+
+
+    def __and__(self, other):
+        if isinstance(other, P):
+            self.chain_expr(P.AndExpr(other))
+            return self
+        raise ValueError
+
+
+    def __or__(self, other):
+        if isinstance(other, P):
+            self.chain_expr(P.OrExpr(other))
+            return self
+        raise ValueError
+
+
+    def __xor__(self, other):
+        if isinstance(other, P):
+            self.chain_expr(P.XorExpr(other))
+            return self
+        raise ValueError
+
+
+    def __invert__(self):
+        return Invertor(*self.checkers, method=self.method)
+
+    def __repr__(self):
+        names = []
+        for checker in self.checkers:
+            names.append(checker.__name__)
+        
+        return "P(%s, method=%s)" % (", ".join(names), self.method)
+
+
+    def exec_checkers(self, request, *args, **kwargs):
+        last_rval = True
+        for checker in self.checkers:
+            last_rval = checker(request, *args, **kwargs)
+            if not last_rval:
+                break
+        return last_rval
+
+
+    def is_successful(self, request, *args, **kwargs):
+        if not self.is_in_expr:
+            logging.debug('evaluating %r' % self)
+        else:
+            logging.debug('%r' % self)
+
+        if not self.method or request.method == self.method:
+            last_rval = self.exec_checkers(request, *args, **kwargs)
+            if self.next_expr:
+                return self.next_expr(last_rval, request, *args, **kwargs)
+
+            return last_rval
+        return False
+
+
+class Invertor(P):
+    def exec_checkers(self, request, *args, **kwargs):
+        ret_val = super(Invertor, self).exec_checkers(request, *args, **kwargs)
+        return not ret_val
+
+    def __repr__(self):
+        names = []
+        for checker in self.checkers:
+            names.append(checker.__name__)
+        return "~P(%s, method=%s)" % (", ".join(names), self.method)
+

pobject/decorators.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from exceptional_middleware.responses import HttpForbidden
+from django.contrib.auth.decorators import login_required
+from django.utils.decorators import available_attrs
+from functools import wraps
+import logging
+import traceback
+from pobject import PermissionExprException, P
+
+
+def permission_required(perm, *checks):
+    """A permission decorator that executes 'checks' in canonical order. If all checks succeed, view function is executed.
+    Otherwise, 403 (Forbidden) is returned.
+
+    'checks' is an iterable of callables that returns a boolean or equivalent. A 'check' can be a single function, a P object
+    or combination of both so long the return value is a truth value.
+
+    For example,
+    # User must be a friend of the current user
+    @permission_required(is_friend)
+
+    # User must be an owner of the instance when request method is POST (assuming POST is used for updating a model instance)
+    @permission_required(
+        P(is_owner, "POST")
+    )
+
+    # User must be an owner of the instance and (a member of the group or a friend of current user)
+    @permission_required(
+        is_owner,
+        P(is_group_member) | P(is_friend)
+    )
+    """
+    def wrapper(view_func):
+        @login_required
+        @wraps(view_func, assigned=available_attrs(view_func))
+        def perm_check(request, *args, **kwargs):
+            if perm and not request.user.has_perm(perm):
+                raise HttpForbidden
+
+            check = None
+            try:
+                for check in checks:
+                    # check must be a callable or an instance of P
+                    result = False
+                    if isinstance(check, P):
+                        result = check.is_successful(request, *args, **kwargs)
+                    if callable(check):
+                        result = check(request, *args, **kwargs)
+
+                    if not result:
+                        logging.debug('%s fails %s permission checks.' % (request.user.username, check))
+                        raise HttpForbidden
+            except PermissionExprException, e:
+                logging.error(traceback.format_exc())
+                raise e
+
+            return view_func(request, *args, **kwargs)
+        return perm_check
+    return wrapper

pobject/development.py

+
+from pobject.settings import *
+DEBUG=True
+TEMPLATE_DEBUG=DEBUG

pobject/models.py

Empty file added.

pobject/settings.py

+
+import os
+
+ADMINS = (
+    # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'sqlite3'    # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+DATABASE_NAME = 'pobject.db'
+DATABASE_USER = ''             # Not used with sqlite3.
+DATABASE_PASSWORD = ''         # Not used with sqlite3.
+DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
+
+TIME_ZONE = 'America/Chicago'
+
+LANGUAGE_CODE = 'en-us'
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = os.path.join(os.path.dirname(__file__), 'media')
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = '/media/'
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/admin_media/'
+
+# Don't share this with anybody.
+SECRET_KEY = 'ou+sn%m4cr!wb(j!w2dz0z@id7f(&u)no=7ywzm3oa)3pc%6)$'
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.middleware.doc.XViewMiddleware',
+)
+
+ROOT_URLCONF = 'pobject.urls'
+
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.admin',
+    'pobject',
+)
+
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+)
+
+TEMPLATE_DIRS = (
+    os.path.join(os.path.dirname(__file__), "templates"),
+)
+
+
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from django.contrib.auth.models import User
+from django.http import HttpRequest
+from django.test import TestCase
+from exceptional_middleware.responses import HttpForbidden
+from pobject import P
+from pobject.decorators import permission_required
+
+def is_family(request, *args, **kwargs):
+    return True
+
+def is_father(request, *args, **kwargs):
+    return True
+
+def is_male(request, *args, **kwargs):
+    return True
+
+def is_female(request, *args, **kwargs):
+    return False
+
+def is_mother(request, *args, **kwargs):
+    return False
+
+def is_foe(request, *args, **kwargs):
+    return False
+
+
+class TestPobject(TestCase):
+
+    def setUp(self):
+        self.request = HttpRequest()
+        self.request.user = User.objects.create_user('test', 'test@example.com')
+
+    def test_pobject(self):
+        p = P(is_family) & P(is_male, is_family) & ~P(is_foe)
+        self.assertEquals(isinstance(p, P), True)
+
+        expr_counter = 0
+        while True:
+            if p.next_expr:
+                p = p.next_expr.p
+                expr_counter += 1
+            else:
+                break
+        self.assertEquals(expr_counter, 2)
+
+    def assert_permission_required(self, *checkers, **kwargs):
+        raises = kwargs.pop('raises', None)
+
+        @permission_required('', *checkers)
+        def test_view(request, *args, **kwargs):
+            return True
+
+        if not raises:
+            self.assertEquals(test_view(self.request), True)
+        else:
+            self.assertRaises(raises, test_view, self.request)
+
+
+    def test_is_family_plain_func1(self):
+        self.assert_permission_required(
+            is_family
+        )
+
+    def test_is_family_plain_func2(self):
+        self.assert_permission_required(
+            is_family, is_father
+        )
+
+    def test_is_family_pobject1(self):
+        self.assert_permission_required(
+            P(is_family)
+        )
+
+    def test_is_family_pobject2(self):
+        self.assert_permission_required(
+            P(is_family, is_father)
+        )
+
+    def test_is_family_pobject2_and(self):
+        self.assert_permission_required(
+            P(is_family) & P(is_father)
+        )
+
+    def test_is_family_pobject2_or(self):
+        self.assert_permission_required(
+            P(is_family) | P(is_father)
+        )
+
+    def test_is_not_foe(self):
+        self.assert_permission_required(
+            P(is_foe), raises=HttpForbidden
+        )
+
+    def test_is_not_foe_inverted(self):
+        self.assert_permission_required(
+            ~P(is_foe)
+        )
+
+    def test_is_family_and_not_foe(self):
+        self.assert_permission_required(
+            P(is_family) & ~P(is_foe)
+        )
+
+    def test_is_family_xor_foe(self):
+        self.assert_permission_required(
+            P(is_family) ^ P(is_foe)
+        )
+
+    def test_is_family_xor_male(self):
+        self.assert_permission_required(
+            P(is_family) ^ P(is_male), raises=HttpForbidden
+        )
+
+    def test_is_family_and_male(self):
+        self.assert_permission_required(
+            P(is_family) & P(is_male)
+        )
+
+    def test_is_father_or_mother(self):
+        self.assert_permission_required(
+            P(is_father) ^ P(is_mother)
+        )
+
+    def test_is_father_and_female(self):
+        self.assert_permission_required(
+            P(is_father) & P(is_female), raises=HttpForbidden
+        )
+
+    def test_is_father_male_not_mother_or_foe1(self):
+        self.assert_permission_required(
+            P(is_father, is_male) & ~P(is_mother) & ~P(is_foe)
+        )
+
+    def test_is_father_male_not_mother_or_foe2(self):
+        self.assert_permission_required(
+            P(is_father, is_male) & ~(P(is_mother) | P(is_foe))
+        )
+
+    def test_is_mother_only(self):
+        self.assert_permission_required(
+            P(is_family, is_female) & ~(P(is_father)), raises=HttpForbidden
+        )
+
+from django.conf.urls.defaults import patterns, include, handler500
+from django.conf import settings
+from django.contrib import admin
+admin.autodiscover()
+
+handler500 # Pyflakes
+
+urlpatterns = patterns(
+    '',
+    (r'^admin/(.*)', admin.site.root),
+    (r'^accounts/login/$', 'django.contrib.auth.views.login'),
+)
+
+if settings.DEBUG:
+    urlpatterns += patterns('',
+        (r'^media/(?P<path>.*)$', 'django.views.static.serve',
+         {'document_root': settings.MEDIA_ROOT}),
+    )
+from setuptools import find_packages, setup
+
+
+setup(
+    name='django-pobject',
+    version='1.0',
+    url='https://bitbucket.org/phuihock/django-pobject',
+    license='GPLv3',
+    author='Chang Phui Hock',
+    author_email='phuihock@codekaki.com',
+    description="Expressive and concise mini permission framework for Django views.",
+    include_package_data=True,
+    zip_safe=False,
+    packages=find_packages('.'),
+    package_dir={'': '.'},
+    install_requires=(
+        'django_exceptional_middleware'
+    ),
+)