Commit 0ac0d872 authored by Michael Fladischer's avatar Michael Fladischer

Update upstream source from tag 'upstream/2.0.0'

Update to upstream version '2.0.0'
with Debian dir ec233b942a9abef4b05f50f8542789191c9913df
parents 296f3bf2 7a2f6d38
[report]
exclude_lines =
pragma: no cover
omit =
*/python?.?/*
*/site-packages/nose/*
*/rules/compat/*
*/rules/apps.py
# ---------------------------------------------------------------------------
# Python support matrix per Django version
#
# 2.6 1.5 1.6
# 2.7 1.5 1.6 1.7 1.8 1.9 1.10 1.11
# 3.3 1.5 1.6 1.7 1.8
# 3.4 1.7 1.8 1.9 1.10 1.11 2.0
# 3.5 1.8 1.9 1.10 1.11 2.0
# 3.6 1.10 1.11 2.0
# ---------------------------------------------------------------------------
sudo: false
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
- "pypy"
- "pypy3"
matrix:
include:
- { python: 2.6, env: TOXENV=py26-django15 }
- { python: 2.6, env: TOXENV=py26-django16 }
- { python: 2.7, env: TOXENV=py27-django15 }
- { python: 2.7, env: TOXENV=py27-django16 }
- { python: 2.7, env: TOXENV=py27-django17 }
- { python: 2.7, env: TOXENV=py27-django18 }
- { python: 2.7, env: TOXENV=py27-django19 }
- { python: 2.7, env: TOXENV=py27-django110 }
- { python: 2.7, env: TOXENV=py27-django111 }
- { python: 3.3, env: TOXENV=py33-django15 }
- { python: 3.3, env: TOXENV=py33-django16 }
- { python: 3.3, env: TOXENV=py33-django17 }
- { python: 3.3, env: TOXENV=py33-django18 }
- { python: 3.4, env: TOXENV=py34-django17 }
- { python: 3.4, env: TOXENV=py34-django18 }
- { python: 3.4, env: TOXENV=py34-django19 }
- { python: 3.4, env: TOXENV=py34-django110 }
- { python: 3.4, env: TOXENV=py34-django20 }
- { python: 3.4, env: TOXENV=py34-django111 }
- { python: 3.5, env: TOXENV=py35-django18 }
- { python: 3.5, env: TOXENV=py35-django19 }
- { python: 3.5, env: TOXENV=py35-django110 }
- { python: 3.5, env: TOXENV=py35-django111 }
- { python: 3.5, env: TOXENV=py35-django20 }
- { python: 3.6, env: TOXENV=py36-django110 }
- { python: 3.6, env: TOXENV=py36-django111 }
- { python: 3.6, env: TOXENV=py36-django20 }
- {python: "3.6", env: TOXENV=packaging}
install:
- pip install tox
- pip install tox tox-travis tox-venv
- pip install coveralls
script: tox
after_success:
- coveralls
Changelog
=========
## v2.0.0 - 2018/07/22
- Removed support for Python 2.6 and 3.3
- Removed support for Django versions before 1.11
- Removed ``SkipPredicate`` exception and ``skip`` method of ``Predicate``
- Removed ``replace_rule`` and related APIs
- Added ``set_rule`` and related APIs to safely replace a rule without having
to ensure one already exists
- Added compatibility with Django v2.1
- Re-introduced support for PyPy and PyPy 3
- Changed Python and Django supported versions policy to exclude end-of-life
versions. Support for EOL'd versions will be dropped in minor version
updates of ``rules`` from now on.
## v1.4.0 - 2018/07/21
- Fixed masking AttributeErrors raised from CBV get_object
- Fixed compatibility with `inspect` in newer Python 3 versions
- Added ability to replace rules and permissions
## v1.3.0 - 2017/12/13
- Added support for Django 2.0
......
......@@ -43,6 +43,7 @@ Table of Contents
=================
- `Requirements`_
- `Upgrading from 1.x`_
- `How to install`_
- `Configuring Django`_
......@@ -76,8 +77,34 @@ Table of Contents
Requirements
============
``rules`` requires Python 2.6/3.3 or newer. It can optionally integrate with
Django, in which case requires Django 1.5 or newer.
``rules`` requires Python 2.7/3.4 or newer. It can optionally integrate with
Django, in which case requires Django 1.11 or newer.
*Note*: At any given moment in time, ``rules`` will maintain support for all
currently supported Django versions, while dropping support for those versions
that reached end-of-life in minor releases. See the `Supported Versions`_
section on Django Project website for the current state and timeline.
.. _Supported Versions: https://www.djangoproject.com/download/#supported-versions
Upgrading from 1.x
==================
* Support Python 2.6 and 3.3, and Django versions before 1.11 has been
dropped.
* The ``SkipPredicate`` exception and ``skip()`` method of ``Predicate``,
that were used to signify that a predicate should be skipped, have been
removed. You may return ``None`` from your predicate to achieve this.
* The APIs to replace a rule's predicate have been renamed and their
behaviour changed. ``replace_rule`` and ``replace_perm`` functions and
``replace_rule`` method of ``RuleSet`` have been renamed to ``set_rule``,
``set_perm`` and ``RuleSet.set_perm`` respectively. The old behaviour was
to raise a ``KeyError`` if a rule by the given name did not exist. Since
version 2.0 this has changed and you can safely use ``set_*`` to set a
rule's predicate without having to ensure the rule exists first.
How to install
......@@ -267,12 +294,7 @@ We can now update our ``can_edit_book`` rule:
.. code:: python
>>> rules.add_rule('can_edit_book', is_book_author_or_editor)
Traceback (most recent call last):
...
KeyError: A rule with name `can_edit_book` already exists
>>> rules.remove_rule('can_edit_book')
>>> rules.add_rule('can_edit_book', is_book_author_or_editor)
>>> rules.set_rule('can_edit_book', is_book_author_or_editor)
>>> rules.test_rule('can_edit_book', adrian, guidetodjango)
True
>>> rules.test_rule('can_delete_book', adrian, guidetodjango)
......@@ -425,11 +447,9 @@ For more information on the decorator and helper function, refer to the
Using the class-based view mixin
++++++++++++++++++++++++++++++++
Django 1.9 introduced a new set of access mixins that you can use in your
class-based views to enforce authorization. ``rules`` extends this framework
to provide a mixin for object-level permissions, ``PermissionRequiredMixin``.
Note that ``rules`` will seamlessly fall back to importing its own copy of
Django's access mixins module for versions of Django prior to 1.9.
Django includes a set of access mixins that you can use in your class-based
views to enforce authorization. ``rules`` extends this framework to provide
object-level permissions via a mixin, ``PermissionRequiredMixin``.
The following example will automatically test for permission against the
instance returned by the view's ``get_object`` method:
......@@ -490,11 +510,14 @@ set to also use ``rules`` to authorize any add/change/delete actions in the
Admin. The Admin asks for *four* different permissions, depending on action:
- ``<app_label>.add_<modelname>``
- ``<app_label>.view_<modelname>``
- ``<app_label>.change_<modelname>``
- ``<app_label>.delete_<modelname>``
- ``<app_label>``
The first three are obvious. The fourth is the required permission for an app
*Note:* view permission is new in Django v2.1.
The first four are obvious. The fifth is the required permission for an app
to be displayed in the Admin's "dashboard". Here's some rules for our
imaginary ``books`` app as an example:
......@@ -502,6 +525,7 @@ imaginary ``books`` app as an example:
>>> rules.add_perm('books', rules.always_allow)
>>> rules.add_perm('books.add_book', is_staff)
>>> rules.add_perm('books.view_book', is_staff | has_secret_access_code)
>>> rules.add_perm('books.change_book', is_staff)
>>> rules.add_perm('books.delete_book', is_staff)
......@@ -513,11 +537,10 @@ If you'd like to tell Django whether a user has permissions on a specific
object, you'd have to override the following methods of a model's
``ModelAdmin``:
- ``has_view_permission(user, obj=None)``
- ``has_change_permission(user, obj=None)``
- ``has_delete_permission(user, obj=None)``
**Note:** There's also ``has_add_permission(user)`` but is not relevant here.
``rules`` comes with a custom ``ModelAdmin`` subclass,
``rules.contrib.admin.ObjectPermissionsModelAdmin``, that overrides these
methods to pass on the edited model instance to the authorization backends,
......@@ -544,6 +567,11 @@ Now this allows you to specify permissions like this:
>>> rules.add_perm('books.change_book', is_book_author_or_editor)
>>> rules.add_perm('books.delete_book', is_book_author)
To preserve backwards compatibility, Django will ask for either *view* or
*change* permission. For maximum flexibility, ``rules`` behaves subtly
different: ``rules`` will ask for the change permission if and only if no rule
exists for the view permission.
Advanced features
=================
......@@ -651,10 +679,6 @@ You may skip evaluation by returning ``None`` from your predicate:
Returning ``None`` signifies that the predicate need not be evaluated, thus
leaving the predicate result up to that point unchanged.
**Note:** This is new in version 1.1.0. It was possible to skip predicates in
older versions by calling the predicate's ``skip()`` method, but this has been
deprecated and support will be completely removed in a future version.
Logging predicate evaluation
----------------------------
......@@ -707,9 +731,9 @@ On the other hand, because importing predicates from all over the place in
order to define rules can lead to circular imports and broken hearts, it's
best to further split predicates and rules in different modules.
If using Django 1.7 and later, ``rules`` may optionally be configured to
autodiscover ``rules.py`` modules in your apps and import them at startup. To
have ``rules`` do so, just edit your ``INSTALLED_APPS`` setting:
``rules`` may optionally be configured to autodiscover ``rules.py`` modules in
your apps and import them at startup. To have ``rules`` do so, just edit your
``INSTALLED_APPS`` setting:
.. code:: python
......@@ -719,8 +743,8 @@ have ``rules`` do so, just edit your ``INSTALLED_APPS`` setting:
)
**Note:** On Python 2, you must also add the following to the top of your
``rules.py`` file, or you'll get import errors trying to import
``django-rules`` itself:
``rules.py`` file, or you'll get import errors trying to import ``rules``
itself:
.. code:: python
......@@ -730,7 +754,8 @@ have ``rules`` do so, just edit your ``INSTALLED_APPS`` setting:
API Reference
=============
Everything is accessible from the root ``rules`` module.
The core APIs are accessible from the root ``rules`` module. Django-specific
functionality for the Admin and views is available from ``rules.contrib``.
Class ``rules.Predicate``
......@@ -795,6 +820,9 @@ Instance methods
Adds a predicate to the rule set, assigning it to the given rule name.
Raises ``KeyError`` if another rule with that name already exists.
``set_rule(name, predicate)``
Set the rule with the given name, regardless if one already exists.
``remove_rule(name)``
Remove the rule with the given name. Raises ``KeyError`` if a rule with
that name does not exist.
......@@ -883,6 +911,10 @@ Managing the shared rule set
``add_rule(name, predicate)``
Adds a rule to the shared rule set. See ``RuleSet.add_rule``.
``set_rule(name, predicate)``
Set the rule with the given name from the shared rule set. See
``RuleSet.set_rule``.
``remove_rule(name)``
Remove a rule from the shared rule set. See ``RuleSet.remove_rule``.
......@@ -900,6 +932,9 @@ Managing the permissions rule set
``add_perm(name, predicate)``
Adds a rule to the permissions rule set. See ``RuleSet.add_rule``.
``set_perm(name, predicate)``
Replace a rule from the permissions rule set. See ``RuleSet.set_rule``.
``remove_perm(name)``
Remove a rule from the permissions rule set. See ``RuleSet.remove_rule``.
......
from .rulesets import RuleSet, add_rule, remove_rule, rule_exists, test_rule
from .permissions import add_perm, remove_perm, perm_exists, has_perm
from .rulesets import RuleSet, add_rule, set_rule, remove_rule, rule_exists, test_rule
from .permissions import add_perm, set_perm, remove_perm, perm_exists, has_perm
from .predicates import (Predicate, predicate, always_true, always_false,
always_allow, always_deny, is_authenticated,
is_superuser, is_staff, is_active, is_group_member)
VERSION = (1, 3, 0, 'final', 1)
VERSION = (2, 0, 0, 'final', 1)
default_app_config = 'rules.apps.RulesConfig'
"""
This is a copy of django.contrib.auth.mixins module, shipped with django-rules
for compatibility with Django versions before 1.9. Used under permission by
the Django Software Foundation.
Copyright (c) Django Software Foundation and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of Django nor the names of its contributors may be used
to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.utils import six
from django.utils.encoding import force_text
class AccessMixin(object):
"""
Abstract CBV mixin that gives access mixins the same customizable
functionality.
"""
login_url = None
permission_denied_message = ''
raise_exception = False
redirect_field_name = REDIRECT_FIELD_NAME
def get_login_url(self):
"""
Override this method to override the login_url attribute.
"""
login_url = self.login_url or settings.LOGIN_URL
if not login_url:
raise ImproperlyConfigured(
'{0} is missing the login_url attribute. Define {0}.login_url, settings.LOGIN_URL, or override '
'{0}.get_login_url().'.format(self.__class__.__name__)
)
return force_text(login_url)
def get_permission_denied_message(self):
"""
Override this method to override the permission_denied_message attribute.
"""
return self.permission_denied_message
def get_redirect_field_name(self):
"""
Override this method to override the redirect_field_name attribute.
"""
return self.redirect_field_name
def handle_no_permission(self):
if self.raise_exception:
raise PermissionDenied(self.get_permission_denied_message())
return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name())
class LoginRequiredMixin(AccessMixin):
"""
CBV mixin which verifies that the current user is authenticated.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated():
return self.handle_no_permission()
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
class PermissionRequiredMixin(AccessMixin):
"""
CBV mixin which verifies that the current user has all specified
permissions.
"""
permission_required = None
def get_permission_required(self):
"""
Override this method to override the permission_required attribute.
Must return an iterable.
"""
if self.permission_required is None:
raise ImproperlyConfigured(
'{0} is missing the permission_required attribute. Define {0}.permission_required, or override '
'{0}.get_permission_required().'.format(self.__class__.__name__)
)
if isinstance(self.permission_required, six.string_types):
perms = (self.permission_required, )
else:
perms = self.permission_required
return perms
def has_permission(self):
"""
Override this method to customize the way permissions are checked.
"""
perms = self.get_permission_required()
return self.request.user.has_perms(perms)
def dispatch(self, request, *args, **kwargs):
if not self.has_permission():
return self.handle_no_permission()
return super(PermissionRequiredMixin, self).dispatch(request, *args, **kwargs)
class UserPassesTestMixin(AccessMixin):
"""
CBV Mixin that allows you to define a test function which must return True
if the current user can access the view.
"""
def test_func(self):
raise NotImplementedError(
'{0} is missing the implementation of the test_func() method.'.format(self.__class__.__name__)
)
def get_test_func(self):
"""
Override this method to use a different test_func method.
"""
return self.test_func
def dispatch(self, request, *args, **kwargs):
user_test_result = self.get_test_func()()
if not user_test_result:
return self.handle_no_permission()
return super(UserPassesTestMixin, self).dispatch(request, *args, **kwargs)
from __future__ import absolute_import
try:
from inspect import getfullargspec
except ImportError:
# Python 2 compatibility
from inspect import getargspec as getfullargspec
from inspect import ismethod, isfunction
from django.contrib import admin
from django.contrib.auth import get_permission_codename
try:
from django.contrib.auth import get_permission_codename
except ImportError: # pragma: no cover
# Django < 1.6
def get_permission_codename(action, opts):
return '%s_%s' % (action, opts.object_name.lower())
from ..permissions import perm_exists
class ObjectPermissionsModelAdminMixin(object):
def has_view_permission(self, request, obj=None):
opts = self.opts
codename = get_permission_codename('view', opts)
perm = '%s.%s' % (opts.app_label, codename)
if perm_exists(perm):
return request.user.has_perm(perm, obj)
else:
return self.has_change_permission(request, obj)
def has_change_permission(self, request, obj=None):
opts = self.opts
codename = get_permission_codename('change', opts)
......
......@@ -2,6 +2,7 @@ from functools import wraps
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import mixins
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied, ImproperlyConfigured, FieldError
from django.shortcuts import get_object_or_404
......@@ -9,12 +10,6 @@ from django.utils import six
from django.utils.decorators import available_attrs
from django.utils.encoding import force_text
try:
from django.contrib.auth import mixins
except ImportError: # pragma: no cover
# Django < 1.9
from ..compat import access_mixins as mixins
# These are made available for convenience, as well as for use in Django
# versions before 1.9. For usage help see Django's docs for 1.9 or later.
......@@ -39,10 +34,10 @@ class PermissionRequiredMixin(mixins.PermissionRequiredMixin):
``SingleObjectMixin``. Returns None if there's no ``get_object``
method.
"""
try:
if hasattr(self, 'get_object') and callable(self.get_object):
# Requires SingleObjectMixin or equivalent ``get_object`` method
return self.get_object()
except AttributeError: # pragma: no cover
else: # pragma: no cover
return None
def has_permission(self):
......@@ -124,7 +119,7 @@ def permission_required(perm, fn=None, login_url=None, raise_exception=False, re
# Get the object to check permissions against
if callable(fn):
obj = fn(request, *args, **kwargs)
else:
else: # pragma: no cover
obj = fn
# Get the user
......
......@@ -8,6 +8,10 @@ def add_perm(name, pred):
permissions.add_rule(name, pred)
def set_perm(name, pred):
permissions.set_rule(name, pred)
def remove_perm(name):
permissions.remove_rule(name)
......@@ -21,7 +25,7 @@ def has_perm(name, *args, **kwargs):
class ObjectPermissionBackend(object):
def authenticate(self, username, password):
def authenticate(self, *args, **kwargs):
return None
def has_perm(self, user, perm, *args, **kwargs):
......
import inspect
import logging
import operator
import threading
from functools import partial, update_wrapper
from warnings import warn
from .compat import inspect
logger = logging.getLogger('rules')
class SkipPredicate(Exception):
"""
Use to reject usage of a predicate.
"""
def __init__(self, *args, **kwargs):
warn('Skipping predicates by raising the SkipPredicate exception '
'has been deprecated. Return `None` from your predicate instead.',
DeprecationWarning)
super(SkipPredicate, self).__init__(*args, **kwargs)
def assert_has_kwonlydefaults(fn, msg):
argspec = inspect.getfullargspec(fn)
if hasattr(argspec, 'kwonlyargs'):
if not argspec.kwonlyargs:
return
if not argspec.kwonlydefaults or len(argspec.kwonlyargs) > len(argspec.kwonlydefaults.keys()):
raise TypeError(msg)
class Context(dict):
......@@ -53,26 +51,29 @@ class Predicate(object):
# - fn(obj=None)
# - fn()
assert callable(fn), 'The given predicate is not callable.'
innerfn = fn
if isinstance(fn, Predicate):
fn, num_args, var_args, name = fn.fn, fn.num_args, fn.var_args, name or fn.name
innerfn = fn
elif isinstance(fn, partial):
argspec = inspect.getargspec(fn.func)
innerfn = fn.func
argspec = inspect.getfullargspec(innerfn)
var_args = argspec.varargs is not None
num_args = len(argspec.args) - len(fn.args)
if inspect.ismethod(fn.func):
if inspect.ismethod(innerfn):
num_args -= 1 # skip `self`
name = fn.func.__name__
elif inspect.ismethod(fn):
argspec = inspect.getargspec(fn)
argspec = inspect.getfullargspec(fn)
var_args = argspec.varargs is not None
num_args = len(argspec.args) - 1 # skip `self`
elif inspect.isfunction(fn):
argspec = inspect.getargspec(fn)
argspec = inspect.getfullargspec(fn)
var_args = argspec.varargs is not None
num_args = len(argspec.args)
elif isinstance(fn, object):
callfn = getattr(fn, '__call__')
argspec = inspect.getargspec(callfn)
innerfn = getattr(fn, '__call__')
argspec = inspect.getfullargspec(innerfn)
var_args = argspec.varargs is not None
num_args = len(argspec.args) - 1 # skip `self`
name = name or type(fn).__name__
......@@ -81,6 +82,7 @@ class Predicate(object):
raise TypeError('Incompatible predicate.')
if bind:
num_args -= 1
assert_has_kwonlydefaults(innerfn, 'The given predicate is missing defaults for keyword-only arguments')
assert num_args <= 2, 'Incompatible predicate.'
self.fn = fn
self.num_args = num_args
......@@ -141,13 +143,6 @@ class Predicate(object):
except IndexError:
return None
def skip(self):
"""
Use this method in a predicate body to signal that it should be
ignored for the current invocation.
"""
raise SkipPredicate()
def test(self, obj=NO_VALUE, target=NO_VALUE):
"""
The canonical method to invoke predicates.
......@@ -214,11 +209,9 @@ class Predicate(object):
callargs = args[:self.num_args]
if self.bind:
callargs = (self,) + callargs
try:
result = self.fn(*callargs)
result = None if result is None else bool(result)
except SkipPredicate:
result = None
logger.debug(' %s = %s', self, 'skipped' if result is None else result)
return result
......
......@@ -13,6 +13,9 @@ class RuleSet(dict):
raise KeyError('A rule with name `%s` already exists' % name)
self[name] = pred
def set_rule(self, name, pred):
self[name] = pred
def remove_rule(self, name):
del self[name]
......@@ -30,6 +33,10 @@ def add_rule(name, pred):
default_rules.add_rule(name, pred)
def set_rule(name, pred):
default_rules.set_rule(name, pred)
def remove_rule(name):
default_rules.remove_rule(name)
......
......@@ -4,19 +4,14 @@ from ..rulesets import default_rules
register = template.Library()
try:
# Django < 2.0
simple_tag = register.assignment_tag
except AttributeError: # pragma: no cover
simple_tag = register.simple_tag
@simple_tag
@register.simple_tag
def test_rule(name, obj=None, target=None):
return default_rules.test_rule(name, obj, target)
@simple_tag
@register.simple_tag
def has_perm(perm, user, obj=None):
if not hasattr(user, 'has_perm'): # pragma: no cover
return False # swapped user model that doesn't support permissions
......
#!/usr/bin/env python
import sys
from os import environ
from os.path import abspath, dirname, join
import nose
def main():
project_dir = dirname(abspath(__file__))
# setup path
sys.path.insert(0, project_dir) # project dir
sys.path.insert(0, join(project_dir, 'tests')) # tests dir
environ['DJANGO_SETTINGS_MODULE'] = 'testapp.settings'
try:
# django >= 1.7
from django import setup
except ImportError:
pass
else:
setup()
# setup test env
from django.test.utils import setup_test_environment
setup_test_environment()
# setup db
from django.core.management import call_command, CommandError
options = {
'interactive': False,
'verbosity': 1,
}