├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG ├── MANIFEST.in ├── README.rst ├── django_rules_light.egg-info ├── PKG-INFO ├── SOURCES.txt ├── dependency_links.txt ├── not-zip-safe ├── requires.txt └── top_level.txt ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── class_decorator.rst │ ├── conf.py │ ├── debug.rst │ ├── decorator.rst │ ├── index.rst │ ├── logging.rst │ ├── middleware.rst │ ├── registry.rst │ ├── shortcuts.rst │ ├── testing.rst │ └── tutorial.rst ├── rules_light ├── __init__.py ├── apps.py ├── class_decorator.py ├── decorators.py ├── exceptions.py ├── locale │ ├── el │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── pt_BR │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── middleware.py ├── models.py ├── registry.py ├── rules_light_registry.py ├── shortcuts.py ├── static │ └── rules_light │ │ ├── github.css │ │ └── rainbow-custom.min.js ├── templates │ └── rules_light │ │ ├── base.html │ │ ├── exception.html │ │ └── registry.html ├── templatetags │ ├── __init__.py │ └── rules_light_tags.py ├── tests │ ├── __init__.py │ ├── fixtures │ │ ├── __init__.py │ │ └── class_decorator_classes.py │ ├── test_autodiscover.py │ ├── test_class_decorator.py │ ├── test_decorators.py │ ├── test_middleware.py │ ├── test_registry.py │ ├── test_shortcuts.py │ └── test_views.py ├── urls.py └── views.py ├── setup.cfg ├── setup.py ├── test_project ├── auth_rules.py ├── db.sqlite ├── manage.py ├── static │ ├── bootstrap │ │ ├── css │ │ │ ├── bootstrap-responsive.css │ │ │ ├── bootstrap-responsive.min.css │ │ │ ├── bootstrap.css │ │ │ └── bootstrap.min.css │ │ ├── img │ │ │ ├── glyphicons-halflings-white.png │ │ │ └── glyphicons-halflings.png │ │ └── js │ │ │ ├── bootstrap.js │ │ │ └── bootstrap.min.js │ └── jquery.min.js ├── templates │ ├── _form.html │ ├── auth │ │ ├── new_user_list.html │ │ ├── user_detail.html │ │ ├── user_form.html │ │ └── user_list.html │ ├── index.html │ ├── registration │ │ └── login.html │ └── site_base.html ├── test_project │ ├── __init__.py │ ├── rules_logging.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── test_requirements_without_django.txt └── test_rule │ ├── __init__.py │ ├── models.py │ └── rules_light_registry.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */rules_light/example_apps/* 4 | */rules_light/tests/* 5 | */test_project/* 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *pyc 2 | *swp 3 | docs/build 4 | # Pycharm 5 | .idea 6 | .tox 7 | env 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | dist: xenial 4 | matrix: 5 | allow_failures: 6 | python: 7 | - "3.6" 8 | env: 9 | global: 10 | - PIP_DISABLE_PIP_VERSION_CHECK=true 11 | matrix: 12 | - TOXENV=py27-django18 13 | - TOXENV=py27-django19 14 | - TOXENV=py27-django110 15 | - TOXENV=py27-django111 16 | 17 | - TOXENV=py36-django18 18 | - TOXENV=py36-django19 19 | - TOXENV=py36-django110 20 | - TOXENV=py36-django111 21 | - TOXENV=py36-django20 22 | - TOXENV=py36-django21 23 | 24 | - TOXENV=checkqa-python2 25 | - TOXENV=checkqa 26 | install: 27 | # Create pip wrapper script, using travis_retry (a function) and 28 | # inject it into tox.ini. 29 | - mkdir -p bin 30 | - PATH=$PWD/bin:$PATH 31 | - PIP_INSTALL=bin/travis_pip_install 32 | - printf '#!/bin/bash -x\n' > $PIP_INSTALL 33 | - declare -f travis_retry >> $PIP_INSTALL 34 | - printf '\necho "=====\nUsing pip-wrapper for \"$@\"\n=====\n" >&2\n' >> $PIP_INSTALL 35 | # Handle "pip install -e" for usedevelop from tox. 36 | - printf '\nif [ "$1" = "-e" ]; then pip install "$@"; exit $?; fi\n' >> $PIP_INSTALL 37 | # First try to install from wheelhouse. 38 | - printf 'for i in "$@"; do pip install --no-index --find-links=${PIP_WHEELHOUSE} "$i"; done\n' >> $PIP_INSTALL 39 | # Then upgrade in case of outdated wheelhouse. 40 | - printf 'for i in "$@"; do travis_retry pip install --upgrade "$i"; done\n' >> $PIP_INSTALL 41 | # ..and add the new/current wheels. 42 | - printf 'pip wheel --wheel-dir=${PIP_WHEELHOUSE} --find-links=${PIP_WHEELHOUSE} "$@"\n' >> $PIP_INSTALL 43 | - chmod +x $PIP_INSTALL 44 | 45 | # Adjust tox.ini. 46 | - sed -i.bak 's/^\[testenv\]/\0\ninstall_command = travis_pip_install {opts} {packages}/' tox.ini 47 | - diff tox.ini tox.ini.bak && { echo "tox.ini was not changed."; return 1; } || true 48 | - sed -i.bak 's/whitelist_externals =/\0\n travis_pip_install/' tox.ini 49 | - diff tox.ini tox.ini.bak && { echo "tox.ini was not changed."; return 1; } || true 50 | # Inject wheel dependency into tox.ini, for travis_pip_install. 51 | - sed -i.bak -e 's/deps =.*/\0\n wheel/' tox.ini 52 | - diff tox.ini tox.ini.bak && { echo "tox.ini was not changed."; return 1; } || true 53 | # Conditionally inject postgres dependency and DSM setting into tox.ini. 54 | - if [ "$USE_POSTGRES" = 1 ]; then sed -i.bak -e 's/deps =/\0\n psycopg2/' -e 's/DJANGO_SETTINGS_MODULE=test_project.settings/\0_postgres/' tox.ini && diff tox.ini tox.ini.bak && { echo "tox.ini was not changed."; return 1; } || true; fi 55 | - cat $PIP_INSTALL 56 | - cat tox.ini 57 | 58 | # Create wheels (skips existing ones from the cached wheelhouse). 59 | - export PIP_WHEELHOUSE=$PWD/wheelhouse 60 | 61 | - travis_pip_install tox 62 | - if [ -n "$EXTRAREQ" ]; then travis_pip_install $EXTRAREQ; fi 63 | 64 | - pip freeze 65 | before_script: 66 | - RUN_TESTS="tox -- rules_light" 67 | # Run tests either with or without coverage (being installed). 68 | - command -v coveralls && RUN_TESTS="$RUN_TESTS --cov" || true 69 | before_install: 70 | - npm install phantomjs 71 | - phantomjs --version 72 | script: 73 | - ls -l $PWD/wheelhouse > /tmp/wheelhouse.before 74 | - $RUN_TESTS 75 | - ls -l $PWD/wheelhouse > /tmp/wheelhouse.after 76 | - diff /tmp/wheelhouse.before /tmp/wheelhouse.after || true 77 | - test -d .tox/$TOXENV/log && cat .tox/$TOXENV/log/*.log || true 78 | after_success: 79 | - command -v coveralls && { coveralls; return $?; } || true 80 | notifications: 81 | irc: 82 | channels: 83 | - "irc.freenode.org#yourlabs" 84 | template: 85 | - "%{repository} (%{commit} %{author}) : %{message} %{build_url} %{compare_url}" 86 | 87 | # Persistent cache across builds (http://docs.travis-ci.com/user/caching/). 88 | cache: 89 | directories: 90 | - $PWD/wheelhouse 91 | before_cache: 92 | - rm -f .tox/$TOXENV/log/*.log 93 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | This attempts to be a complete list of contributors in alphabetical order. 2 | 3 | - Fabio Caritas Barrionuevo da Luz @luzfcb 4 | - Serafeim Papastefanos 5 | - James Pic 6 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.3.2 Django 4.1.x support 2 | 3 | 0.3.1 Django 4.x support 4 | Provide context for error page by @martinmaillard 5 | 6 | 0.3.0 Django 2.x support 7 | 8 | 0.1.3 Django 1.8 support by Serafeim Papastefanos @spapas. 9 | 10 | 0.1.2 Added Brazilian Portuguese Translation. 11 | 12 | 0.1.1 Decorated rule name was just 'function' in Python 3. 13 | 14 | 0.1.0 Python 3 support. 15 | 16 | 0.0.9 Greek translation, div wrapper in template. 17 | 18 | Thanks Serafeim Papastefanos for contributing. 19 | 20 | 0.0.8 Bugfix: decorated function was losing its name 21 | 22 | 0.0.7 FR locale 23 | 24 | 0.0.6 Added {% rule %} templatetag. 25 | 26 | 0.0.5 27 | 28 | - Added is_authenticated and is_staff shortcuts, 29 | - Added make_decorator which enables decorating rules with other rules. 30 | - Improved registration debug log messages with bools. 31 | 32 | 0.0.4 Django 1.4 compatibility 33 | 34 | 0.0.3 UnicodeDecodeError fixed and improved logging 35 | 36 | 0.0.2 Fixed MANIFEST.in to include templates 37 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst *.txt README LICENSE AUTHORS CHANGELOG 2 | recursive-include rules_light *.html *.css *.js *.py *.po *.mo 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://secure.travis-ci.org/yourlabs/django-rules-light.png?branch=master 2 | :target: http://travis-ci.org/yourlabs/django-rules-light 3 | .. image:: https://img.shields.io/pypi/dm/django-rules-light.svg 4 | :target: https://crate.io/packages/django-rules-light 5 | .. image:: https://img.shields.io/pypi/v/django-rules-light.svg 6 | :target: https://crate.io/packages/django-rules-light 7 | 8 | This is a simple alternative to django-rules. Its core difference is that 9 | it does not rely on models. Instead, it uses a registry which can be 10 | modified at runtime. 11 | 12 | One of its goals is to enable developers of external apps to make rules, 13 | depend on it, while allowing a project to override rules. 14 | 15 | Example ``your_app/rules_light_registry.py``: 16 | 17 | .. code-block:: python 18 | 19 | # Everybody can read a blog post (for now!): 20 | rules_light.registry['blog.post.read'] = True 21 | 22 | # Require authentication to create a blog post, using a shortcut: 23 | rules_light.registry['blog.post.create'] = rules_light.is_authenticated 24 | 25 | # But others shouldn't mess with my posts ! 26 | def is_staff_or_mine(user, rule, obj): 27 | return user.is_staff or obj.author == user 28 | 29 | rules_light.registry['blog.post.update'] = is_staff_or_mine 30 | rules_light.registry['blog.post.delete'] = is_staff_or_mine 31 | 32 | Example ``your_app/views.py``: 33 | 34 | .. code-block:: python 35 | 36 | @rules_light.class_decorator 37 | class PostDetailView(generic.DetailView): 38 | model = Post 39 | 40 | @rules_light.class_decorator 41 | class PostCreateView(generic.CreateView): 42 | model = Post 43 | 44 | @rules_light.class_decorator 45 | class PostUpdateView(generic.UpdateView): 46 | model = Post 47 | 48 | @rules_light.class_decorator 49 | class PostDeleteView(generic.DeleteView): 50 | model = Post 51 | 52 | You might want to read the `tutorial 53 | `_ for 54 | more. 55 | 56 | What's the catch ? 57 | ------------------ 58 | 59 | The catch is that this approach does not offer any feature to get secure 60 | querysets. 61 | 62 | This means you have to: 63 | 64 | - think about security when making querysets, 65 | - `override 66 | `_ 67 | eventual external app ListViews, 68 | 69 | Requirements 70 | ------------ 71 | 72 | - Python 2.7+ (Python 3 supported) 73 | - Django 1.8+ 74 | 75 | Quick Install 76 | ------------- 77 | 78 | - Install module: ``pip install django-rules-light``, 79 | - Add to ``settings.INSTALLED_APPS``: ``rules_light``, 80 | - Add in ``settings.MIDDLEWARE_CLASSES`` (or ``settings.MIDDLEWARE`` for Django 1.10+): ``rules_light.middleware.Middleware``, 81 | 82 | 83 | You might want to read the `tutorial 84 | `_. 85 | 86 | There is also a lot of documentation, from the core to the tools, including 87 | pointers to debug, log and test your security. 88 | 89 | Contributing 90 | ------------ 91 | 92 | Run tests with the `tox 93 | `_ command. Documented patches passing all 94 | tests have a better chance to get merged in. See `community guidelines 95 | `_ for details. 96 | 97 | Resources 98 | --------- 99 | 100 | To ask questions or just get informed about package updates, you could 101 | subscribe to the mailing list. 102 | 103 | - `Mailing list graciously hosted 104 | `_ by `Google 105 | `_ 106 | - `Git graciously hosted 107 | `_ by `GitHub 108 | `_, 109 | - `Documentation graciously hosted 110 | `_ by `RTFD 111 | `_, 112 | - `Package graciously hosted 113 | `_ by `PyPi 114 | `_, 115 | - `Continuous integration graciously hosted 116 | `_ by `Travis-ci 117 | `_ 118 | -------------------------------------------------------------------------------- /django_rules_light.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: django-rules-light 3 | Version: 0.3.1 4 | Summary: Rule registry for django 5 | Home-page: http://github.com/yourlabs/django-rules-light 6 | Author: James Pic 7 | Author-email: jamespic@gmail.com 8 | License: MIT 9 | Description: .. image:: https://secure.travis-ci.org/yourlabs/django-rules-light.png?branch=master 10 | :target: http://travis-ci.org/yourlabs/django-rules-light 11 | .. image:: https://img.shields.io/pypi/dm/django-rules-light.svg 12 | :target: https://crate.io/packages/django-rules-light 13 | .. image:: https://img.shields.io/pypi/v/django-rules-light.svg 14 | :target: https://crate.io/packages/django-rules-light 15 | 16 | This is a simple alternative to django-rules. Its core difference is that 17 | it does not rely on models. Instead, it uses a registry which can be 18 | modified at runtime. 19 | 20 | One of its goals is to enable developers of external apps to make rules, 21 | depend on it, while allowing a project to override rules. 22 | 23 | Example ``your_app/rules_light_registry.py``: 24 | 25 | .. code-block:: python 26 | 27 | # Everybody can read a blog post (for now!): 28 | rules_light.registry['blog.post.read'] = True 29 | 30 | # Require authentication to create a blog post, using a shortcut: 31 | rules_light.registry['blog.post.create'] = rules_light.is_authenticated 32 | 33 | # But others shouldn't mess with my posts ! 34 | def is_staff_or_mine(user, rule, obj): 35 | return user.is_staff or obj.author == user 36 | 37 | rules_light.registry['blog.post.update'] = is_staff_or_mine 38 | rules_light.registry['blog.post.delete'] = is_staff_or_mine 39 | 40 | Example ``your_app/views.py``: 41 | 42 | .. code-block:: python 43 | 44 | @rules_light.class_decorator 45 | class PostDetailView(generic.DetailView): 46 | model = Post 47 | 48 | @rules_light.class_decorator 49 | class PostCreateView(generic.CreateView): 50 | model = Post 51 | 52 | @rules_light.class_decorator 53 | class PostUpdateView(generic.UpdateView): 54 | model = Post 55 | 56 | @rules_light.class_decorator 57 | class PostDeleteView(generic.DeleteView): 58 | model = Post 59 | 60 | You might want to read the `tutorial 61 | `_ for 62 | more. 63 | 64 | What's the catch ? 65 | ------------------ 66 | 67 | The catch is that this approach does not offer any feature to get secure 68 | querysets. 69 | 70 | This means you have to: 71 | 72 | - think about security when making querysets, 73 | - `override 74 | `_ 75 | eventual external app ListViews, 76 | 77 | Requirements 78 | ------------ 79 | 80 | - Python 2.7+ (Python 3 supported) 81 | - Django 1.8+ 82 | 83 | Quick Install 84 | ------------- 85 | 86 | - Install module: ``pip install django-rules-light``, 87 | - Add to ``settings.INSTALLED_APPS``: ``rules_light``, 88 | - Add in ``settings.MIDDLEWARE_CLASSES`` (or ``settings.MIDDLEWARE`` for Django 1.10+): ``rules_light.middleware.Middleware``, 89 | 90 | 91 | You might want to read the `tutorial 92 | `_. 93 | 94 | There is also a lot of documentation, from the core to the tools, including 95 | pointers to debug, log and test your security. 96 | 97 | Contributing 98 | ------------ 99 | 100 | Run tests with the `tox 101 | `_ command. Documented patches passing all 102 | tests have a better chance to get merged in. See `community guidelines 103 | `_ for details. 104 | 105 | Resources 106 | --------- 107 | 108 | To ask questions or just get informed about package updates, you could 109 | subscribe to the mailing list. 110 | 111 | - `Mailing list graciously hosted 112 | `_ by `Google 113 | `_ 114 | - `Git graciously hosted 115 | `_ by `GitHub 116 | `_, 117 | - `Documentation graciously hosted 118 | `_ by `RTFD 119 | `_, 120 | - `Package graciously hosted 121 | `_ by `PyPi 122 | `_, 123 | - `Continuous integration graciously hosted 124 | `_ by `Travis-ci 125 | `_ 126 | 127 | Keywords: django security rules acl rbac 128 | Platform: UNKNOWN 129 | Classifier: Development Status :: 5 - Production/Stable 130 | Classifier: Intended Audience :: Developers 131 | Classifier: License :: OSI Approved :: MIT License 132 | Classifier: Operating System :: OS Independent 133 | Classifier: Programming Language :: Python 134 | Classifier: Programming Language :: Python :: 2 135 | Classifier: Programming Language :: Python :: 3 136 | Classifier: Topic :: Software Development :: Libraries :: Python Modules 137 | -------------------------------------------------------------------------------- /django_rules_light.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | AUTHORS 2 | CHANGELOG 3 | MANIFEST.in 4 | README.rst 5 | setup.cfg 6 | setup.py 7 | django_rules_light.egg-info/PKG-INFO 8 | django_rules_light.egg-info/SOURCES.txt 9 | django_rules_light.egg-info/dependency_links.txt 10 | django_rules_light.egg-info/not-zip-safe 11 | django_rules_light.egg-info/requires.txt 12 | django_rules_light.egg-info/top_level.txt 13 | rules_light/__init__.py 14 | rules_light/apps.py 15 | rules_light/class_decorator.py 16 | rules_light/decorators.py 17 | rules_light/exceptions.py 18 | rules_light/middleware.py 19 | rules_light/models.py 20 | rules_light/registry.py 21 | rules_light/rules_light_registry.py 22 | rules_light/shortcuts.py 23 | rules_light/urls.py 24 | rules_light/views.py 25 | rules_light/locale/el/LC_MESSAGES/django.mo 26 | rules_light/locale/el/LC_MESSAGES/django.po 27 | rules_light/locale/fr/LC_MESSAGES/django.mo 28 | rules_light/locale/fr/LC_MESSAGES/django.po 29 | rules_light/locale/pt_BR/LC_MESSAGES/django.mo 30 | rules_light/locale/pt_BR/LC_MESSAGES/django.po 31 | rules_light/static/rules_light/github.css 32 | rules_light/static/rules_light/rainbow-custom.min.js 33 | rules_light/templates/rules_light/base.html 34 | rules_light/templates/rules_light/exception.html 35 | rules_light/templates/rules_light/registry.html 36 | rules_light/templatetags/__init__.py 37 | rules_light/templatetags/rules_light_tags.py 38 | rules_light/tests/__init__.py 39 | rules_light/tests/test_autodiscover.py 40 | rules_light/tests/test_class_decorator.py 41 | rules_light/tests/test_decorators.py 42 | rules_light/tests/test_middleware.py 43 | rules_light/tests/test_registry.py 44 | rules_light/tests/test_shortcuts.py 45 | rules_light/tests/test_views.py 46 | rules_light/tests/fixtures/__init__.py 47 | rules_light/tests/fixtures/class_decorator_classes.py -------------------------------------------------------------------------------- /django_rules_light.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /django_rules_light.egg-info/not-zip-safe: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /django_rules_light.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | six 2 | django-classy-tags 3 | -------------------------------------------------------------------------------- /django_rules_light.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | rules_light 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | set DJANGO_SETTINGS_MODULE="test_project.test_project.settings" 46 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 47 | @echo 48 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 49 | 50 | dirhtml: 51 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 52 | @echo 53 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 54 | 55 | singlehtml: 56 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 57 | @echo 58 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 59 | 60 | pickle: 61 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 62 | @echo 63 | @echo "Build finished; now you can process the pickle files." 64 | 65 | json: 66 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 67 | @echo 68 | @echo "Build finished; now you can process the JSON files." 69 | 70 | htmlhelp: 71 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 72 | @echo 73 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 74 | ".hhp project file in $(BUILDDIR)/htmlhelp." 75 | 76 | qthelp: 77 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 78 | @echo 79 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 80 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 81 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-rules-light.qhcp" 82 | @echo "To view the help file:" 83 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-rules-light.qhc" 84 | 85 | devhelp: 86 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 87 | @echo 88 | @echo "Build finished." 89 | @echo "To view the help file:" 90 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-rules-light" 91 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-rules-light" 92 | @echo "# devhelp" 93 | 94 | epub: 95 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 96 | @echo 97 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 98 | 99 | latex: 100 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 101 | @echo 102 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 103 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 104 | "(use \`make latexpdf' here to do that automatically)." 105 | 106 | latexpdf: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo "Running LaTeX files through pdflatex..." 109 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 110 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 111 | 112 | text: 113 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 114 | @echo 115 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 116 | 117 | man: 118 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 119 | @echo 120 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 121 | 122 | texinfo: 123 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 124 | @echo 125 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 126 | @echo "Run \`make' in that directory to run these through makeinfo" \ 127 | "(use \`make info' here to do that automatically)." 128 | 129 | info: 130 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 131 | @echo "Running Texinfo files through makeinfo..." 132 | make -C $(BUILDDIR)/texinfo info 133 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 134 | 135 | gettext: 136 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 137 | @echo 138 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 139 | 140 | changes: 141 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 142 | @echo 143 | @echo "The overview file is in $(BUILDDIR)/changes." 144 | 145 | linkcheck: 146 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 147 | @echo 148 | @echo "Link check complete; look for any errors in the above output " \ 149 | "or in $(BUILDDIR)/linkcheck/output.txt." 150 | 151 | doctest: 152 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 153 | @echo "Testing of doctests in the sources finished, look at the " \ 154 | "results in $(BUILDDIR)/doctest/output.txt." 155 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-rules-light.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-rules-light.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | sqlparse>=0.5.0 # not directly required, pinned by Snyk to avoid a vulnerability 3 | -------------------------------------------------------------------------------- /docs/source/class_decorator.rst: -------------------------------------------------------------------------------- 1 | Class decorator 2 | =============== 3 | 4 | API 5 | --- 6 | 7 | .. automodule:: rules_light.class_decorator 8 | :members: 9 | 10 | Examples 11 | -------- 12 | 13 | .. literalinclude:: ../../rules_light/tests/class_decorator_classes.py 14 | :language: python 15 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-rules-light documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Nov 26 00:08:46 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os, os.path 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | sys.path.insert(0, os.path.abspath('../../')) 21 | sys.path.insert(0, os.path.abspath('../../../../lib/python2.7/site-packages/')) 22 | from django.conf import settings 23 | settings.configure() 24 | 25 | autoclass_content = "both" 26 | 27 | # -- General configuration ----------------------------------------------------- 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | #needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be extensions 33 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 34 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | #source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = u'django-rules-light' 50 | copyright = u'2012-2015, James Pic and contributors' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = '0.3' 58 | # The full version, including alpha/beta/rc tags. 59 | release = '0.3.0' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | #language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | #today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | #today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = [] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all documents. 76 | #default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | #add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | #add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | #show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | # A list of ignored prefixes for module index sorting. 93 | #modindex_common_prefix = [] 94 | 95 | 96 | # -- Options for HTML output --------------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | html_theme = 'default' 101 | 102 | # Theme options are theme-specific and customize the look and feel of a theme 103 | # further. For a list of options available for each theme, see the 104 | # documentation. 105 | #html_theme_options = {} 106 | 107 | # Add any paths that contain custom themes here, relative to this directory. 108 | #html_theme_path = [] 109 | 110 | # The name for this set of Sphinx documents. If None, it defaults to 111 | # " v documentation". 112 | #html_title = None 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | #html_short_title = None 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | #html_logo = None 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | #html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | html_static_path = ['_static'] 130 | 131 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 132 | # using the given strftime format. 133 | #html_last_updated_fmt = '%b %d, %Y' 134 | 135 | # If true, SmartyPants will be used to convert quotes and dashes to 136 | # typographically correct entities. 137 | #html_use_smartypants = True 138 | 139 | # Custom sidebar templates, maps document names to template names. 140 | #html_sidebars = {} 141 | 142 | # Additional templates that should be rendered to pages, maps page names to 143 | # template names. 144 | #html_additional_pages = {} 145 | 146 | # If false, no module index is generated. 147 | #html_domain_indices = True 148 | 149 | # If false, no index is generated. 150 | #html_use_index = True 151 | 152 | # If true, the index is split into individual pages for each letter. 153 | #html_split_index = False 154 | 155 | # If true, links to the reST sources are added to the pages. 156 | #html_show_sourcelink = True 157 | 158 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 159 | #html_show_sphinx = True 160 | 161 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 162 | #html_show_copyright = True 163 | 164 | # If true, an OpenSearch description file will be output, and all pages will 165 | # contain a tag referring to it. The value of this option must be the 166 | # base URL from which the finished HTML is served. 167 | #html_use_opensearch = '' 168 | 169 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 170 | #html_file_suffix = None 171 | 172 | # Output file base name for HTML help builder. 173 | htmlhelp_basename = 'django-rules-lightdoc' 174 | 175 | 176 | # -- Options for LaTeX output -------------------------------------------------- 177 | 178 | latex_elements = { 179 | # The paper size ('letterpaper' or 'a4paper'). 180 | #'papersize': 'letterpaper', 181 | 182 | # The font size ('10pt', '11pt' or '12pt'). 183 | #'pointsize': '10pt', 184 | 185 | # Additional stuff for the LaTeX preamble. 186 | #'preamble': '', 187 | } 188 | 189 | # Grouping the document tree into LaTeX files. List of tuples 190 | # (source start file, target name, title, author, documentclass [howto/manual]). 191 | latex_documents = [ 192 | ('index', 'django-rules-light.tex', u'django-rules-light Documentation', 193 | u'James Pic', 'manual'), 194 | ] 195 | 196 | # The name of an image file (relative to this directory) to place at the top of 197 | # the title page. 198 | #latex_logo = None 199 | 200 | # For "manual" documents, if this is true, then toplevel headings are parts, 201 | # not chapters. 202 | #latex_use_parts = False 203 | 204 | # If true, show page references after internal links. 205 | #latex_show_pagerefs = False 206 | 207 | # If true, show URL addresses after external links. 208 | #latex_show_urls = False 209 | 210 | # Documents to append as an appendix to all manuals. 211 | #latex_appendices = [] 212 | 213 | # If false, no module index is generated. 214 | #latex_domain_indices = True 215 | 216 | 217 | # -- Options for manual page output -------------------------------------------- 218 | 219 | # One entry per manual page. List of tuples 220 | # (source start file, name, description, authors, manual section). 221 | man_pages = [ 222 | ('index', 'django-rules-light', u'django-rules-light Documentation', 223 | [u'James Pic'], 1) 224 | ] 225 | 226 | # If true, show URL addresses after external links. 227 | #man_show_urls = False 228 | 229 | 230 | # -- Options for Texinfo output ------------------------------------------------ 231 | 232 | # Grouping the document tree into Texinfo files. List of tuples 233 | # (source start file, target name, title, author, 234 | # dir menu entry, description, category) 235 | texinfo_documents = [ 236 | ('index', 'django-rules-light', u'django-rules-light Documentation', 237 | u'James Pic', 'django-rules-light', 'One line description of project.', 238 | 'Miscellaneous'), 239 | ] 240 | 241 | # Documents to append as an appendix to all manuals. 242 | #texinfo_appendices = [] 243 | 244 | # If false, no module index is generated. 245 | #texinfo_domain_indices = True 246 | 247 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 248 | #texinfo_show_urls = 'footnote' 249 | 250 | 251 | # Example configuration for intersphinx: refer to the Python standard library. 252 | intersphinx_mapping = {'http://docs.python.org/': None} 253 | -------------------------------------------------------------------------------- /docs/source/debug.rst: -------------------------------------------------------------------------------- 1 | Debugging 2 | ========= 3 | 4 | Two tools are provided to debug issues with your registry: 5 | 6 | - the :doc:`logger logs` everything (and it likes to log malicious 7 | users too), 8 | - the url provides a live rule registry browser (see below). 9 | 10 | As usual, resort to ``ipdb``, for example in 11 | ``rules_light.RuleRegistry.run()`` place:: 12 | 13 | import ipdb; ipdb.set_trace() 14 | 15 | The registry browser 16 | -------------------- 17 | 18 | .. automodule:: rules_light.views 19 | :members: 20 | -------------------------------------------------------------------------------- /docs/source/decorator.rst: -------------------------------------------------------------------------------- 1 | Make your decorators 2 | ==================== 3 | 4 | .. automodule:: rules_light.decorators 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. django-rules-light documentation master file, created by 2 | sphinx-quickstart on Mon Nov 26 00:08:46 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-rules-light's documentation! 7 | ============================================== 8 | 9 | .. include:: ../../README.rst 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | tutorial 17 | registry 18 | class_decorator 19 | middleware 20 | shortcuts 21 | decorators 22 | logging 23 | debug 24 | testing 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | 33 | -------------------------------------------------------------------------------- /docs/source/logging.rst: -------------------------------------------------------------------------------- 1 | Logging 2 | ======= 3 | 4 | Everything is logged in the ``rules_light`` logger: 5 | 6 | - rule registered is logged with ``DEBUG`` level, 7 | - rule ``run()`` is logged with ``INFO`` level, 8 | - ``require()`` failure is logged with ``WARN`` level. 9 | 10 | Install 11 | ------- 12 | 13 | Example ``settings.LOGGING`` that will display all logged events in the 14 | console, as well as denials in malicious.log. 15 | 16 | See http://docs.djangoproject.com/en/dev/topics/logging for 17 | more details on how to customize your logging configuration. 18 | 19 | 20 | .. literalinclude:: ../../test_project/test_project/rules_logging.py 21 | :language: python 22 | -------------------------------------------------------------------------------- /docs/source/middleware.rst: -------------------------------------------------------------------------------- 1 | Middleware 2 | ========== 3 | 4 | .. automodule:: rules_light.middleware 5 | :members: 6 | 7 | Template 8 | -------- 9 | 10 | .. literalinclude:: ../../rules_light/templates/rules_light/exception.html 11 | :language: django 12 | -------------------------------------------------------------------------------- /docs/source/registry.rst: -------------------------------------------------------------------------------- 1 | Rule registry 2 | ============= 3 | 4 | API 5 | --- 6 | 7 | .. automodule:: rules_light.registry 8 | :members: 9 | 10 | Examples 11 | -------- 12 | 13 | .. literalinclude:: ../../test_project/auth_rules.py 14 | :language: python 15 | 16 | Even django-rules-light's view uses a permission, it is registered in 17 | ``rules_light/rules_light_registry.py`` and thus is picked up by 18 | ``rules_light.autodiscover()``: 19 | 20 | .. literalinclude:: ../../rules_light/rules_light_registry.py 21 | :language: python 22 | 23 | Of course, you could use any callable instead of the lambda function. 24 | -------------------------------------------------------------------------------- /docs/source/shortcuts.rst: -------------------------------------------------------------------------------- 1 | Shortcuts 2 | ========= 3 | 4 | .. automodule:: rules_light.shortcuts 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/testing.rst: -------------------------------------------------------------------------------- 1 | Security testing 2 | ================ 3 | 4 | It is important to test your security. Here is an example: 5 | 6 | .. literalinclude:: ../../rules_light/tests/views.py 7 | :language: python 8 | -------------------------------------------------------------------------------- /docs/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | Install 5 | ------- 6 | 7 | Either install the last release:: 8 | 9 | pip install django-rules-light 10 | 11 | Either install a development version:: 12 | 13 | pip install -e git+https://github.com/yourlabs/django-rules-light.git#egg=django-rules-light 14 | 15 | That should be enough to work with the registry. 16 | 17 | Middleware 18 | `````````` 19 | 20 | To enable the middleware that processes ``rules_light.Denied`` 21 | exception, add to ``setings.MIDDLEWARE_CLASSES`` or ``settings.MIDDLEWARE`` for Django >= 1.10: 22 | 23 | .. code-block:: python 24 | 25 | MIDDLEWARE_CLASSES = ( 26 | # ... 27 | 'rules_light.middleware.Middleware', 28 | ) 29 | 30 | See :doc:`docs on middleware` for more details. 31 | 32 | Logging 33 | ``````` 34 | 35 | To enable logging, add a ``rules_light`` logger for example: 36 | 37 | .. code-block:: python 38 | 39 | LOGGING = { 40 | # ... 41 | 'handlers': { 42 | # ... 43 | 'console':{ 44 | 'level':'DEBUG', 45 | 'class':'logging.StreamHandler', 46 | }, 47 | }, 48 | 'loggers': { 49 | 'rules_light': { 50 | 'handlers': ['console'], 51 | 'propagate': True, 52 | 'level': 'DEBUG', 53 | } 54 | } 55 | } 56 | 57 | See :doc:`docs on logging` for more details on logging. 58 | 59 | Debug view 60 | `````````` 61 | 62 | Add to ``settings.INSTALLED_APPS``: 63 | 64 | .. code-block:: python 65 | 66 | INSTALLED_APPS = ( 67 | 'rules_light', 68 | # .... 69 | ) 70 | 71 | Then the view should be usable, install it as such: 72 | 73 | .. code-block:: python 74 | 75 | url(r'^rules/', include('rules_light.urls')), 76 | 77 | See :doc:`docs on debugging` for more details on debugging rules. 78 | 79 | Creating Rules 80 | -------------- 81 | 82 | Declare rules 83 | ````````````` 84 | 85 | Declaring rules consist of filling up the ``rules_light.registry`` dict. This 86 | dict uses rule "names" as keys, ie. ``do_something``, 87 | ``some_app.some_model.create``, etc, etc ... For values, it can use booleans: 88 | 89 | .. code-block:: python 90 | 91 | # Enable read for everybody 92 | rules_light.registry['your_app.your_model.read'] = True 93 | 94 | # Disable delete for everybody 95 | rules_light.registry['your_app.your_model.delete'] = False 96 | 97 | Optionnaly, use the Python dict method ``setdefault()`` in default rules. For 98 | example: 99 | 100 | .. code-block:: python 101 | 102 | # Only allow everybody if another (project-specific) callback was not set 103 | rules_light.registry.setdefault('your_app.your_model.read', True) 104 | 105 | It can also use callbacks: 106 | 107 | .. code-block:: python 108 | 109 | def your_custom_rule(user, rule_name, model, *args, **kwargs): 110 | if user in model.your_custom_stuff: 111 | return True # Allow user ! 112 | 113 | rules_light.registry['app.model.read'] = your_custom_rule 114 | 115 | See :doc:`docs on registry` for more details. 116 | 117 | Mix rules, DRY security 118 | ``````````````````````` 119 | 120 | Callbacks may also be used to decorate each other, using 121 | ``rules_light.make_decorator()`` will transform a simple rule callback, into a 122 | rule callback that can also be used as decorator for another callback. 123 | 124 | Just decorate a callback with ``make_decorator()`` to make it reusable as 125 | decorator: 126 | 127 | .. code-block:: python 128 | 129 | @rules_light.make_decorator 130 | def some_condition(user, rule, *args, **kwargs): 131 | # do stuff 132 | 133 | rules_light.registry.setdefault('your_app.your_model.create', some_condition) 134 | 135 | @some_condition 136 | def extra_condition(user, rule, *args, **kwargs): 137 | # do extra stuff 138 | 139 | rules_light.registry.setdefault('your_app.your_model.update', extra_condition) 140 | 141 | This will cause ``some_condition()`` to be evaluated first, and if it passes, 142 | ``extra_condition()`` will be evaluated to, for the update rule. 143 | 144 | See :doc:`docs on decorator` for more details. 145 | 146 | Using rules 147 | ----------- 148 | 149 | The rule registry is in charge of using rules, using the ``run()`` method. It 150 | should return True or False. 151 | 152 | Run 153 | ``` 154 | 155 | For example with this: 156 | 157 | .. code-block:: python 158 | 159 | def some_condition(user, rulename, *args, **kwargs): 160 | # ... 161 | 162 | rules_light.registry['your_app.your_model.create'] = some_condition 163 | 164 | Doing: 165 | 166 | .. code-block:: python 167 | 168 | rules_light.run(request.user, 'your_app.your_model.create') 169 | 170 | Will call: 171 | 172 | .. code-block:: python 173 | 174 | some_condition(request.user, 'your_app.your_model.create') 175 | 176 | Kwargs are forwarded, for example: 177 | 178 | .. code-block:: python 179 | 180 | rules_light.run(request.user, 'your_app.your_model.create', 181 | with_widget=request.GET['widget']) 182 | 183 | Will call: 184 | 185 | .. code-block:: python 186 | 187 | some_condition(request.user, 'your_app.your_model.create', 188 | with_widget=request.GET['widget']) 189 | 190 | See :doc:`docs on registry` for more details. 191 | 192 | Require 193 | ``````` 194 | 195 | The ``require()`` method is useful too, it does the same as ``run()`` except 196 | that it will raise ``rules_light.Denied``. This will block the request process 197 | and will be catched by the middleware if installed. 198 | 199 | See :doc:`docs on registry` for more details. 200 | 201 | Decorator 202 | ````````` 203 | 204 | You can decorate a class based view as such: 205 | 206 | .. code-block:: python 207 | 208 | @rules_light.class_decorator 209 | class SomeCreateView(views.CreateView): 210 | model=SomeModel 211 | 212 | This will automatically require ``'some_app.some_model.create'``. 213 | 214 | See :doc:`docs on class decorator` for more usages of the decorator. 215 | 216 | Template 217 | ```````` 218 | 219 | In templates, you can run rules using '{% rule %}' templatetag. 220 | 221 | Usage: 222 | 223 | .. code-block:: django 224 | 225 | {% rule rule_name [args] [kwargs] as var_name %} 226 | 227 | This is an example from the test project: 228 | 229 | .. code-block:: django 230 | 231 | {% load rules_light_tags %} 232 | 233 | 244 | 245 | 246 | Tips and tricks 247 | --------------- 248 | 249 | Override rules 250 | `````````````` 251 | 252 | If your project wants to change the behaviour of ``your_app`` to allows users 253 | to create models and edit the models they have created, you could add after 254 | ``rules_light.autodiscover()``: 255 | 256 | .. code-block:: python 257 | 258 | def my_model_or_staff(user, rulename, obj): 259 | return user.is_staff or user == obj.author 260 | 261 | rules_light.registry['your_app.your_model.create'] = True 262 | rules_light.registry['your_app.your_model.update'] = my_model_or_staff 263 | rules_light.registry['your_app.your_model.delete'] = my_model_or_staff 264 | 265 | As you can see, a project can **completely** change the security logic of an 266 | app, which should enpower creative django developers hehe ... 267 | 268 | See :doc:`docs on registry` for more details. 269 | 270 | Take a shortcut 271 | ``````````````` 272 | 273 | django-rules-light comes with a predefined ``is_staff`` rule which you could 274 | use in ``your_app/rules_light_registry.py``: 275 | 276 | .. code-block:: python 277 | 278 | import rules_light 279 | 280 | # Allow all users to see your_model 281 | rules_light.registry.setdefault('your_app.your_model.read', True) 282 | 283 | # Allow admins to create and edit models 284 | rules_light.registry.setdefault('your_app.your_model.create', rules_light.is_staff) 285 | rules_light.registry.setdefault('your_app.your_model.update', rules_light.is_staff) 286 | rules_light.registry.setdefault('your_app.your_model.delete', rules_light.is_staff) 287 | 288 | See :doc:`docs on shortcuts`. 289 | 290 | Test security 291 | ````````````` 292 | 293 | See :doc:`security testing docs`. 294 | -------------------------------------------------------------------------------- /rules_light/__init__.py: -------------------------------------------------------------------------------- 1 | from .registry import RuleRegistry, registry, run, require, autodiscover 2 | from .class_decorator import class_decorator 3 | from .decorators import make_decorator 4 | from .exceptions import Denied, DoesNotExist, RulesLightException 5 | from .middleware import Middleware 6 | from .shortcuts import is_authenticated, is_staff 7 | 8 | default_app_config = 'rules_light.apps.RulesLightConfig' 9 | 10 | __version__ = (0, 2, 0) 11 | -------------------------------------------------------------------------------- /rules_light/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RulesLightConfig(AppConfig): 5 | name = 'rules_light' 6 | 7 | def ready(self): 8 | from rules_light.registry import autodiscover 9 | autodiscover() 10 | -------------------------------------------------------------------------------- /rules_light/class_decorator.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | from __future__ import unicode_literals 5 | import six 6 | 7 | import django 8 | from django.views import generic 9 | 10 | from .exceptions import RulesLightException 11 | from .registry import registry 12 | 13 | 14 | __all__ = ('class_decorator',) 15 | 16 | 17 | def patch_get_object(cls, suffix, override): 18 | old_get_object = cls.get_object 19 | 20 | def new_get_object(self, *args, **kwargs): 21 | obj = old_get_object(self, *args, **kwargs) 22 | 23 | if self.get_object._rule_override: 24 | rule_name = self.get_object._rule_override 25 | else: 26 | try: 27 | model_name = obj.__class__._meta.model_name 28 | except AttributeError: 29 | model_name = obj.__class__._meta.module_name 30 | rule_name = '%s.%s.%s' % (obj.__class__._meta.app_label, 31 | model_name, self.get_object._rule_suffix) 32 | 33 | registry.require(self.request.user, rule_name, obj) 34 | 35 | return obj 36 | 37 | new_get_object._rule_suffix = suffix 38 | new_get_object._rule_override = override 39 | cls.get_object = new_get_object 40 | 41 | 42 | class class_decorator(object): 43 | """ 44 | Can be used to secure class based views. 45 | 46 | If the view has ``model=YourModel``, it will support: 47 | 48 | - ``CreateView``, it will decorate ``get_form()``, to run 49 | ``rules_light.require('yourapp.yourmodel.create')``, 50 | - ``UpdateView``, it will decorate ``get_object()``, to run 51 | ``rules_light.require('yourapp.yourmodel.update', obj)``, 52 | - ``DeleteView``, it will decorate ``get_object()``, to run 53 | ``rules_light.require('yourapp.yourmodel.delete', obj)``, 54 | - ``DetailView``, it will decorate ``get_object()``, to run 55 | ``rules_light.require('yourapp.yourmodel.read', obj)``, 56 | - others views, if the rule name is specified in the decorator for example 57 | ``@class_decorator('some_rule')``, then it will decorate either 58 | ``get_object()`` if the CBV is subclassed from ``SingleObjectMixin`` 59 | else it will decorate ``dispatch()``, 60 | - Else it raises an exception. 61 | """ 62 | rule = None 63 | 64 | def __new__(self, *args): 65 | if hasattr(args[0], 'as_view'): 66 | cls = args[0] 67 | elif isinstance(args[0], six.string_types): 68 | if six.PY2: 69 | decorator_name = b'new_class_decorator' 70 | elif six.PY3: 71 | decorator_name = 'new_class_decorator' 72 | 73 | new_class_decorator = type(decorator_name, 74 | (class_decorator,), {'rule': args[0]}) 75 | return new_class_decorator 76 | elif hasattr(args[0], '__call__'): 77 | raise Exception("No function support") 78 | else: 79 | raise Exception("What?") 80 | 81 | if issubclass(cls, generic.CreateView): 82 | old_get_form = cls.get_form 83 | 84 | if django.VERSION >= (1, 8): 85 | def new_get_form(self, *args, **kwargs): 86 | model = self.get_form_class().Meta.model 87 | try: 88 | model_name = model._meta.model_name 89 | except AttributeError: 90 | model_name = model._meta.module_name 91 | rule_name = '%s.%s.create' % (model._meta.app_label, 92 | model_name) 93 | 94 | registry.require(self.request.user, rule_name) 95 | 96 | return old_get_form(self, *args, **kwargs) 97 | 98 | else: 99 | def new_get_form(self, form_class, *args, **kwargs): 100 | model = form_class.Meta.model 101 | try: 102 | model_name = model._meta.model_name 103 | except AttributeError: 104 | model_name = model._meta.module_name 105 | rule_name = '%s.%s.create' % (model._meta.app_label, 106 | model_name) 107 | 108 | registry.require(self.request.user, rule_name) 109 | 110 | return old_get_form(self, form_class, *args, **kwargs) 111 | 112 | cls.get_form = new_get_form 113 | 114 | elif issubclass(cls, generic.UpdateView): 115 | patch_get_object(cls, 'update', self.rule) 116 | 117 | elif issubclass(cls, generic.DetailView): 118 | patch_get_object(cls, 'read', self.rule) 119 | 120 | elif issubclass(cls, generic.DeleteView): 121 | patch_get_object(cls, 'delete', self.rule) 122 | 123 | elif self.rule: 124 | if issubclass(cls, generic.detail.SingleObjectMixin): 125 | patch_get_object(cls, 'generic', self.rule) 126 | else: 127 | old_dispatch = cls.dispatch 128 | 129 | def new_dispatch(self, request, *args, **kwargs): 130 | registry.require(request.user, self.dispatch._rule) 131 | return old_dispatch(self, request, *args, **kwargs) 132 | new_dispatch._rule = self.rule 133 | cls.dispatch = new_dispatch 134 | 135 | else: 136 | raise RulesLightException('Dont understand what to do') 137 | 138 | return cls 139 | -------------------------------------------------------------------------------- /rules_light/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module enables piling rules on each others. 3 | 4 | Consider this simple rule:: 5 | 6 | def is_authenticated(user, *args, **kwargs): 7 | return user and user.is_authenticated() 8 | 9 | It can of course be used directly:: 10 | 11 | rules_light.registry['do_something'] = is_authenticated 12 | 13 | But if defined using ``make_decorator`` as such:: 14 | 15 | @rules_light.make_decorator 16 | def is_authenticated(user, *args, **kwargs): 17 | return user and user.is_authenticated() 18 | 19 | Then you can use it to decorate other rules too:: 20 | 21 | @is_authenticated 22 | def my_book(user, rule, book): 23 | return user == book.author 24 | 25 | rules_light.registry['do_something'] = my_book 26 | 27 | """ 28 | from __future__ import unicode_literals 29 | 30 | 31 | def make_decorator(_rule): 32 | def _decorator(*args, **kwargs): 33 | if len(args) == 1 and len(kwargs) == 0: 34 | func = args[0] 35 | 36 | def _decorated(user, rule, *args, **kwargs): 37 | if not _rule(user, rule, *args, **kwargs): 38 | return False 39 | return func(user, rule, *args, **kwargs) 40 | _decorated.__name__ = func.__name__ 41 | return _decorated 42 | else: # rule 43 | return _rule(*args, **kwargs) 44 | _decorator.__name__ = _rule.__name__ 45 | return _decorator 46 | -------------------------------------------------------------------------------- /rules_light/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | 4 | class RulesLightException(Exception): 5 | """ Base class for all exceptions of this package. """ 6 | pass 7 | 8 | 9 | class Denied(RulesLightException): 10 | def __init__(self, rule_text): 11 | super(Denied, self).__init__(u'%s evaluates to False' % rule_text) 12 | 13 | 14 | class DoesNotExist(RulesLightException): 15 | def __init__(self, name): 16 | super(DoesNotExist, self).__init__( 17 | u'Rule "%s" is not registered' % name) 18 | -------------------------------------------------------------------------------- /rules_light/locale/el/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/rules_light/locale/el/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /rules_light/locale/el/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-03-19 21:10+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 19 | 20 | #: .\templates\rules_light\exception.html.py:6 21 | msgid "You do not have permission to do that." 22 | msgstr "Δεν έχετε δικαιώμα να πραγματοποιήσετε τη συγκεκριμένη ενέργεια." 23 | 24 | #: .\templates\rules_light\exception.html.py:9 25 | msgid "Try logging in" 26 | msgstr "Παρακαλώ δοκιμάστε να συνδεθείτε" 27 | 28 | #: .\templates\rules_light\exception.html.py:9 29 | msgid "with other credentials" 30 | msgstr "με διαφορετικά στοιχεία" 31 | -------------------------------------------------------------------------------- /rules_light/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/rules_light/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /rules_light/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-02-13 16:16+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 20 | 21 | #: templates/rules_light/exception.html:6 22 | msgid "You do not have permission to do that." 23 | msgstr "Vous n'en avez pas la permission" 24 | 25 | #: templates/rules_light/exception.html:9 26 | msgid "Try logging in" 27 | msgstr "Essayez de vous authentifier" 28 | 29 | #: templates/rules_light/exception.html:9 30 | msgid "with other credentials" 31 | msgstr "avec d'autres accès" 32 | -------------------------------------------------------------------------------- /rules_light/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/rules_light/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /rules_light/locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 James Pic 2 | # This file is distributed under the same license as the 3 | # django-session-security package. 4 | # James Pic 2013 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 0.1.2\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-12-11 13:47-0300\n" 12 | "PO-Revision-Date: 2013-12-11 13:47-0300\n" 13 | "Last-Translator: Fabio Caritas Barrionuevo da Luz \n" 14 | "Language: Brazilian Portuguese\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: templates/rules_light/exception.html:7 21 | msgid "You do not have permission to do that." 22 | msgstr "Você não possui permissão para fazer isso." 23 | 24 | #: templates/rules_light/exception.html:10 25 | msgid "Try logging in" 26 | msgstr "Tente efetuar login" 27 | 28 | #: templates/rules_light/exception.html:10 29 | msgid "with other credentials" 30 | msgstr "com outras credenciais" 31 | -------------------------------------------------------------------------------- /rules_light/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | The role of the middleware is to present a user friendly error page when a rule 3 | denied process of the request by raising ``Denied``. 4 | """ 5 | from __future__ import unicode_literals 6 | 7 | from django import template, http, VERSION 8 | 9 | from django.conf import settings 10 | 11 | from .exceptions import RulesLightException 12 | 13 | 14 | class Middleware(object): 15 | """ 16 | Install this middleware by adding `rules_light.middleware.Middleware`` to 17 | ``settings.MIDDLEWARE_CLASSES`` or ``settings.MIDDLEWARE`` for Django1.10+ 18 | """ 19 | def process_exception(self, request, exception): 20 | """ 21 | Render ``rules_light/exception.html`` when a ``Denied`` exception was 22 | raised. 23 | """ 24 | if not isinstance(exception, RulesLightException): 25 | return 26 | 27 | if VERSION > (1, 8): 28 | ctx = dict(request=request, exception=exception, settings=settings) 29 | else: 30 | ctx = template.RequestContext(request, dict(exception=exception, 31 | settings=settings)) 32 | return http.HttpResponseForbidden(template.loader.render_to_string( 33 | 'rules_light/exception.html', ctx)) 34 | 35 | def __init__(self, get_response=None): 36 | super(Middleware, self).__init__() 37 | # Support Django 1.10 middleware. 38 | if get_response is not None: 39 | self.get_response = get_response 40 | 41 | def __call__(self, request): 42 | return self.get_response(request) 43 | -------------------------------------------------------------------------------- /rules_light/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/rules_light/models.py -------------------------------------------------------------------------------- /rules_light/registry.py: -------------------------------------------------------------------------------- 1 | """ 2 | The rule registry is in charge of keeping and executing security rules. 3 | 4 | It is the core of this app, everything else is optionnal. 5 | 6 | This module provides a variable, ``registry``, which is just a module-level, 7 | default RuleRegistry instance. 8 | 9 | A rule can be a callback or a variable that will be evaluated as bool. 10 | """ 11 | from __future__ import unicode_literals 12 | 13 | import logging 14 | 15 | from django.utils.encoding import smart_str as smart_text 16 | 17 | try: 18 | from django.utils.module_loading import autodiscover_modules 19 | except ImportError: 20 | autodiscover_modules = None 21 | 22 | from .exceptions import Denied, DoesNotExist 23 | 24 | __all__ = ('RuleRegistry', 'registry', 'require', 'run', 'autodiscover') 25 | 26 | 27 | class RuleRegistry(dict): 28 | """ 29 | Dict subclass to manage rules. 30 | 31 | logger 32 | The standard logging logger instance to use. 33 | """ 34 | def __init__(self): 35 | self.logger = logging.getLogger('rules_light') 36 | 37 | def __setitem__(self, key, value): 38 | """ 39 | Adds a debug-level log on registration. 40 | """ 41 | super(RuleRegistry, self).__setitem__(key, value) 42 | self.logger.debug(u'[rules_light] "%s" registered with: %s' % ( 43 | key, self.rule_text_name(value))) 44 | 45 | def run(self, user, name, *args, **kwargs): 46 | """ 47 | Run a rule, return True if whatever it returns evaluates to True. 48 | 49 | Also logs calls with the info-level. 50 | """ 51 | if name not in self: 52 | self.logger.error(u'[rules_light] Rule does not exist "%s"' % name) 53 | raise DoesNotExist(name) 54 | 55 | rule = self[name] 56 | 57 | if hasattr(rule, '__call__'): 58 | result = self[name](user, name, *args, **kwargs) 59 | else: 60 | result = rule 61 | 62 | text = self.as_text(user, name, *args, **kwargs) 63 | if result: 64 | self.logger.info(u'[rules_light] %s passed' % text) 65 | return True 66 | else: 67 | self.logger.info(u'[rules_light] %s failed' % text) 68 | return False 69 | 70 | def require(self, user, name, *args, **kwargs): 71 | """ 72 | Run a rule, raise ``rules_light.Denied`` if returned False. 73 | 74 | Log denials with warn-level. 75 | """ 76 | result = self.run(user, name, *args, **kwargs) 77 | 78 | if not result: 79 | text = self.as_text(user, name, *args, **kwargs) 80 | self.logger.warn(u'[rules_light] Deny %s' % text) 81 | raise Denied(text) 82 | 83 | def as_text(self, user, name, *args, **kwargs): 84 | """ Format a rule to be human readable for logging """ 85 | if name not in self: 86 | raise DoesNotExist(name) 87 | 88 | formated_args = [] 89 | for arg in args: 90 | formated_args.append(u'"%s"' % smart_text(arg)) 91 | 92 | for key, value in kwargs.items(): 93 | formated_args.append(u'%s="%s"' % (smart_text(key), 94 | smart_text(value))) 95 | formated_args = u', '.join(formated_args) 96 | 97 | if hasattr(self[name], '__call__'): 98 | text_name = self.rule_text_name(self[name]) 99 | 100 | if formated_args: 101 | return u'%s(%s, "%s", %s)' % (text_name, user, name, 102 | formated_args) 103 | else: 104 | return u'%s(%s, "%s")' % (text_name, user, name) 105 | else: 106 | return u'%s is %s' % (name, self[name]) 107 | 108 | def rule_text_name(self, rule): 109 | if hasattr(rule, 'func_name'): 110 | return rule.func_name 111 | elif rule is True: 112 | return u'True' 113 | elif rule is False: 114 | return u'False' 115 | elif hasattr(rule, '__name__'): 116 | return rule.__name__ 117 | elif hasattr(rule, '__class__'): 118 | return rule.__class__.__name__ 119 | else: 120 | return smart_text(rule) 121 | 122 | 123 | registry = RuleRegistry() 124 | 125 | 126 | def run(user, name, *args, **kwargs): 127 | """ Proxy ``rules_light.registry.run()``. """ 128 | return registry.run(user, name, *args, **kwargs) 129 | 130 | 131 | def require(user, name, *args, **kwargs): 132 | """ Proxy ``rules_light.registry.require()``. """ 133 | registry.require(user, name, *args, **kwargs) 134 | 135 | 136 | def _autodiscover(registry): 137 | """See documentation for autodiscover (without the underscore)""" 138 | import copy 139 | from django.conf import settings 140 | from django.utils.importlib import import_module 141 | from django.utils.module_loading import module_has_submodule 142 | 143 | for app in settings.INSTALLED_APPS: 144 | mod = import_module(app) 145 | # Attempt to import the app's admin module. 146 | try: 147 | before_import_registry = copy.copy(registry) 148 | import_module('%s.rules_light_registry' % app) 149 | except Exception: 150 | # Reset the model registry to the state before the last import as 151 | # this import will have to reoccur on the next request and this 152 | # could raise NotRegistered and AlreadyRegistered exceptions 153 | # (see #8245). 154 | registry = before_import_registry 155 | 156 | # Decide whether to bubble up this error. If the app just 157 | # doesn't have an admin module, we can ignore the error 158 | # attempting to import it, otherwise we want it to bubble up. 159 | if module_has_submodule(mod, 'rules_light_registry'): 160 | raise 161 | 162 | 163 | def autodiscover(): 164 | """ 165 | Check all apps in INSTALLED_APPS for stuff related to rules_light. 166 | 167 | For each app, autodiscover imports ``app.rules_light_registry`` if 168 | available, resulting in execution of ``rules_light.registry[...] = ...`` 169 | statements in that module, filling registry. 170 | 171 | Consider a standard app called 'cities_light' with such a structure:: 172 | 173 | cities_light/ 174 | __init__.py 175 | models.py 176 | urls.py 177 | views.py 178 | rules_light_registry.py 179 | 180 | With such a rules_light_registry.py:: 181 | 182 | import rules_light 183 | 184 | rules_light.register('cities_light.city.read', True) 185 | rules_light.register('cities_light.city.update', 186 | lambda user, rulename, country: user.is_staff) 187 | 188 | When autodiscover() imports cities_light.rules_light_registry, both 189 | `'cities_light.city.read'` and `'cities_light.city.update'` will be 190 | registered. 191 | """ 192 | if autodiscover_modules: 193 | autodiscover_modules('rules_light_registry') 194 | else: 195 | _autodiscover(registry) 196 | -------------------------------------------------------------------------------- /rules_light/rules_light_registry.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import rules_light 4 | 5 | 6 | rules_light.registry['rules_light.rule.read'] = rules_light.is_staff 7 | -------------------------------------------------------------------------------- /rules_light/shortcuts.py: -------------------------------------------------------------------------------- 1 | """ 2 | It is trivial to take shortcuts because the rule registry is a simple dict. 3 | 4 | You can reuse your rules several times in standard python:: 5 | 6 | def my_model_or_is_staff(user, rule, model, obj=None): 7 | return user.is_staff or (obj and obj.author == user) 8 | 9 | rules_light.registry.setdefault('your_app.your_model.create', 10 | my_model_or_is_staff) 11 | rules_light.registry.setdefault('your_app.your_model.update', 12 | my_model_or_is_staff) 13 | rules_light.registry.setdefault('your_app.your_model.delete', 14 | my_model_or_is_staff) 15 | 16 | This module provides some shortcut(s). Shortcuts are also usable as decorators 17 | too (see ``make_decorator``):: 18 | 19 | @rules_light.is_authenticated 20 | def my_book(user, rule, book): 21 | return book.author == user 22 | 23 | rules_light.registry.setdefault('your_app.your_model.update', my_book) 24 | """ 25 | from __future__ import unicode_literals 26 | 27 | from .decorators import make_decorator 28 | 29 | __all__ = ['is_staff', 'is_authenticated'] 30 | 31 | 32 | @make_decorator 33 | def is_staff(user, rulename, *args, **kwargs): 34 | """ 35 | Return True if user.is_staff. 36 | 37 | For example, in ``your_app/rules_light_registry.py``:: 38 | 39 | rules_light.registry.setdefault('your_app.your_model.create', 40 | rules_light.is_staff) 41 | 42 | Is equivalent to:: 43 | 44 | rules_light.registry.setdefault('your_app.your_model.create', 45 | lambda: user, rulename, *args, **kwargs: user.is_staff) 46 | 47 | Also:: 48 | 49 | rules_light.registry.setdefault('your_app.your_model.create', 50 | rules_light.is_staff(your_rule_callback)) 51 | 52 | Is equivalent to:: 53 | 54 | def staff_and_stuff(user, rule, *args, **kwargs): 55 | if not rules_light.is_staff(user, rule, *args, **kwargs): 56 | return False 57 | 58 | if your_stuff(): 59 | return True 60 | 61 | rules_light.registry.setdefault('your_app.your_model.create', 62 | rules_light.is_staff(your_rule_callback)) 63 | """ 64 | return user and user.is_staff 65 | 66 | 67 | @make_decorator 68 | def is_authenticated(user, rulename, *args, **kwargs): 69 | """ 70 | Return user.is_authenticated(). 71 | """ 72 | try: 73 | return user and user.is_authenticated() 74 | except Exception: 75 | return user and user.is_authenticated 76 | -------------------------------------------------------------------------------- /rules_light/static/rules_light/github.css: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub theme 3 | * 4 | * @author Craig Campbell 5 | * @version 1.0.4 6 | */ 7 | pre { 8 | border: 1px solid #ccc; 9 | word-wrap: break-word; 10 | padding: 6px 10px; 11 | line-height: 19px; 12 | margin-bottom: 20px; 13 | } 14 | 15 | code { 16 | border: 1px solid #eaeaea; 17 | margin: 0px 2px; 18 | padding: 0px 5px; 19 | font-size: 12px; 20 | } 21 | 22 | pre code { 23 | border: 0px; 24 | padding: 0px; 25 | margin: 0px; 26 | -moz-border-radius: 0px; 27 | -webkit-border-radius: 0px; 28 | border-radius: 0px; 29 | } 30 | 31 | pre, code { 32 | font-family: Consolas, 'Liberation Mono', Courier, monospace; 33 | color: #333; 34 | background: #f8f8f8; 35 | -moz-border-radius: 3px; 36 | -webkit-border-radius: 3px; 37 | border-radius: 3px; 38 | } 39 | 40 | pre, pre code { 41 | font-size: 13px; 42 | } 43 | 44 | pre .comment { 45 | color: #998; 46 | } 47 | 48 | pre .support { 49 | color: #0086B3; 50 | } 51 | 52 | pre .tag, pre .tag-name { 53 | color: navy; 54 | } 55 | 56 | pre .keyword, pre .css-property, pre .vendor-prefix, pre .sass, pre .class, pre .id, pre .css-value, pre .entity.function, pre .storage.function { 57 | font-weight: bold; 58 | } 59 | 60 | pre .css-property, pre .css-value, pre .vendor-prefix, pre .support.namespace { 61 | color: #333; 62 | } 63 | 64 | pre .constant.numeric, pre .keyword.unit, pre .hex-color { 65 | font-weight: normal; 66 | color: #099; 67 | } 68 | 69 | pre .entity.class { 70 | color: #458; 71 | } 72 | 73 | pre .entity.id, pre .entity.function { 74 | color: #900; 75 | } 76 | 77 | pre .attribute, pre .variable { 78 | color: teal; 79 | } 80 | 81 | pre .string, pre .support.value { 82 | font-weight: normal; 83 | color: #d14; 84 | } 85 | 86 | pre .regexp { 87 | color: #009926; 88 | } 89 | -------------------------------------------------------------------------------- /rules_light/static/rules_light/rainbow-custom.min.js: -------------------------------------------------------------------------------- 1 | /* Rainbow v1.1.8 rainbowco.de | included languages: generic, python */ 2 | window.Rainbow=function(){function q(a){var b,c=a.getAttribute&&a.getAttribute("data-language")||0;if(!c){a=a.attributes;for(b=0;b=e[d][c])delete e[d][c],delete j[d][c];if(a>=c&&ac&&b'+b+""}function s(a,b,c,h){var f=a.exec(c);if(f){++t;!b.name&&"string"==typeof b.matches[0]&&(b.name=b.matches[0],delete b.matches[0]);var k=f[0],i=f.index,u=f[0].length+i,g=function(){function f(){s(a,b,c,h)}t%100>0?f():setTimeout(f,0)};if(C(i,u))g();else{var m=v(b.matches),l=function(a,c,h){if(a>=c.length)h(k);else{var d=f[c[a]];if(d){var e=b.matches[c[a]],i=e.language,g=e.name&&e.matches? 4 | e.matches:e,j=function(b,d,e){var i;i=0;var g;for(g=1;g/g,">").replace(/&(?![\w\#]+;)/g, 6 | "&"),b,c)}function o(a,b,c){if(b 7 | {% trans 'You do not have permission to do that.' %} 8 | 9 | {% if settings.LOGIN_URL %} 10 | {% trans 'Try logging in' %} {% if request.user.is_authenticated %}{% trans 'with other credentials' %}{% endif %} 11 | {% endif %} 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /rules_light/templates/rules_light/registry.html: -------------------------------------------------------------------------------- 1 | {% extends 'rules_light/base.html' %} 2 | 3 | {% load rules_light_tags %} 4 | 5 | {% block body %} 6 | 7 | 8 | 9 | 10 | {% for name, rule in registry.items %} 11 | 12 | 15 | 18 | 19 | {% endfor %} 20 |
13 |
{{ name }}
14 |
16 |
{{ registry|rule_code:name }}
17 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /rules_light/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/rules_light/templatetags/__init__.py -------------------------------------------------------------------------------- /rules_light/templatetags/rules_light_tags.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from django import template 4 | 5 | from classytags.core import Options 6 | from classytags.helpers import AsTag 7 | from classytags.arguments import (Argument, MultiKeywordArgument, 8 | MultiValueArgument) 9 | 10 | import rules_light 11 | 12 | register = template.Library() 13 | 14 | 15 | @register.filter 16 | def rule_code(registry, name): 17 | rule = registry[name] 18 | 19 | if hasattr(rule, '__call__'): 20 | return inspect.getsource(rule) 21 | 22 | return repr(rule) 23 | 24 | 25 | class Rule(AsTag): 26 | options = Options( 27 | Argument('rule_name'), 28 | MultiValueArgument('args', required=False), 29 | MultiKeywordArgument('kwargs', required=False), 30 | 'as', 31 | Argument('varname', resolve=False, required=False), 32 | ) 33 | 34 | def get_value(self, context, rule_name, args, kwargs): 35 | try: 36 | return rules_light.run(context.request.user, rule_name, 37 | *args, **kwargs) 38 | except AttributeError: 39 | rules_light.run(context['request'].user, rule_name, 40 | *args, **kwargs) 41 | 42 | 43 | register.tag('rule', Rule) 44 | -------------------------------------------------------------------------------- /rules_light/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | 4 | if django.VERSION < (1, 7): 5 | import rules_light 6 | rules_light.autodiscover() 7 | -------------------------------------------------------------------------------- /rules_light/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/rules_light/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /rules_light/tests/fixtures/class_decorator_classes.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.views import generic 4 | from django.contrib.auth.models import User 5 | 6 | import rules_light 7 | 8 | @rules_light.class_decorator 9 | class CreateView(generic.CreateView): 10 | model = User 11 | fields = [f.name for f in User._meta.fields] 12 | 13 | 14 | @rules_light.class_decorator 15 | class UpdateView(generic.UpdateView): 16 | model = User 17 | fields = [f.name for f in User._meta.fields] 18 | 19 | 20 | @rules_light.class_decorator 21 | class DetailView(generic.DetailView): 22 | model = User 23 | fields = [f.name for f in User._meta.fields] 24 | 25 | 26 | @rules_light.class_decorator 27 | class DeleteView(generic.DeleteView): 28 | model = User 29 | fields = [f.name for f in User._meta.fields] 30 | 31 | 32 | @rules_light.class_decorator('funny') 33 | class FunnyUpdateView(generic.UpdateView): 34 | model = User 35 | fields = [f.name for f in User._meta.fields] 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /rules_light/tests/test_autodiscover.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from django.test import TestCase 3 | 4 | import rules_light 5 | 6 | 7 | class AutodiscoverTestCase(TestCase): 8 | def test_autodiscover(self): 9 | self.assertTrue('rules_light.rule.read' in rules_light.registry.keys()) 10 | self.assertFalse('Foo bar' in rules_light.registry.keys()) 11 | -------------------------------------------------------------------------------- /rules_light/tests/test_class_decorator.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import unittest 3 | import pytest 4 | 5 | from django.views import generic 6 | from django.test.client import RequestFactory 7 | from django.contrib.auth.models import User 8 | 9 | import rules_light 10 | from rules_light.views import RegistryView 11 | 12 | from .fixtures.class_decorator_classes import * 13 | 14 | 15 | @pytest.mark.django_db 16 | class ClassDecoratorTestCase(unittest.TestCase): 17 | def setUp(self): 18 | self.request = RequestFactory().get('/') 19 | self.request.user, c = User.objects.get_or_create(username='foo') 20 | 21 | def test_create_view_decorator(self): 22 | rules_light.registry['auth.user.create'] = False 23 | view = CreateView.as_view() 24 | 25 | with self.assertRaises(rules_light.Denied) as cm: 26 | view(self.request) 27 | 28 | rules_light.registry['auth.user.create'] = True 29 | # it should not raise an exception 30 | view(self.request) 31 | 32 | def test_update_view_decorator(self): 33 | rules_light.registry['auth.user.update'] = False 34 | view = UpdateView.as_view() 35 | 36 | with self.assertRaises(rules_light.Denied) as cm: 37 | view(self.request, pk=1) 38 | 39 | rules_light.registry['auth.user.update'] = True 40 | # it should not raise an exception 41 | view(self.request, pk=1) 42 | 43 | def test_detail_view_decorator(self): 44 | rules_light.registry['auth.user.read'] = False 45 | view = DetailView.as_view() 46 | 47 | with self.assertRaises(rules_light.Denied) as cm: 48 | view(self.request, pk=1) 49 | 50 | rules_light.registry['auth.user.read'] = True 51 | # it should not raise an exception 52 | view(self.request, pk=1) 53 | 54 | def test_delete_view_decorator(self): 55 | rules_light.registry['auth.user.delete'] = False 56 | view = DeleteView.as_view() 57 | 58 | with self.assertRaises(rules_light.Denied) as cm: 59 | view(self.request, pk=1) 60 | 61 | rules_light.registry['auth.user.delete'] = True 62 | # it should not raise an exception 63 | view(self.request, pk=1) 64 | 65 | def test_funny_view_decorator(self): 66 | rules_light.registry['funny'] = False 67 | # ensure that it would not raise an exception if it tried 68 | # auth.user.read 69 | rules_light.registry['auth.user.read'] = True 70 | view = FunnyUpdateView.as_view() 71 | 72 | with self.assertRaises(rules_light.Denied) as cm: 73 | view(self.request, pk=1) 74 | 75 | rules_light.registry['funny'] = True 76 | # it should not raise an exception 77 | view(self.request, pk=1) 78 | 79 | def test_dispatch_decorator(self): 80 | rules_light.registry['foo'] = False 81 | 82 | @rules_light.class_decorator('foo') 83 | class MyView(generic.View): 84 | pass 85 | view = MyView.as_view() 86 | 87 | with self.assertRaises(rules_light.Denied) as cm: 88 | view(self.request) 89 | 90 | rules_light.registry['foo'] = True 91 | # it should not raise an exception 92 | view(self.request) 93 | 94 | def test_fail(self): 95 | with self.assertRaises(rules_light.RulesLightException) as cm: 96 | @rules_light.class_decorator 97 | class MyView(generic.View): 98 | pass 99 | -------------------------------------------------------------------------------- /rules_light/tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import unittest 3 | 4 | import rules_light 5 | 6 | @rules_light.make_decorator 7 | def return_true(*args): 8 | return True 9 | 10 | 11 | @rules_light.make_decorator 12 | def return_false(*args): 13 | return False 14 | 15 | 16 | class DecoratorsTestCase(unittest.TestCase): 17 | def test_decorator_return_true(self): 18 | rule = return_true(lambda x, y: True) 19 | self.assertTrue(rule('x', 'y')) 20 | 21 | rule = return_true(lambda x, y: False) 22 | self.assertFalse(rule('x', 'y')) 23 | 24 | def test_decorator_return_false(self): 25 | rule = return_false(lambda x, y: True) 26 | self.assertFalse(rule('x', 'y')) 27 | 28 | rule = return_false(lambda x, y: False) 29 | self.assertFalse(rule('x', 'y')) 30 | 31 | def test_decorator_name(self): 32 | @return_true 33 | def foo(*args): 34 | pass 35 | 36 | self.assertEqual(foo.__name__, 'foo') 37 | -------------------------------------------------------------------------------- /rules_light/tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import pytest 3 | import unittest 4 | 5 | from django.test.client import Client 6 | 7 | import rules_light 8 | 9 | 10 | @pytest.mark.django_db 11 | class MiddlewareTestCase(unittest.TestCase): 12 | def setUp(self): 13 | self.client = Client() 14 | 15 | def test_redirect(self): 16 | self.client.get('/') 17 | -------------------------------------------------------------------------------- /rules_light/tests/test_registry.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import logging 4 | import pytest 5 | import unittest 6 | from mock import Mock 7 | 8 | from django.contrib.auth.models import User 9 | 10 | import rules_light 11 | 12 | 13 | @pytest.mark.django_db 14 | class RegistryTestCase(unittest.TestCase): 15 | def setUp(self): 16 | self.registry = rules_light.RuleRegistry() 17 | self.registry.logger = Mock(spec_set=['debug', 'info', 'warn', 'error']) 18 | self.user, c = User.objects.get_or_create(username='test') 19 | 20 | def test_run_rule_no_args(self): 21 | mock = Mock(return_value=True, spec_set=['__call__']) 22 | self.registry['x.y.z'] = mock 23 | 24 | self.registry.logger.debug.assert_called_once_with( 25 | u'[rules_light] "x.y.z" registered with: Mock') 26 | 27 | result = self.registry.run(self.user, 'x.y.z') 28 | 29 | self.registry.logger.info.assert_called_once_with( 30 | u'[rules_light] Mock(test, "x.y.z") passed') 31 | 32 | self.assertEqual(result, True) 33 | mock.assert_called_once_with(self.user, 'x.y.z') 34 | 35 | def test_run_rule_with_args(self): 36 | mock = Mock(return_value=True, spec_set=['__call__']) 37 | self.registry['x.y.z'] = mock 38 | 39 | result = self.registry.run(self.user, 'x.y.z', 'foo', x='bar') 40 | 41 | self.registry.logger.info.assert_called_once_with( 42 | u'[rules_light] Mock(test, "x.y.z", "foo", x="bar") passed') 43 | 44 | self.assertEqual(result, True) 45 | mock.assert_called_once_with(self.user, 'x.y.z', 'foo', x='bar') 46 | 47 | def test_raises_Denied(self): 48 | mock = Mock(return_value=False, spec_set=['__call__']) 49 | self.registry['x.y.z'] = mock 50 | 51 | with self.assertRaises(rules_light.Denied) as cm: 52 | self.registry.require(self.user, 'x.y.z') 53 | 54 | self.registry.logger.warn.assert_called_once_with( 55 | u'[rules_light] Deny Mock(test, "x.y.z")') 56 | 57 | def test_return_False(self): 58 | mock = Mock(return_value=False, spec_set=['__call__']) 59 | self.registry['x.y.z'] = mock 60 | 61 | self.assertFalse(self.registry.run(self.user, 'x.y.z')) 62 | self.registry.logger.info.assert_called_once_with( 63 | u'[rules_light] Mock(test, "x.y.z") failed') 64 | 65 | def test_raises_RuleDoesNotExist(self): 66 | with self.assertRaises(rules_light.DoesNotExist) as cm: 67 | self.registry.run(self.user, 'x') 68 | 69 | self.registry.logger.error.assert_called_once_with( 70 | u'[rules_light] Rule does not exist "x"') 71 | -------------------------------------------------------------------------------- /rules_light/tests/test_shortcuts.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import unittest 3 | import pytest 4 | 5 | from django.contrib.auth.models import User 6 | 7 | import rules_light 8 | 9 | 10 | @pytest.mark.django_db 11 | class ShortcutsTestCase(unittest.TestCase): 12 | def setUp(self): 13 | self.user, c = User.objects.get_or_create(username='foo') 14 | self.admin, c = User.objects.get_or_create(username='bar', 15 | is_staff=True) 16 | 17 | def test_is_authenticated_decorator(self): 18 | return_true = rules_light.is_authenticated(lambda u, r: True) 19 | 20 | self.assertFalse(return_true(None, 'foo')) 21 | self.assertTrue(return_true(self.user, 'foo')) 22 | 23 | def test_is_authenticated_rule(self): 24 | self.assertFalse(rules_light.is_authenticated(None, 'foo')) 25 | self.assertTrue(rules_light.is_authenticated(self.user, 'foo')) 26 | 27 | def test_is_staff_decorator(self): 28 | return_true = rules_light.is_staff(lambda u, r: True) 29 | 30 | self.assertFalse(return_true(None, 'foo')) 31 | self.assertFalse(return_true(self.user, 'foo')) 32 | self.assertTrue(return_true(self.admin, 'foo')) 33 | 34 | def test_is_staff_rule(self): 35 | self.assertFalse(rules_light.is_staff(None, 'foo')) 36 | self.assertFalse(rules_light.is_staff(self.user, 'foo')) 37 | self.assertTrue(rules_light.is_staff(self.admin, 'foo')) 38 | -------------------------------------------------------------------------------- /rules_light/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import pytest 3 | import unittest 4 | 5 | from django.test.client import RequestFactory 6 | from django.contrib.auth.models import User, AnonymousUser 7 | 8 | import rules_light 9 | from rules_light.views import RegistryView 10 | 11 | 12 | @pytest.mark.django_db 13 | class ViewsTestCase(unittest.TestCase): 14 | def setUp(self): 15 | """ 16 | Note that URL doesn't matter here because the tests excute the views 17 | directly. 18 | """ 19 | User.objects.all().delete() 20 | 21 | self.anonymous_request = RequestFactory().get('/') 22 | self.anonymous_request.user = AnonymousUser() 23 | 24 | self.user_request = RequestFactory().get('/') 25 | self.user_request.user, c = User.objects.get_or_create( 26 | username='foo', is_staff=False) 27 | 28 | self.admin_request = RequestFactory().get('/') 29 | self.admin_request.user, c = User.objects.get_or_create( 30 | username='bar', is_staff=True) 31 | 32 | def test_registry_view(self): 33 | view = RegistryView.as_view() 34 | 35 | with self.assertRaises(rules_light.Denied) as cm: 36 | view(self.anonymous_request) 37 | 38 | with self.assertRaises(rules_light.Denied) as cm: 39 | view(self.user_request) 40 | 41 | # it should not raise an exception 42 | view(self.admin_request) 43 | -------------------------------------------------------------------------------- /rules_light/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.urls import re_path 4 | 5 | from .views import RegistryView 6 | 7 | 8 | urlpatterns = [ 9 | re_path(r'$', RegistryView.as_view(), name='rules_light_registry'), 10 | ] 11 | -------------------------------------------------------------------------------- /rules_light/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.views import generic 4 | 5 | import rules_light 6 | 7 | 8 | @rules_light.class_decorator('rules_light.rule.read') 9 | class RegistryView(generic.TemplateView): 10 | """ 11 | Expose the rule registry for debug purposes. 12 | 13 | Install it as such:: 14 | 15 | url(r'^rules/$', RegistryView.as_view(), name='rules_light_registry'), 16 | 17 | Or just:: 18 | 19 | url(r'^rules/', include('rules_light.urls')), 20 | 21 | Note: view requires ``'rules_light.rule.read'`` which is enabled for admins 22 | by default. 23 | """ 24 | 25 | template_name = 'rules_light/registry.html' 26 | 27 | def get_context_data(self): 28 | """ Add the registry to the context. """ 29 | return {'registry': rules_light.registry} 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pep8] 2 | ignore = E124,E128 3 | exclude = example_apps,tests,migrations 4 | 5 | [flake8] 6 | ignore = E124,E128 7 | exclude = example_apps,tests,migrations,__init__.py 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | from setuptools import setup, find_packages, Command 3 | 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | class RunTests(Command): 9 | description = "Run the django test suite from the testproj dir." 10 | 11 | user_options = [] 12 | 13 | def initialize_options(self): 14 | pass 15 | 16 | def finalize_options(self): 17 | pass 18 | 19 | def run(self): 20 | this_dir = os.getcwd() 21 | testproj_dir = os.path.join(this_dir, "test_project") 22 | os.chdir(testproj_dir) 23 | sys.path.append(testproj_dir) 24 | from django.core.management import execute_from_command_line 25 | os.environ["DJANGO_SETTINGS_MODULE"] = 'test_project.settings' 26 | settings_file = os.environ["DJANGO_SETTINGS_MODULE"] 27 | settings_mod = __import__(settings_file, {}, {}, ['']) 28 | execute_from_command_line( 29 | argv=[__file__, "test", "rules_light"]) 30 | os.chdir(this_dir) 31 | 32 | if 'sdist' in sys.argv: 33 | dir = os.getcwd() 34 | os.chdir(os.path.join(dir, 'rules_light')) 35 | os.system('django-admin.py compilemessages') 36 | os.chdir(dir) 37 | 38 | 39 | setup( 40 | name='django-rules-light', 41 | version='0.3.2', 42 | description='Rule registry for django', 43 | author='James Pic', 44 | author_email='jamespic@gmail.com', 45 | url='http://github.com/yourlabs/django-rules-light', 46 | packages=find_packages(), 47 | cmdclass={'test': RunTests}, 48 | include_package_data=True, 49 | zip_safe=False, 50 | long_description=read('README.rst'), 51 | license='MIT', 52 | keywords='django security rules acl rbac', 53 | install_requires=[ 54 | 'six', 55 | 'django-classy-tags', 56 | ], 57 | classifiers=[ 58 | 'Development Status :: 5 - Production/Stable', 59 | 'Intended Audience :: Developers', 60 | 'License :: OSI Approved :: MIT License', 61 | 'Operating System :: OS Independent', 62 | 'Programming Language :: Python', 63 | 'Programming Language :: Python :: 2', 64 | 'Programming Language :: Python :: 3', 65 | 'Topic :: Software Development :: Libraries :: Python Modules', 66 | ] 67 | ) 68 | -------------------------------------------------------------------------------- /test_project/auth_rules.py: -------------------------------------------------------------------------------- 1 | import rules_light 2 | 3 | rules_light.registry['auth.user.read'] = True 4 | rules_light.registry['auth.user.update'] = lambda user, *args: user.is_staff 5 | -------------------------------------------------------------------------------- /test_project/db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/test_project/db.sqlite -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /test_project/static/bootstrap/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.1.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | .clearfix { 12 | *zoom: 1; 13 | } 14 | 15 | .clearfix:before, 16 | .clearfix:after { 17 | display: table; 18 | line-height: 0; 19 | content: ""; 20 | } 21 | 22 | .clearfix:after { 23 | clear: both; 24 | } 25 | 26 | .hide-text { 27 | font: 0/0 a; 28 | color: transparent; 29 | text-shadow: none; 30 | background-color: transparent; 31 | border: 0; 32 | } 33 | 34 | .input-block-level { 35 | display: block; 36 | width: 100%; 37 | min-height: 30px; 38 | -webkit-box-sizing: border-box; 39 | -moz-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | visibility: hidden; 46 | } 47 | 48 | .visible-phone { 49 | display: none !important; 50 | } 51 | 52 | .visible-tablet { 53 | display: none !important; 54 | } 55 | 56 | .hidden-desktop { 57 | display: none !important; 58 | } 59 | 60 | .visible-desktop { 61 | display: inherit !important; 62 | } 63 | 64 | @media (min-width: 768px) and (max-width: 979px) { 65 | .hidden-desktop { 66 | display: inherit !important; 67 | } 68 | .visible-desktop { 69 | display: none !important ; 70 | } 71 | .visible-tablet { 72 | display: inherit !important; 73 | } 74 | .hidden-tablet { 75 | display: none !important; 76 | } 77 | } 78 | 79 | @media (max-width: 767px) { 80 | .hidden-desktop { 81 | display: inherit !important; 82 | } 83 | .visible-desktop { 84 | display: none !important; 85 | } 86 | .visible-phone { 87 | display: inherit !important; 88 | } 89 | .hidden-phone { 90 | display: none !important; 91 | } 92 | } 93 | 94 | @media (min-width: 1200px) { 95 | .row { 96 | margin-left: -30px; 97 | *zoom: 1; 98 | } 99 | .row:before, 100 | .row:after { 101 | display: table; 102 | line-height: 0; 103 | content: ""; 104 | } 105 | .row:after { 106 | clear: both; 107 | } 108 | [class*="span"] { 109 | float: left; 110 | min-height: 1px; 111 | margin-left: 30px; 112 | } 113 | .container, 114 | .navbar-static-top .container, 115 | .navbar-fixed-top .container, 116 | .navbar-fixed-bottom .container { 117 | width: 1170px; 118 | } 119 | .span12 { 120 | width: 1170px; 121 | } 122 | .span11 { 123 | width: 1070px; 124 | } 125 | .span10 { 126 | width: 970px; 127 | } 128 | .span9 { 129 | width: 870px; 130 | } 131 | .span8 { 132 | width: 770px; 133 | } 134 | .span7 { 135 | width: 670px; 136 | } 137 | .span6 { 138 | width: 570px; 139 | } 140 | .span5 { 141 | width: 470px; 142 | } 143 | .span4 { 144 | width: 370px; 145 | } 146 | .span3 { 147 | width: 270px; 148 | } 149 | .span2 { 150 | width: 170px; 151 | } 152 | .span1 { 153 | width: 70px; 154 | } 155 | .offset12 { 156 | margin-left: 1230px; 157 | } 158 | .offset11 { 159 | margin-left: 1130px; 160 | } 161 | .offset10 { 162 | margin-left: 1030px; 163 | } 164 | .offset9 { 165 | margin-left: 930px; 166 | } 167 | .offset8 { 168 | margin-left: 830px; 169 | } 170 | .offset7 { 171 | margin-left: 730px; 172 | } 173 | .offset6 { 174 | margin-left: 630px; 175 | } 176 | .offset5 { 177 | margin-left: 530px; 178 | } 179 | .offset4 { 180 | margin-left: 430px; 181 | } 182 | .offset3 { 183 | margin-left: 330px; 184 | } 185 | .offset2 { 186 | margin-left: 230px; 187 | } 188 | .offset1 { 189 | margin-left: 130px; 190 | } 191 | .row-fluid { 192 | width: 100%; 193 | *zoom: 1; 194 | } 195 | .row-fluid:before, 196 | .row-fluid:after { 197 | display: table; 198 | line-height: 0; 199 | content: ""; 200 | } 201 | .row-fluid:after { 202 | clear: both; 203 | } 204 | .row-fluid [class*="span"] { 205 | display: block; 206 | float: left; 207 | width: 100%; 208 | min-height: 30px; 209 | margin-left: 2.564102564102564%; 210 | *margin-left: 2.5109110747408616%; 211 | -webkit-box-sizing: border-box; 212 | -moz-box-sizing: border-box; 213 | box-sizing: border-box; 214 | } 215 | .row-fluid [class*="span"]:first-child { 216 | margin-left: 0; 217 | } 218 | .row-fluid .span12 { 219 | width: 100%; 220 | *width: 99.94680851063829%; 221 | } 222 | .row-fluid .span11 { 223 | width: 91.45299145299145%; 224 | *width: 91.39979996362975%; 225 | } 226 | .row-fluid .span10 { 227 | width: 82.90598290598291%; 228 | *width: 82.8527914166212%; 229 | } 230 | .row-fluid .span9 { 231 | width: 74.35897435897436%; 232 | *width: 74.30578286961266%; 233 | } 234 | .row-fluid .span8 { 235 | width: 65.81196581196582%; 236 | *width: 65.75877432260411%; 237 | } 238 | .row-fluid .span7 { 239 | width: 57.26495726495726%; 240 | *width: 57.21176577559556%; 241 | } 242 | .row-fluid .span6 { 243 | width: 48.717948717948715%; 244 | *width: 48.664757228587014%; 245 | } 246 | .row-fluid .span5 { 247 | width: 40.17094017094017%; 248 | *width: 40.11774868157847%; 249 | } 250 | .row-fluid .span4 { 251 | width: 31.623931623931625%; 252 | *width: 31.570740134569924%; 253 | } 254 | .row-fluid .span3 { 255 | width: 23.076923076923077%; 256 | *width: 23.023731587561375%; 257 | } 258 | .row-fluid .span2 { 259 | width: 14.52991452991453%; 260 | *width: 14.476723040552828%; 261 | } 262 | .row-fluid .span1 { 263 | width: 5.982905982905983%; 264 | *width: 5.929714493544281%; 265 | } 266 | .row-fluid .offset12 { 267 | margin-left: 105.12820512820512%; 268 | *margin-left: 105.02182214948171%; 269 | } 270 | .row-fluid .offset12:first-child { 271 | margin-left: 102.56410256410257%; 272 | *margin-left: 102.45771958537915%; 273 | } 274 | .row-fluid .offset11 { 275 | margin-left: 96.58119658119658%; 276 | *margin-left: 96.47481360247316%; 277 | } 278 | .row-fluid .offset11:first-child { 279 | margin-left: 94.01709401709402%; 280 | *margin-left: 93.91071103837061%; 281 | } 282 | .row-fluid .offset10 { 283 | margin-left: 88.03418803418803%; 284 | *margin-left: 87.92780505546462%; 285 | } 286 | .row-fluid .offset10:first-child { 287 | margin-left: 85.47008547008548%; 288 | *margin-left: 85.36370249136206%; 289 | } 290 | .row-fluid .offset9 { 291 | margin-left: 79.48717948717949%; 292 | *margin-left: 79.38079650845607%; 293 | } 294 | .row-fluid .offset9:first-child { 295 | margin-left: 76.92307692307693%; 296 | *margin-left: 76.81669394435352%; 297 | } 298 | .row-fluid .offset8 { 299 | margin-left: 70.94017094017094%; 300 | *margin-left: 70.83378796144753%; 301 | } 302 | .row-fluid .offset8:first-child { 303 | margin-left: 68.37606837606839%; 304 | *margin-left: 68.26968539734497%; 305 | } 306 | .row-fluid .offset7 { 307 | margin-left: 62.393162393162385%; 308 | *margin-left: 62.28677941443899%; 309 | } 310 | .row-fluid .offset7:first-child { 311 | margin-left: 59.82905982905982%; 312 | *margin-left: 59.72267685033642%; 313 | } 314 | .row-fluid .offset6 { 315 | margin-left: 53.84615384615384%; 316 | *margin-left: 53.739770867430444%; 317 | } 318 | .row-fluid .offset6:first-child { 319 | margin-left: 51.28205128205128%; 320 | *margin-left: 51.175668303327875%; 321 | } 322 | .row-fluid .offset5 { 323 | margin-left: 45.299145299145295%; 324 | *margin-left: 45.1927623204219%; 325 | } 326 | .row-fluid .offset5:first-child { 327 | margin-left: 42.73504273504273%; 328 | *margin-left: 42.62865975631933%; 329 | } 330 | .row-fluid .offset4 { 331 | margin-left: 36.75213675213675%; 332 | *margin-left: 36.645753773413354%; 333 | } 334 | .row-fluid .offset4:first-child { 335 | margin-left: 34.18803418803419%; 336 | *margin-left: 34.081651209310785%; 337 | } 338 | .row-fluid .offset3 { 339 | margin-left: 28.205128205128204%; 340 | *margin-left: 28.0987452264048%; 341 | } 342 | .row-fluid .offset3:first-child { 343 | margin-left: 25.641025641025642%; 344 | *margin-left: 25.53464266230224%; 345 | } 346 | .row-fluid .offset2 { 347 | margin-left: 19.65811965811966%; 348 | *margin-left: 19.551736679396257%; 349 | } 350 | .row-fluid .offset2:first-child { 351 | margin-left: 17.094017094017094%; 352 | *margin-left: 16.98763411529369%; 353 | } 354 | .row-fluid .offset1 { 355 | margin-left: 11.11111111111111%; 356 | *margin-left: 11.004728132387708%; 357 | } 358 | .row-fluid .offset1:first-child { 359 | margin-left: 8.547008547008547%; 360 | *margin-left: 8.440625568285142%; 361 | } 362 | input, 363 | textarea, 364 | .uneditable-input { 365 | margin-left: 0; 366 | } 367 | .controls-row [class*="span"] + [class*="span"] { 368 | margin-left: 30px; 369 | } 370 | input.span12, 371 | textarea.span12, 372 | .uneditable-input.span12 { 373 | width: 1156px; 374 | } 375 | input.span11, 376 | textarea.span11, 377 | .uneditable-input.span11 { 378 | width: 1056px; 379 | } 380 | input.span10, 381 | textarea.span10, 382 | .uneditable-input.span10 { 383 | width: 956px; 384 | } 385 | input.span9, 386 | textarea.span9, 387 | .uneditable-input.span9 { 388 | width: 856px; 389 | } 390 | input.span8, 391 | textarea.span8, 392 | .uneditable-input.span8 { 393 | width: 756px; 394 | } 395 | input.span7, 396 | textarea.span7, 397 | .uneditable-input.span7 { 398 | width: 656px; 399 | } 400 | input.span6, 401 | textarea.span6, 402 | .uneditable-input.span6 { 403 | width: 556px; 404 | } 405 | input.span5, 406 | textarea.span5, 407 | .uneditable-input.span5 { 408 | width: 456px; 409 | } 410 | input.span4, 411 | textarea.span4, 412 | .uneditable-input.span4 { 413 | width: 356px; 414 | } 415 | input.span3, 416 | textarea.span3, 417 | .uneditable-input.span3 { 418 | width: 256px; 419 | } 420 | input.span2, 421 | textarea.span2, 422 | .uneditable-input.span2 { 423 | width: 156px; 424 | } 425 | input.span1, 426 | textarea.span1, 427 | .uneditable-input.span1 { 428 | width: 56px; 429 | } 430 | .thumbnails { 431 | margin-left: -30px; 432 | } 433 | .thumbnails > li { 434 | margin-left: 30px; 435 | } 436 | .row-fluid .thumbnails { 437 | margin-left: 0; 438 | } 439 | } 440 | 441 | @media (min-width: 768px) and (max-width: 979px) { 442 | .row { 443 | margin-left: -20px; 444 | *zoom: 1; 445 | } 446 | .row:before, 447 | .row:after { 448 | display: table; 449 | line-height: 0; 450 | content: ""; 451 | } 452 | .row:after { 453 | clear: both; 454 | } 455 | [class*="span"] { 456 | float: left; 457 | min-height: 1px; 458 | margin-left: 20px; 459 | } 460 | .container, 461 | .navbar-static-top .container, 462 | .navbar-fixed-top .container, 463 | .navbar-fixed-bottom .container { 464 | width: 724px; 465 | } 466 | .span12 { 467 | width: 724px; 468 | } 469 | .span11 { 470 | width: 662px; 471 | } 472 | .span10 { 473 | width: 600px; 474 | } 475 | .span9 { 476 | width: 538px; 477 | } 478 | .span8 { 479 | width: 476px; 480 | } 481 | .span7 { 482 | width: 414px; 483 | } 484 | .span6 { 485 | width: 352px; 486 | } 487 | .span5 { 488 | width: 290px; 489 | } 490 | .span4 { 491 | width: 228px; 492 | } 493 | .span3 { 494 | width: 166px; 495 | } 496 | .span2 { 497 | width: 104px; 498 | } 499 | .span1 { 500 | width: 42px; 501 | } 502 | .offset12 { 503 | margin-left: 764px; 504 | } 505 | .offset11 { 506 | margin-left: 702px; 507 | } 508 | .offset10 { 509 | margin-left: 640px; 510 | } 511 | .offset9 { 512 | margin-left: 578px; 513 | } 514 | .offset8 { 515 | margin-left: 516px; 516 | } 517 | .offset7 { 518 | margin-left: 454px; 519 | } 520 | .offset6 { 521 | margin-left: 392px; 522 | } 523 | .offset5 { 524 | margin-left: 330px; 525 | } 526 | .offset4 { 527 | margin-left: 268px; 528 | } 529 | .offset3 { 530 | margin-left: 206px; 531 | } 532 | .offset2 { 533 | margin-left: 144px; 534 | } 535 | .offset1 { 536 | margin-left: 82px; 537 | } 538 | .row-fluid { 539 | width: 100%; 540 | *zoom: 1; 541 | } 542 | .row-fluid:before, 543 | .row-fluid:after { 544 | display: table; 545 | line-height: 0; 546 | content: ""; 547 | } 548 | .row-fluid:after { 549 | clear: both; 550 | } 551 | .row-fluid [class*="span"] { 552 | display: block; 553 | float: left; 554 | width: 100%; 555 | min-height: 30px; 556 | margin-left: 2.7624309392265194%; 557 | *margin-left: 2.709239449864817%; 558 | -webkit-box-sizing: border-box; 559 | -moz-box-sizing: border-box; 560 | box-sizing: border-box; 561 | } 562 | .row-fluid [class*="span"]:first-child { 563 | margin-left: 0; 564 | } 565 | .row-fluid .span12 { 566 | width: 100%; 567 | *width: 99.94680851063829%; 568 | } 569 | .row-fluid .span11 { 570 | width: 91.43646408839778%; 571 | *width: 91.38327259903608%; 572 | } 573 | .row-fluid .span10 { 574 | width: 82.87292817679558%; 575 | *width: 82.81973668743387%; 576 | } 577 | .row-fluid .span9 { 578 | width: 74.30939226519337%; 579 | *width: 74.25620077583166%; 580 | } 581 | .row-fluid .span8 { 582 | width: 65.74585635359117%; 583 | *width: 65.69266486422946%; 584 | } 585 | .row-fluid .span7 { 586 | width: 57.18232044198895%; 587 | *width: 57.12912895262725%; 588 | } 589 | .row-fluid .span6 { 590 | width: 48.61878453038674%; 591 | *width: 48.56559304102504%; 592 | } 593 | .row-fluid .span5 { 594 | width: 40.05524861878453%; 595 | *width: 40.00205712942283%; 596 | } 597 | .row-fluid .span4 { 598 | width: 31.491712707182323%; 599 | *width: 31.43852121782062%; 600 | } 601 | .row-fluid .span3 { 602 | width: 22.92817679558011%; 603 | *width: 22.87498530621841%; 604 | } 605 | .row-fluid .span2 { 606 | width: 14.3646408839779%; 607 | *width: 14.311449394616199%; 608 | } 609 | .row-fluid .span1 { 610 | width: 5.801104972375691%; 611 | *width: 5.747913483013988%; 612 | } 613 | .row-fluid .offset12 { 614 | margin-left: 105.52486187845304%; 615 | *margin-left: 105.41847889972962%; 616 | } 617 | .row-fluid .offset12:first-child { 618 | margin-left: 102.76243093922652%; 619 | *margin-left: 102.6560479605031%; 620 | } 621 | .row-fluid .offset11 { 622 | margin-left: 96.96132596685082%; 623 | *margin-left: 96.8549429881274%; 624 | } 625 | .row-fluid .offset11:first-child { 626 | margin-left: 94.1988950276243%; 627 | *margin-left: 94.09251204890089%; 628 | } 629 | .row-fluid .offset10 { 630 | margin-left: 88.39779005524862%; 631 | *margin-left: 88.2914070765252%; 632 | } 633 | .row-fluid .offset10:first-child { 634 | margin-left: 85.6353591160221%; 635 | *margin-left: 85.52897613729868%; 636 | } 637 | .row-fluid .offset9 { 638 | margin-left: 79.8342541436464%; 639 | *margin-left: 79.72787116492299%; 640 | } 641 | .row-fluid .offset9:first-child { 642 | margin-left: 77.07182320441989%; 643 | *margin-left: 76.96544022569647%; 644 | } 645 | .row-fluid .offset8 { 646 | margin-left: 71.2707182320442%; 647 | *margin-left: 71.16433525332079%; 648 | } 649 | .row-fluid .offset8:first-child { 650 | margin-left: 68.50828729281768%; 651 | *margin-left: 68.40190431409427%; 652 | } 653 | .row-fluid .offset7 { 654 | margin-left: 62.70718232044199%; 655 | *margin-left: 62.600799341718584%; 656 | } 657 | .row-fluid .offset7:first-child { 658 | margin-left: 59.94475138121547%; 659 | *margin-left: 59.838368402492065%; 660 | } 661 | .row-fluid .offset6 { 662 | margin-left: 54.14364640883978%; 663 | *margin-left: 54.037263430116376%; 664 | } 665 | .row-fluid .offset6:first-child { 666 | margin-left: 51.38121546961326%; 667 | *margin-left: 51.27483249088986%; 668 | } 669 | .row-fluid .offset5 { 670 | margin-left: 45.58011049723757%; 671 | *margin-left: 45.47372751851417%; 672 | } 673 | .row-fluid .offset5:first-child { 674 | margin-left: 42.81767955801105%; 675 | *margin-left: 42.71129657928765%; 676 | } 677 | .row-fluid .offset4 { 678 | margin-left: 37.01657458563536%; 679 | *margin-left: 36.91019160691196%; 680 | } 681 | .row-fluid .offset4:first-child { 682 | margin-left: 34.25414364640884%; 683 | *margin-left: 34.14776066768544%; 684 | } 685 | .row-fluid .offset3 { 686 | margin-left: 28.45303867403315%; 687 | *margin-left: 28.346655695309746%; 688 | } 689 | .row-fluid .offset3:first-child { 690 | margin-left: 25.69060773480663%; 691 | *margin-left: 25.584224756083227%; 692 | } 693 | .row-fluid .offset2 { 694 | margin-left: 19.88950276243094%; 695 | *margin-left: 19.783119783707537%; 696 | } 697 | .row-fluid .offset2:first-child { 698 | margin-left: 17.12707182320442%; 699 | *margin-left: 17.02068884448102%; 700 | } 701 | .row-fluid .offset1 { 702 | margin-left: 11.32596685082873%; 703 | *margin-left: 11.219583872105325%; 704 | } 705 | .row-fluid .offset1:first-child { 706 | margin-left: 8.56353591160221%; 707 | *margin-left: 8.457152932878806%; 708 | } 709 | input, 710 | textarea, 711 | .uneditable-input { 712 | margin-left: 0; 713 | } 714 | .controls-row [class*="span"] + [class*="span"] { 715 | margin-left: 20px; 716 | } 717 | input.span12, 718 | textarea.span12, 719 | .uneditable-input.span12 { 720 | width: 710px; 721 | } 722 | input.span11, 723 | textarea.span11, 724 | .uneditable-input.span11 { 725 | width: 648px; 726 | } 727 | input.span10, 728 | textarea.span10, 729 | .uneditable-input.span10 { 730 | width: 586px; 731 | } 732 | input.span9, 733 | textarea.span9, 734 | .uneditable-input.span9 { 735 | width: 524px; 736 | } 737 | input.span8, 738 | textarea.span8, 739 | .uneditable-input.span8 { 740 | width: 462px; 741 | } 742 | input.span7, 743 | textarea.span7, 744 | .uneditable-input.span7 { 745 | width: 400px; 746 | } 747 | input.span6, 748 | textarea.span6, 749 | .uneditable-input.span6 { 750 | width: 338px; 751 | } 752 | input.span5, 753 | textarea.span5, 754 | .uneditable-input.span5 { 755 | width: 276px; 756 | } 757 | input.span4, 758 | textarea.span4, 759 | .uneditable-input.span4 { 760 | width: 214px; 761 | } 762 | input.span3, 763 | textarea.span3, 764 | .uneditable-input.span3 { 765 | width: 152px; 766 | } 767 | input.span2, 768 | textarea.span2, 769 | .uneditable-input.span2 { 770 | width: 90px; 771 | } 772 | input.span1, 773 | textarea.span1, 774 | .uneditable-input.span1 { 775 | width: 28px; 776 | } 777 | } 778 | 779 | @media (max-width: 767px) { 780 | body { 781 | padding-right: 20px; 782 | padding-left: 20px; 783 | } 784 | .navbar-fixed-top, 785 | .navbar-fixed-bottom, 786 | .navbar-static-top { 787 | margin-right: -20px; 788 | margin-left: -20px; 789 | } 790 | .container-fluid { 791 | padding: 0; 792 | } 793 | .dl-horizontal dt { 794 | float: none; 795 | width: auto; 796 | clear: none; 797 | text-align: left; 798 | } 799 | .dl-horizontal dd { 800 | margin-left: 0; 801 | } 802 | .container { 803 | width: auto; 804 | } 805 | .row-fluid { 806 | width: 100%; 807 | } 808 | .row, 809 | .thumbnails { 810 | margin-left: 0; 811 | } 812 | .thumbnails > li { 813 | float: none; 814 | margin-left: 0; 815 | } 816 | [class*="span"], 817 | .row-fluid [class*="span"] { 818 | display: block; 819 | float: none; 820 | width: 100%; 821 | margin-left: 0; 822 | -webkit-box-sizing: border-box; 823 | -moz-box-sizing: border-box; 824 | box-sizing: border-box; 825 | } 826 | .span12, 827 | .row-fluid .span12 { 828 | width: 100%; 829 | -webkit-box-sizing: border-box; 830 | -moz-box-sizing: border-box; 831 | box-sizing: border-box; 832 | } 833 | .input-large, 834 | .input-xlarge, 835 | .input-xxlarge, 836 | input[class*="span"], 837 | select[class*="span"], 838 | textarea[class*="span"], 839 | .uneditable-input { 840 | display: block; 841 | width: 100%; 842 | min-height: 30px; 843 | -webkit-box-sizing: border-box; 844 | -moz-box-sizing: border-box; 845 | box-sizing: border-box; 846 | } 847 | .input-prepend input, 848 | .input-append input, 849 | .input-prepend input[class*="span"], 850 | .input-append input[class*="span"] { 851 | display: inline-block; 852 | width: auto; 853 | } 854 | .controls-row [class*="span"] + [class*="span"] { 855 | margin-left: 0; 856 | } 857 | .modal { 858 | position: fixed; 859 | top: 20px; 860 | right: 20px; 861 | left: 20px; 862 | width: auto; 863 | margin: 0; 864 | } 865 | .modal.fade.in { 866 | top: auto; 867 | } 868 | } 869 | 870 | @media (max-width: 480px) { 871 | .nav-collapse { 872 | -webkit-transform: translate3d(0, 0, 0); 873 | } 874 | .page-header h1 small { 875 | display: block; 876 | line-height: 20px; 877 | } 878 | input[type="checkbox"], 879 | input[type="radio"] { 880 | border: 1px solid #ccc; 881 | } 882 | .form-horizontal .control-label { 883 | float: none; 884 | width: auto; 885 | padding-top: 0; 886 | text-align: left; 887 | } 888 | .form-horizontal .controls { 889 | margin-left: 0; 890 | } 891 | .form-horizontal .control-list { 892 | padding-top: 0; 893 | } 894 | .form-horizontal .form-actions { 895 | padding-right: 10px; 896 | padding-left: 10px; 897 | } 898 | .modal { 899 | top: 10px; 900 | right: 10px; 901 | left: 10px; 902 | } 903 | .modal-header .close { 904 | padding: 10px; 905 | margin: -10px; 906 | } 907 | .carousel-caption { 908 | position: static; 909 | } 910 | } 911 | 912 | @media (max-width: 979px) { 913 | body { 914 | padding-top: 0; 915 | } 916 | .navbar-fixed-top, 917 | .navbar-fixed-bottom { 918 | position: static; 919 | } 920 | .navbar-fixed-top { 921 | margin-bottom: 20px; 922 | } 923 | .navbar-fixed-bottom { 924 | margin-top: 20px; 925 | } 926 | .navbar-fixed-top .navbar-inner, 927 | .navbar-fixed-bottom .navbar-inner { 928 | padding: 5px; 929 | } 930 | .navbar .container { 931 | width: auto; 932 | padding: 0; 933 | } 934 | .navbar .brand { 935 | padding-right: 10px; 936 | padding-left: 10px; 937 | margin: 0 0 0 -5px; 938 | } 939 | .nav-collapse { 940 | clear: both; 941 | } 942 | .nav-collapse .nav { 943 | float: none; 944 | margin: 0 0 10px; 945 | } 946 | .nav-collapse .nav > li { 947 | float: none; 948 | } 949 | .nav-collapse .nav > li > a { 950 | margin-bottom: 2px; 951 | } 952 | .nav-collapse .nav > .divider-vertical { 953 | display: none; 954 | } 955 | .nav-collapse .nav .nav-header { 956 | color: #777777; 957 | text-shadow: none; 958 | } 959 | .nav-collapse .nav > li > a, 960 | .nav-collapse .dropdown-menu a { 961 | padding: 9px 15px; 962 | font-weight: bold; 963 | color: #777777; 964 | -webkit-border-radius: 3px; 965 | -moz-border-radius: 3px; 966 | border-radius: 3px; 967 | } 968 | .nav-collapse .btn { 969 | padding: 4px 10px 4px; 970 | font-weight: normal; 971 | -webkit-border-radius: 4px; 972 | -moz-border-radius: 4px; 973 | border-radius: 4px; 974 | } 975 | .nav-collapse .dropdown-menu li + li a { 976 | margin-bottom: 2px; 977 | } 978 | .nav-collapse .nav > li > a:hover, 979 | .nav-collapse .dropdown-menu a:hover { 980 | background-color: #f2f2f2; 981 | } 982 | .navbar-inverse .nav-collapse .nav > li > a:hover, 983 | .navbar-inverse .nav-collapse .dropdown-menu a:hover { 984 | background-color: #111111; 985 | } 986 | .nav-collapse.in .btn-group { 987 | padding: 0; 988 | margin-top: 5px; 989 | } 990 | .nav-collapse .dropdown-menu { 991 | position: static; 992 | top: auto; 993 | left: auto; 994 | display: block; 995 | float: none; 996 | max-width: none; 997 | padding: 0; 998 | margin: 0 15px; 999 | background-color: transparent; 1000 | border: none; 1001 | -webkit-border-radius: 0; 1002 | -moz-border-radius: 0; 1003 | border-radius: 0; 1004 | -webkit-box-shadow: none; 1005 | -moz-box-shadow: none; 1006 | box-shadow: none; 1007 | } 1008 | .nav-collapse .dropdown-menu:before, 1009 | .nav-collapse .dropdown-menu:after { 1010 | display: none; 1011 | } 1012 | .nav-collapse .dropdown-menu .divider { 1013 | display: none; 1014 | } 1015 | .nav-collapse .nav > li > .dropdown-menu:before, 1016 | .nav-collapse .nav > li > .dropdown-menu:after { 1017 | display: none; 1018 | } 1019 | .nav-collapse .navbar-form, 1020 | .nav-collapse .navbar-search { 1021 | float: none; 1022 | padding: 10px 15px; 1023 | margin: 10px 0; 1024 | border-top: 1px solid #f2f2f2; 1025 | border-bottom: 1px solid #f2f2f2; 1026 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1027 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1028 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1029 | } 1030 | .navbar-inverse .nav-collapse .navbar-form, 1031 | .navbar-inverse .nav-collapse .navbar-search { 1032 | border-top-color: #111111; 1033 | border-bottom-color: #111111; 1034 | } 1035 | .navbar .nav-collapse .nav.pull-right { 1036 | float: none; 1037 | margin-left: 0; 1038 | } 1039 | .nav-collapse, 1040 | .nav-collapse.collapse { 1041 | height: 0; 1042 | overflow: hidden; 1043 | } 1044 | .navbar .btn-navbar { 1045 | display: block; 1046 | } 1047 | .navbar-static .navbar-inner { 1048 | padding-right: 10px; 1049 | padding-left: 10px; 1050 | } 1051 | } 1052 | 1053 | @media (min-width: 980px) { 1054 | .nav-collapse.collapse { 1055 | height: auto !important; 1056 | overflow: visible !important; 1057 | } 1058 | } 1059 | -------------------------------------------------------------------------------- /test_project/static/bootstrap/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.1.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade.in{top:auto}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:block;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /test_project/static/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/test_project/static/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /test_project/static/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/test_project/static/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /test_project/static/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){e(function(){"use strict";e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()},e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e(function(){e("body").on("click.alert.data-api",t,n.prototype.close)})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")},e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e(function(){e("body").on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=n,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},to:function(t){var n=this.$element.find(".item.active"),r=n.parent().children(),i=r.index(n),s=this;if(t>r.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){s.to(t)}):i==t?this.pause().cycle():this.slide(t>i?"next":"prev",e(r[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f=e.Event("slide",{relatedTarget:i[0]});this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u]();if(i.hasClass("active"))return;if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}},e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e(function(){e("body").on("click.carousel.data-api","[data-slide]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=!i.data("modal")&&e.extend({},i.data(),n.data());i.carousel(s),t.preventDefault()})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning)return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning)return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=typeof n=="object"&&n;i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e(function(){e("body").on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})})}(window.jQuery),!function(e){"use strict";function r(){i(e(t)).removeClass("open")}function i(t){var n=t.attr("data-target"),r;return n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=e(n),r.length||(r=t.parent()),r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||(s.toggleClass("open"),n.focus()),!1},keydown:function(t){var n,r,s,o,u,a;if(!/(38|40|27)/.test(t.keyCode))return;n=e(this),t.preventDefault(),t.stopPropagation();if(n.is(".disabled, :disabled"))return;o=i(n),u=o.hasClass("open");if(!u||u&&t.keyCode==27)return n.click();r=e("[role=menu] li:not(.divider) a",o);if(!r.length)return;a=r.index(r.filter(":focus")),t.keyCode==38&&a>0&&a--,t.keyCode==40&&a').appendTo(document.body),this.options.backdrop!="static"&&this.$backdrop.click(e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,e.proxy(this.removeBackdrop,this)):this.removeBackdrop()):t&&t()}},e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e(function(){e("body").on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,this.options.trigger=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):this.options.trigger!="manual"&&(i=this.options.trigger=="hover"?"mouseenter":"focus",s=this.options.trigger=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this))),this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,t,this.$element.data()),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);if(!n.options.delay||!n.options.delay.show)return n.show();clearTimeout(this.timeout),n.hoverState="in",this.timeout=setTimeout(function(){n.hoverState=="in"&&n.show()},n.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var e,t,n,r,i,s,o;if(this.hasContent()&&this.enabled){e=this.tip(),this.setContent(),this.options.animation&&e.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,e[0],this.$element[0]):this.options.placement,t=/in/.test(s),e.remove().css({top:0,left:0,display:"block"}).appendTo(t?this.$element:document.body),n=this.getPosition(t),r=e[0].offsetWidth,i=e[0].offsetHeight;switch(t?s.split(" ")[1]:s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}e.css(o).addClass(s).addClass("in")}},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function r(){var t=setTimeout(function(){n.off(e.support.transition.end).remove()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.remove()})}var t=this,n=this.tip();return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?r():n.remove(),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").removeAttr("title")},hasContent:function(){return this.getTitle()},getPosition:function(t){return e.extend({},t?{top:0,left:0}:this.$element.offset(),{width:this.$element[0].offsetWidth,height:this.$element[0].offsetHeight})},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(){this[this.tip().hasClass("in")?"hide":"show"]()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}},e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
',trigger:"hover",title:"",delay:0,html:!0}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content > *")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-content")||(typeof n.content=="function"?n.content.call(t[0]):n.content),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}}),e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

'})}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var t=e(this),n=t.data("target")||t.attr("href"),r=/^#\w/.test(n)&&e(n);return r&&r.length&&[[r.position().top,n]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}},e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active a").last()[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}},e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e(function(){e("body").on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=e(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:t.top+t.height,left:t.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),(e.browser.chrome||e.browser.webkit||e.browser.msie)&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this))},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=!~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(e){var t=this;setTimeout(function(){t.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(t){this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")}},e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e(function(){e("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;t.preventDefault(),n.typeahead(n.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))},e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); -------------------------------------------------------------------------------- /test_project/templates/_form.html: -------------------------------------------------------------------------------- 1 |
    2 | {% csrf_token %} 3 | {{ form.as_p }} 4 | 5 |
    6 | 7 | -------------------------------------------------------------------------------- /test_project/templates/auth/new_user_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'site_base.html' %} 2 | {% load rules_light_tags %} 3 | 4 | {% block body %} 5 |

    Welcome to django-rules-light test project

    6 | 7 | List of users: 8 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /test_project/templates/auth/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'site_base.html' %} 2 | 3 | {% block body %} 4 | You can 'auth.user.read' user: {{ object }}. 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /test_project/templates/auth/user_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'site_base.html' %} 2 | 3 | {% block body %} 4 | {% include '_form.html' %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /test_project/templates/auth/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'site_base.html' %} 2 | {% load url from future %} 3 | 4 | {% load rules_light_tags %} 5 | 6 | {% block body %} 7 |

    Welcome to django-rules-light test project

    8 | 9 | List of users: 10 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /test_project/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'site_base.html' %} 2 | 3 | {% block body %} 4 |

    Welcome to django-rules-light test project

    5 | 6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /test_project/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'site_base.html' %} 2 | 3 | {% block body %} 4 |

    Try with test/test

    5 | {% include '_form.html' %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /test_project/templates/site_base.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | {% block head_title %}{% endblock %} 5 | 6 | {% block extra_head %} 7 | {% endblock %} 8 | 9 | 10 |

    11 | {% if request.user.is_authenticated %} 12 | Authenticated as {{ request.user }}. 13 | {% trans 'Login again' %} 14 | or 15 | {% trans 'Logout' %} 16 | {% else %} 17 | Not authenticated. 18 | {% trans 'Login' %} 19 | {% endif %} 20 |

    21 | 22 | {% block body %} 23 | {% endblock %} 24 | 25 | 26 | 27 | 28 | {% block extra_body %} 29 | {% endblock %} 30 | 31 | 32 | -------------------------------------------------------------------------------- /test_project/test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/test_project/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/test_project/rules_logging.py: -------------------------------------------------------------------------------- 1 | LOGGING = { 2 | 'version': 1, 3 | 'disable_existing_loggers': False, 4 | 'filters': { 5 | 'require_debug_false': { 6 | '()': 'django.utils.log.RequireDebugFalse' 7 | } 8 | }, 9 | 'handlers': { 10 | 'mail_admins': { 11 | 'level': 'ERROR', 12 | 'filters': ['require_debug_false'], 13 | 'class': 'django.utils.log.AdminEmailHandler' 14 | }, 15 | 'console': { 16 | 'level': 'DEBUG', 17 | 'class': 'logging.StreamHandler', 18 | }, 19 | 'malicious': { 20 | 'level': 'WARN', 21 | 'class': 'logging.FileHandler', 22 | 'filename': 'malicious.log', 23 | }, 24 | }, 25 | 'loggers': { 26 | 'django.request': { 27 | 'handlers': ['mail_admins'], 28 | 'level': 'ERROR', 29 | 'propagate': True, 30 | }, 31 | 'rules_light': { 32 | 'handlers': ['console', 'malicious'], 33 | 'propagate': True, 34 | 'level': 'DEBUG', 35 | }, 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /test_project/test_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for test_project project. 2 | 3 | import os.path 4 | import posixpath 5 | import django 6 | 7 | CACHES = { 8 | 'default': { 9 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 10 | 'LOCATION': 'unique-snowflake' 11 | } 12 | } 13 | 14 | PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 15 | 16 | DEBUG = True 17 | TEMPLATE_DEBUG = DEBUG 18 | 19 | ADMINS = ( 20 | # ('Your Name', 'your_email@example.com'), 21 | ) 22 | 23 | FIXTURE_DIRS = [ 24 | os.path.join(PROJECT_ROOT, 'fixtures'), 25 | ] 26 | 27 | MANAGERS = ADMINS 28 | 29 | LOGIN_URL='/auth/login/' 30 | 31 | DATABASES = { 32 | 'default': { 33 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 34 | 'NAME': 'db.sqlite', # Or path to database file if using sqlite3. 35 | } 36 | } 37 | # Local time zone for this installation. Choices can be found here: 38 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 39 | # although not all choices may be available on all operating systems. 40 | # In a Windows environment this must be set to your system time zone. 41 | TIME_ZONE = 'America/Chicago' 42 | 43 | # Language code for this installation. All choices can be found here: 44 | # http://www.i18nguy.com/unicode/language-identifiers.html 45 | LANGUAGE_CODE = 'en-us' 46 | 47 | SITE_ID = 1 48 | 49 | # If you set this to False, Django will make some optimizations so as not 50 | # to load the internationalization machinery. 51 | USE_I18N = True 52 | 53 | # If you set this to False, Django will not format dates, numbers and 54 | # calendars according to the current locale. 55 | USE_L10N = True 56 | 57 | # If you set this to False, Django will not use timezone-aware datetimes. 58 | USE_TZ = True 59 | 60 | # Absolute filesystem path to the directory that will hold user-uploaded files. 61 | # Example: "/home/media/media.lawrence.com/media/" 62 | MEDIA_ROOT = '' 63 | 64 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 65 | # trailing slash. 66 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 67 | MEDIA_URL = '' 68 | 69 | # Absolute path to the directory static files should be collected to. 70 | # Don't put anything in this directory yourself; store your static files 71 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 72 | # Example: "/home/media/media.lawrence.com/static/" 73 | STATIC_ROOT = '' 74 | 75 | # URL prefix for static files. 76 | # Example: "http://media.lawrence.com/static/" 77 | STATIC_URL = '/static/' 78 | 79 | # Additional locations of static files 80 | STATICFILES_DIRS = ( 81 | os.path.join(PROJECT_ROOT, 'static'), 82 | ) 83 | 84 | # List of finder classes that know how to find static files in 85 | # various locations. 86 | STATICFILES_FINDERS = ( 87 | 'django.contrib.staticfiles.finders.FileSystemFinder', 88 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 89 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 90 | ) 91 | 92 | # Make this unique, and don't share it with anybody. 93 | SECRET_KEY = 'vuu5^s4r7qj2g*np&vg*+f!%(2@ur=fgchp^2f$(9kjr1sy*yc' 94 | 95 | # List of callables that know how to import templates from various sources. 96 | TEMPLATE_LOADERS = ( 97 | 'django.template.loaders.filesystem.Loader', 98 | 'django.template.loaders.app_directories.Loader', 99 | # 'django.template.loaders.eggs.Loader', 100 | ) 101 | 102 | MIDDLEWARE = MIDDLEWARE_CLASSES = ( 103 | 'django.middleware.common.CommonMiddleware', 104 | 'django.contrib.sessions.middleware.SessionMiddleware', 105 | 'django.middleware.csrf.CsrfViewMiddleware', 106 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 107 | 'django.contrib.messages.middleware.MessageMiddleware', 108 | 'rules_light.middleware.Middleware', 109 | # Uncomment the next line for simple clickjacking protection: 110 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 111 | ) 112 | 113 | ROOT_URLCONF = 'test_project.urls' 114 | 115 | # Python dotted path to the WSGI application used by Django's runserver. 116 | WSGI_APPLICATION = 'test_project.wsgi.application' 117 | 118 | TEMPLATE_DIRS = ( 119 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 120 | # Always use forward slashes, even on Windows. 121 | # Don't forget to use absolute paths, not relative paths. 122 | os.path.join(PROJECT_ROOT, 'templates'), 123 | ) 124 | 125 | if django.VERSION < (1, 8): 126 | TEMPLATE_CONTEXT_PROCESSORS = ( 127 | 'django.contrib.auth.context_processors.auth', 128 | 'django.core.context_processors.debug', 129 | 'django.core.context_processors.i18n', 130 | 'django.core.context_processors.media', 131 | 'django.core.context_processors.static', 132 | 'django.core.context_processors.tz', 133 | 'django.contrib.messages.context_processors.messages' 134 | ) 135 | else: 136 | TEMPLATE_CONTEXT_PROCESSORS = ( 137 | 'django.contrib.auth.context_processors.auth', 138 | 'django.template.context_processors.debug', 139 | 'django.template.context_processors.i18n', 140 | 'django.template.context_processors.media', 141 | 'django.template.context_processors.static', 142 | 'django.template.context_processors.tz', 143 | 'django.contrib.messages.context_processors.messages' 144 | ) 145 | 146 | TEMPLATES = [ 147 | { 148 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 149 | 'APP_DIRS': True, 150 | 'OPTIONS': { 151 | 'context_processors': TEMPLATE_CONTEXT_PROCESSORS, 152 | }, 153 | 'DIRS': TEMPLATE_DIRS, 154 | }, 155 | ] 156 | 157 | INSTALLED_APPS = ( 158 | 'django.contrib.auth', 159 | 'django.contrib.contenttypes', 160 | 'django.contrib.sessions', 161 | 'django.contrib.sites', 162 | 'django.contrib.messages', 163 | 'django.contrib.staticfiles', 164 | 'django.contrib.admin', 165 | 'rules_light', 166 | 'test_rule', 167 | # Uncomment the next line to enable admin documentation: 168 | # 'django.contrib.admindocs', 169 | ) 170 | 171 | # A sample logging configuration. The only tangible logging 172 | # performed by this configuration is to send an email to 173 | # the site admins on every HTTP 500 error when DEBUG=False. 174 | from .rules_logging import LOGGING 175 | -------------------------------------------------------------------------------- /test_project/test_project/urls.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.conf.urls import include, url 3 | from django.views import generic 4 | from django.contrib.auth.models import User 5 | 6 | import rules_light 7 | # not need in this particular project ... oh well it'll serve as example 8 | rules_light.autodiscover() 9 | # import our project specific rules 10 | import auth_rules 11 | from django.contrib import admin 12 | 13 | urlpatterns = [ 14 | url(r'^admin/', admin.site.urls), 15 | url(r'^auth/', include('django.contrib.auth.urls')), 16 | url(r'^rules/', include('rules_light.urls')), 17 | 18 | url(r'^$', generic.ListView.as_view(model=User, 19 | template_name='auth/user_list.html' if django.VERSION < (1, 7) 20 | else 'auth/new_user_list.html'), name='auth_user_list'), 21 | url(r'user/(?P[\w_-]+)/$', 22 | rules_light.class_decorator(generic.DetailView).as_view( 23 | slug_field='username', slug_url_kwarg='username', model=User), 24 | name='auth_user_detail'), 25 | url(r'user/(?P[\w_-]+)/update/$', 26 | rules_light.class_decorator(generic.UpdateView).as_view( 27 | slug_field='username', slug_url_kwarg='username', model=User), 28 | name='auth_user_update'), 29 | ] 30 | -------------------------------------------------------------------------------- /test_project/test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_project project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /test_project/test_requirements_without_django.txt: -------------------------------------------------------------------------------- 1 | # Test requirements without Django, to work around https://github.com/pypa/pip/issues/2367. 2 | mock 3 | cssselect 4 | pytest 5 | pytest-django 6 | -------------------------------------------------------------------------------- /test_project/test_rule/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/test_project/test_rule/__init__.py -------------------------------------------------------------------------------- /test_project/test_rule/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yourlabs/django-rules-light/adae5a08c4e9d3bda6b58f226a36189650604d23/test_project/test_rule/models.py -------------------------------------------------------------------------------- /test_project/test_rule/rules_light_registry.py: -------------------------------------------------------------------------------- 1 | import rules_light 2 | 3 | 4 | rules_light.registry['test_rule.example'] = lambda *a: False 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27}-django{18,19,110,111}, py{36}-django{18,19,110,111,20,21} 3 | 4 | [testenv] 5 | usedevelop = true 6 | commands = python setup.py test 7 | whitelist_externals = 8 | deps = 9 | mock 10 | pytest 11 | django18: Django>=1.8,<1.9 12 | django19: Django>=1.9,<1.10 13 | django110: Django>=1.10,<111 14 | django111: Django>=1.11,<2.0 15 | django20: Django>=2.0,<2.1 16 | django21: Django>=2.1,<2.2 17 | setenv = 18 | DJANGO_SETTINGS_MODULE=test_project.settings 19 | PIP_ALLOW_EXTERNAL=true 20 | PYTHONPATH=test_project 21 | 22 | [testenv:checkqa] 23 | basepython = python3.6 24 | commands = flake8 rules_light 25 | deps = flake8 26 | 27 | [testenv:checkqa-python2] 28 | basepython = python2.7 29 | commands = flake8 rules_light 30 | deps = flake8 31 | 32 | [pytest] 33 | markers = 34 | django_db 35 | --------------------------------------------------------------------------------