Compare commits

..

133 commits
0.8 ... master

Author SHA1 Message Date
jazzband-bot
f58bbef27e Jazzband: Created local 'CODE_OF_CONDUCT.md' from remote 'CODE_OF_CONDUCT.md' 2021-10-27 16:59:39 +06:00
Jannis Leidel
bcb3216dd1
Merge pull request #72 from jazzband/gha
Migrate to GitHub Actions
2020-11-26 16:31:26 +01:00
Jannis Leidel
28b6e4cb59
Minor updates. 2020-11-26 10:00:51 +01:00
Jannis Leidel
ff95d54a26
Remove use of six. 2020-11-26 09:55:45 +01:00
Jannis Leidel
f0948c08e9
Update render_to_response. 2020-11-26 09:49:45 +01:00
Jannis Leidel
19766a7469
Add tox config for Python and Django additions. 2020-11-26 09:40:27 +01:00
Jannis Leidel
e3bd439961
Add Python 3.6 and 3.8 and Django 3.0 and 3.1. 2020-11-26 09:38:53 +01:00
Jannis Leidel
9dc0516683
Add release workflow. 2020-11-26 09:32:28 +01:00
Jannis Leidel
5c2803c32c
Write coverage file. 2020-11-26 09:29:44 +01:00
Jannis Leidel
8f7df22869
Map gha to tox. 2020-11-26 09:28:20 +01:00
Jannis Leidel
7494d94b54
Initial GitHub Actions workflow. 2020-11-26 09:24:14 +01:00
Asif Saif Uddin
6c82e3db63
Merge pull request #66 from mikebq/action-error-list-no-attrib-data
Fixes ActionErrorList creation resulting in AttributeError
2020-02-26 15:24:42 +06:00
Asif Saif Uddin
5c5236f0d2
Merge pull request #67 from mikebq/django-1-10-cycle-syntax-issue
Fixes cycle syntax
2020-02-26 15:23:21 +06:00
Michael Brannan
e94703f675 Fixes cycle syntax 2020-02-25 13:33:53 +00:00
Michael Brannan
94d6f29a15 Initialise base class before calling into it. 2020-02-25 12:00:49 +00:00
Jannis Leidel
3677b421fe
Make wheel file universal. 2020-02-07 13:19:07 +01:00
Jannis Leidel
c2289898fb
Skipping even less. 2020-02-07 12:12:46 +01:00
Jannis Leidel
b08191e1b8
Skip less. 2020-02-07 12:01:31 +01:00
Jannis Leidel
a755cbe456
More updates to changelog. 2020-02-07 11:43:33 +01:00
Jannis Leidel
1372cf1f6f
Move tests from example app into main package. 2020-02-07 11:38:50 +01:00
Jannis Leidel
fcc569b0d9
Remove local_settings since it's an anti-pattern. 2020-02-07 11:38:31 +01:00
Jannis Leidel
3d07c79272
Remove compat code from the past. 2020-02-07 11:38:19 +01:00
Jannis Leidel
0a6e3bdbcb
Minor fixes. 2020-02-07 11:27:59 +01:00
Gunnlaugur Þór Briem
06905e90c4
Fix BasePermission.assign for group perms
fixes #26
2020-02-07 11:17:51 +01:00
Jannis Leidel
1866f087b2
Update year. 2020-02-07 11:14:48 +01:00
Jannis Leidel
4d5a4ceeab
Blacken docs config. 2020-02-07 11:14:43 +01:00
Jannis Leidel
c39adbb1bb
More updates to changelog. 2020-02-07 11:13:48 +01:00
Jannis Leidel
c5be724ec9
Merge branch 'master' of github.com:jazzband/django-authority 2020-02-07 11:10:34 +01:00
Jannis Leidel
f793207cb9
Merge remote-tracking branch 'libraM/master' 2020-02-07 11:10:21 +01:00
Jannis Leidel
82d7feb47b
Merge pull request #57 from epfl-idevelop/master
Change module discovery to use the one from Django
2020-02-07 11:06:21 +01:00
Jannis Leidel
2dcbffe297
Merge branch 'master' into master 2020-02-07 11:04:41 +01:00
Jannis Leidel
07f0935ae2
Check the build files with twine check. 2020-02-07 10:59:34 +01:00
Jannis Leidel
a2cc00e3b5
Skipping more. 2020-02-07 10:54:37 +01:00
Jannis Leidel
d8542ffb87
Blacken the code. 2020-02-07 10:50:47 +01:00
Jannis Leidel
d891b2753d
Update changelog and simplify packaging. 2020-02-07 10:49:01 +01:00
Jannis Leidel
8bdca24ced
Updated travis and tox config files to match test envs. 2020-02-07 10:40:40 +01:00
Jannis Leidel
91ddd54a8d
Remove hgignore. 2020-02-07 10:40:13 +01:00
Safwan Rahman
52dc715454
Fixing tox 2020-01-24 16:45:48 +06:00
Jason Ward
4cf4853232 Add support for Django 2.2 (#64)
* Updated tox to test only python{2,3}.7 and djagno1.11/2.2

* Updated travis.

* Updated models/migrations.

* Updated path to `reverse`

* Updated path to login view.

* Updated middlewares/installed apps

* Updated .travis again.

* Lets see if that will get tests working in python3 on travis.

* Switch to in memory DB for tests

* Stop running tests with python2.7 and django 2.2. They shouldn't work anyway.

* Sure did that wrong. Fix which python versions are running with which code.

* That isn't needed. Lets see if that is what is breaking python3.

* Testing a theory.

* Revert "Testing a theory."

This reverts commit 69b3e4c906.

* Added initial migration for example users.

* is_authenticated is a bool, not a method.
2020-01-24 16:04:44 +06:00
Reece Dunham
58e08483cd
Fix Jannis’s name! 2019-01-13 17:10:40 -05:00
Reece Dunham
e3e4cd713e
Merge pull request #62 from cclauss/patch-1
'basestring' was removed in Python 3
2019-01-10 15:29:07 -05:00
Reece Dunham
e823da69d9
Merge pull request #54 from jazzband/copyright
Copyright fixes
2019-01-10 15:26:53 -05:00
Reece Dunham
4bbf12c095
Update conf.py 2019-01-10 15:26:36 -05:00
Reece Dunham
371c477aa5
Update LICENSE 2019-01-10 15:26:01 -05:00
cclauss
eebdcaa009
'basestring' was removed in Python 3
__from django.utils.six import string_types__
2018-12-02 11:49:53 +01:00
Benjamin Pereto
97cd73f9d7
Merge pull request #59 from bashu/patch-1
Syntax specification for codeblocks in README.rst
2018-10-14 16:39:30 +02:00
Basil Shubin
d0f5667687
let there be colour 2018-02-20 17:32:18 +07:00
Jannis Leidel
f58ec2d240
rST fixes. 2018-01-28 15:03:18 +01:00
Jannis Leidel
ce14ebd6a0
Update changelog. 2018-01-28 15:00:26 +01:00
Jannis Leidel
85ee004837
Remove requirements file again. 2018-01-28 14:58:16 +01:00
Jannis Leidel
f1ba811043
Fix distribution name. 2018-01-28 14:57:35 +01:00
Jannis Leidel
a07c553918
Another try. 2018-01-28 14:55:44 +01:00
Jannis Leidel
0083e11b8a
Add docs requirements.txt. 2018-01-28 14:51:19 +01:00
Jannis Leidel
5f47c69f6b
Update date of release of 0.13. 2018-01-28 14:36:28 +01:00
Jannis Leidel
d0bf6512a9
Use coveragerc. 2018-01-28 14:35:33 +01:00
Jannis Leidel
2356491fd7
Install coverage. 2018-01-28 14:31:44 +01:00
Jannis Leidel
0945016e59
Use tox-travis and drop support for Python 3.3. Also coverage. 2018-01-28 14:27:53 +01:00
Jannis Leidel
5d466ac318
Use setuptools_scm. 2018-01-28 14:17:11 +01:00
Jannis Leidel
9a0d4ab678
Fixed setup.py. 2018-01-28 14:14:01 +01:00
Jannis Leidel
76a180bcc9
Enabled project releases via Jazzband. 2018-01-28 14:11:32 +01:00
Safwan Rahman
72692ad381
Fixing for release version 2018-01-19 17:46:07 +06:00
Safwan Rahman
44595e7066
Update Readme for release 2018-01-19 17:44:02 +06:00
Jason Ward
a868d54945 Add support for Django 1.11 (#58)
* refs #1: Updated tox + travis.

* refs #1; Fixed path to register.

* refs #1: Updated urls.py

* refs #1: Added username field. Not really sure why it was needed, but whatever.

* Added an update note.

* refs #2: Updated travis.

* refs #2: Updated command to run tests.

* refs #2: Added a test showing permission_required is busted.

* refs #2: Custom user modal needs a default manager.

* refs #2: Updated settings.

* refs #2: Stop the exception from being raised.

* refs #2: Fixed a problem with named parameters.
2018-01-19 17:37:15 +06:00
Julien Delasoie
c50368eb34 Change module discovery to use the one from Django 2017-10-12 15:45:59 +02:00
jpic
bc3e375718 Copyright fixes 2017-08-28 19:49:19 +02:00
Asif Saifuddin Auvi
14b92a0dcd Merge pull request #52 from GregLeBarbar/django_1_10
Django 1.10
2017-01-08 12:19:42 +06:00
Wes Winham
74137a540b Merge pull request #53 from cl0ne/patch-1
Correct SCM name in installation guide
2016-12-17 11:43:27 -05:00
Vladislav Glinsky
85bf2e2d2b Correct SCM name in installation guide 2016-12-17 18:26:32 +02:00
GregLeBarbar
6fad04fc4c Merge pull request #1 from reycf/django_1_10
Fix render_to_string() calls with unexpected context_instance param
2016-09-20 13:46:41 +02:00
Charles Francois Rey
9e4035118e Fix render_to_string() calls with unexpected context_instance parameter (Django 1.10 compatibility) 2016-09-16 16:39:44 +02:00
greg
2f7822af83 change in travis config to test it against django 1.10 2016-08-15 11:50:44 +02:00
greg
57684b94df Add a blank line for the last line + adapt tox.ini for django1.10 2016-08-15 11:30:38 +02:00
greg
d4dc955e63 Django 1.10 - django.conf.urls.patterns() is removed 2016-08-12 15:41:30 +02:00
greg
8aa4c85942 update tests 2016-08-12 13:01:06 +02:00
greg
91929a76c9 Features removed in 1.9 :
1. All models need to be defined inside an installed application or declare an explicit app_label. Furthermore, it isn’t possible to import them before their application is loaded. In particular, it isn’t possible to import models inside the root package of an application

2. The django.contrib.contenttypes.generic module is removed

3. django.db.models.loading is removed.
2016-07-27 18:41:25 +02:00
Safwan Rahman
1cef19d7de Bump Release 0.11 2016-07-17 03:19:48 +06:00
Safwan Rahman
f7c85994b9 Remove the old migrations 2016-07-17 03:14:36 +06:00
Safwan Rahman
232ab607fc Merge pull request #49 from adamchainz/readthedocs.io
Convert readthedocs links for their .org -> .io migration for hosted projects
2016-06-16 03:07:51 +06:00
Adam Chainz
bf57a34af4 Convert readthedocs links for their .org -> .io migration for hosted projects
As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’:

> Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard.

Test Plan: Manually visited all the links I’ve modified.
2016-06-11 10:58:02 +01:00
Jannis Leidel
dd1d840379 Jazzband badge. 2016-05-11 14:53:23 +02:00
Jannis Leidel
7d242ebf00 Some badges. 2016-05-11 14:52:28 +02:00
Jannis Leidel
9bc6159d32 Updated Tox and Travis config. 2016-05-11 14:49:30 +02:00
Jannis Leidel
46634c3b46 Updated docs for Jazzband. 2016-05-11 14:49:15 +02:00
Jannis Leidel
26ed10824d Get rid of ancient bootstrap setup. 2016-05-11 14:46:51 +02:00
Jannis Leidel
c713eb5419 Merge pull request #46 from safwanrahman/master
Update and add migration for django 1.8
2016-04-19 13:57:14 +02:00
Safwan Rahman
bc5118b524 update readme for a new release 2016-03-29 23:30:26 +06:00
Safwan Rahman
8935320f4b Adding migration to support Django 1.8 2016-03-29 23:26:48 +06:00
Safwan Rahman
08359c4880 Remove older Django support and fix styling issues 2016-03-29 23:24:31 +06:00
Wes Winham
cfe9d670ba Merge pull request #47 from jlward/move-to-jazzbandco
Prep for moving to the Jazzband organization
2016-03-15 15:19:10 -04:00
winhamwr
762c1e9594 Jazzbandco: Contributing guidelines and readme badge 2016-03-15 14:09:41 -04:00
Jason Ward
1bcf8d5685 Merge pull request #45 from jlward/issue_45
BasePermissionForm needs a Meta.exclude for django 1.8
2015-12-14 11:29:37 -05:00
Jason Ward
c289790102 refs #45: Update Note 2015-12-14 11:12:18 -05:00
Jason Ward
8f81984939 refs #45: Fixed broken test. 2015-12-14 11:09:43 -05:00
Jason Ward
c5c853e12b refs #45: Updated the tests. 2015-12-14 11:09:32 -05:00
Jason Ward
8e33c667f9 refs #45: Updated gitignore 2015-12-14 11:09:12 -05:00
Bob Cribbs
a6cd4d080a Bumped to version 0.9 2015-11-11 18:18:01 +02:00
Bob Cribbs
3b5ebfd4c5 Merge pull request #44 from bocribbz/tox-dj18
Django1.8: Use prefetch_related for M2M to `user__groups`
2015-11-11 00:41:00 +02:00
Bob Cribbs
11bc2fe188 drop support for django 1.3; use prefetch_related for m2m to user__groups 2015-11-11 00:26:21 +02:00
Bob Cribbs
265ce90d86 Merge pull request #43 from bocribbz/dj17-example
Updates for Django 1.7
2015-11-10 22:50:29 +02:00
Bob Cribbs
2c2927f696 add compat.py file for user model 2015-11-10 22:45:10 +02:00
Bob Cribbs
9589dfc820 import admin in example so it works with dj17 2015-11-10 19:31:54 +02:00
Bob Cribbs
dbd0c39d78 Merge pull request #42 from bocribbz/tox-dj17
Run tests on Django 1.7
2015-11-10 19:04:02 +02:00
Bob Cribbs
14aaf4b065 run tests on Django 1.7 2015-11-10 16:17:50 +00:00
Bob Cribbs
e712aa607e Merge pull request #33 from DylanLukes/master
Django 1.7 compatibility
2015-11-10 18:10:28 +02:00
Bob Cribbs
bbe44db5ac Merge pull request #41 from bocribbz/tox
Add tox and make travis run tox tests
2015-11-10 18:08:30 +02:00
Bob Cribbs
091c20929c add self to authors 2015-11-10 15:50:36 +00:00
Bob Cribbs
d28f11468c Add tox and make travis run tox tests 2015-11-10 14:52:36 +00:00
Yakovlev Alexander
bf2423b574 Django 1.6 admin compatibility fix. Refs #37. 2014-04-16 08:53:39 +04:00
Jannis Leidel
2288ccdf36 Merge pull request #34 from mozillazg/fix-typo
Fixed typo
2014-02-21 15:13:01 +01:00
Mozillazg
3cb36d9e24 Fixed typo 2014-02-21 13:22:58 +08:00
Dylan Lukes
b1f63c8d25 Django 1.7 compatibility 2014-02-20 13:35:33 -05:00
Jannis Leidel
49a52b7957 Merge pull request #11 from gthb/issue_15
Fix TypeError in edit_permissions
2014-02-20 17:53:22 +01:00
Jannis Leidel
f26feb3e47 Merge pull request #29 from gthb/fix-edit_permission-action
Fix edit permission action
2014-02-20 17:53:02 +01:00
Jannis Leidel
c394cc699e Merge pull request #28 from gthb/patch-1
Fix lurking bug in edit_permissions
2014-02-20 17:52:44 +01:00
Jannis Leidel
15aab326cb Merge pull request #31 from mozillazg/patch-2
Change argument extra_context's default value
2014-02-20 17:52:16 +01:00
mozillazg
00aaa026d9 Change argument extra_context's default value
Change keyword argument extra_context's default value from `{}` to `None`.
2014-02-20 22:11:57 +08:00
Wes Winham
d3f67e7361 Merge pull request #30 from mozillazg/patch-1
Fixed typo
2014-02-19 08:48:39 -05:00
mozillazg
63334e667f Fixed typo 2014-02-19 21:32:57 +08:00
Gunnlaugur Þór Briem
37cef6308e Change submit name to make edit_permission work
Turns out that since Django 1.2 the admin only invokes actions when
handling POST if the POST data specifically *doesn't* contain `_save`:

cce32a9b09

So this submit name was preventing `edit_permission` from acting on the
POST data since Django 1.2.
2014-02-12 00:15:16 +00:00
Gunnlaugur Þór Briem
423bb90c71 Show action form errors using message_user
Not very neat, but way better than nothing when there are errors
(previously the error was invisible and the action simply seemed to have
completed).
2014-02-12 00:15:13 +00:00
Gunnlaugur Thor Briem
40924e532d Fix lurking bug in edit_permissions
I don't have a test case, this is just code review; that string extrapolation will always raise.
2014-02-11 11:20:31 +00:00
Wes Winham
8c8e478a31 Added Gunnlaugur Thor Briem to AUTHORS
Thanks for #24 and #25!
2014-02-10 13:26:23 -05:00
Wes Winham
13f425c5c2 Merge pull request #25 from gthb/fix-jsi18n-path
Fix hardcoded jsi18n script path
2014-02-10 13:25:31 -05:00
Wes Winham
8ea429d448 Merge pull request #24 from gthb/issue18
Fix CSRF failure posting permission change form
2014-02-10 13:24:52 -05:00
Gunnlaugur Þór Briem
0023127fc7 Fix hardcoded jsi18n script path
Use normal Django URL reversal instead of hardcoded relative path.
2014-02-10 18:05:18 +00:00
Gunnlaugur Þór Briem
c81f252840 Fix CSRF failure posting permission change form
Add missing `{% csrf_token %}` in form.

fixes #18
2014-02-10 18:03:24 +00:00
Jannis Leidel
8c5339d5f4 Merge pull request #23 from jennyq/patch-1
fixing invalid syntax error
2014-01-30 01:14:19 -08:00
Jenny Qian
ad61de0cae fixing invalid syntax error
Got the following error when installing django-authority:
  File "/path/to/env/build/django-authority/example/users/admin.py", line 2
    from example.users.User
                          ^
SyntaxError: invalid syntax
2014-01-29 16:18:38 -06:00
Wes Winham
426e0db6af Removed liar comment RE: smart cache default
The smart cache defaults to enabled.
2014-01-29 15:29:13 -05:00
Jannis Leidel
ff0ad30673 Merge pull request #21 from tloiret/patch-1
Fix FlatPage class name
2014-01-06 04:37:11 -08:00
Thomas
9900543ce5 Fix FlatPage class name 2014-01-06 10:19:52 +01:00
Gunnlaugur Þór Briem
a88432cc28 Break out of for-loop when field found
Matching the `return` statement that the previous commit replaced.
2013-07-13 00:34:23 +00:00
Gunnlaugur Þór Briem
6728ba94b3 Fix TypeError in edit_permissions under Django 1.3
Call superclass `formfield_for_dbfield` so that it gets to do its stuff
(including removing `request` from `kwargs`)

fixes: #15
2013-05-29 10:55:54 +00:00
60 changed files with 1466 additions and 1060 deletions

6
.coveragerc Normal file
View file

@ -0,0 +1,6 @@
[run]
source = authority
branch = 1
[report]
omit = *tests*,*migrations*

53
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: Release
on:
push:
tags:
- '*'
jobs:
build:
if: github.repository == 'jazzband/django-authority'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: release-${{ hashFiles('**/setup.py') }}
restore-keys: |
release-
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools twine wheel
- name: Build package
run: |
python setup.py --version
python setup.py sdist --format=gztar bdist_wheel
twine check dist/*
- name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/django-authority/upload

48
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: Test
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
python-version: ['2.7', '3.6', '3.7', '3.8']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ matrix.python-version }}-v1-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade tox tox-gh-actions
- name: Tox tests
run: |
tox -v
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
name: Python ${{ matrix.python-version }}

7
.gitignore vendored
View file

@ -2,4 +2,9 @@
*.egg-info
*.sql
docs/build/*
.DS_Store
.DS_Store
.tox/
dist/
build/
.coverage
.eggs/

View file

@ -1,26 +0,0 @@
syntax: glob
*.pyc
*.pyo
*~
*.swp
*.orig
*.kpf
*.egg-info
.project
.pydevproject
.DS_Store
MANIFEST
dist
build
dev.db
local_settings.py
parts/*
eggs/*
downloads/*
.installed.cfg
bin/*
develop-eggs/*.egg-link
example/example.db
docs/build
TODO
example/example.db

View file

@ -1,22 +0,0 @@
language: python
python:
- "2.6"
- "2.7"
- "3.3"
script: ./example/manage.py test authority
env:
- TRAVIS_DJANGO_VERSION=1.3
- TRAVIS_DJANGO_VERSION=1.4
- TRAVIS_DJANGO_VERSION=1.5
- TRAVIS_DJANGO_VERSION=1.6
install:
- pip install django==$TRAVIS_DJANGO_VERSION --use-mirrors
matrix:
exclude:
- python: "3.3"
env: TRAVIS_DJANGO_VERSION=1.3
- python: "3.3"
env: TRAVIS_DJANGO_VERSION=1.4
notifications:
email:
- jason.louard.ward@gmail.com

View file

@ -7,3 +7,5 @@ Kyle Gibson <kyle.gibson@policystat.com>
Jason Ward <jason.ward@policystat.com>
Travis Chase <http://www.supercodepoet.com/>
Remigiusz Dymecki <remigiusz@gmail.com>
Gunnlaugur Thor Briem <https://github.com/gthb>
Bob Cribbs <https://github.com/bocribbz>

46
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,46 @@
# Code of Conduct
As contributors and maintainers of the Jazzband projects, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in the Jazzband a harassment-free experience
for everyone, regardless of the level of experience, gender, gender identity and
expression, sexual orientation, disability, personal appearance, body size, race,
ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic addresses,
without explicit permission
- Other unethical or unprofessional conduct
The Jazzband roadies have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are not
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, the roadies commit themselves to fairly and
consistently applying these principles to every aspect of managing the jazzband
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
removed from the Jazzband roadies.
This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
reporter of an incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/3/0/

5
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,5 @@
[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/)
This is a [Jazzband](https://jazzband.co/) project.
By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct)
and follow the [guidelines](https://jazzband.co/about/guidelines).

View file

@ -1,4 +1,4 @@
Copyright (c) 2009, Jannis Leidel
Copyright (c) 2009-2020, Jannis Leidel
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -25,4 +25,4 @@ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,8 +1,6 @@
include AUTHORS
include LICENSE
include README.rst
include buildout.cfg
include bootstrap.py
recursive-include authority/templates/authority *.html
recursive-include authority/templates/admin *.html
recursive-include authority/fixtures *.json

View file

@ -2,6 +2,17 @@
django-authority
================
.. image:: https://jazzband.co/static/img/badge.svg
:target: https://jazzband.co/
:alt: Jazzband
.. image:: https://github.com/jazzband/django-authority/workflows/Test/badge.svg
:target: https://github.com/jazzband/django-authority/actions
:alt: GitHub Actions
.. image:: https://codecov.io/gh/jazzband/django-authority/branch/master/graph/badge.svg
:target: https://codecov.io/gh/jazzband/django-authority
This is a Django app for per-object-permissions that includes a bunch of
helpers to create custom permission checks.
@ -10,36 +21,31 @@ The main website for django-authority is
`in-development version`_ of django-authority with
``pip install django-authority==dev`` or ``easy_install django-authority==dev``.
.. _`django-authority.readthedocs.org`: http://django-authority.readthedocs.org/
.. _in-development version: https://github.com/jezdez/django-authority/archive/master.zip#egg=django-authority-dev
.. _`django-authority.readthedocs.org`: https://django-authority.readthedocs.io/
.. _in-development version: https://github.com/jazzband/django-authority/archive/master.zip#egg=django-authority-dev
Example
=======
To get the example project running do:
- Bootstrap the buildout by running::
- Bootstrap the environment by running in a virtualenv::
python bootstrap.py
- Get the required packages by running::
bin/buildout
pip install Django
pip install -e .
- Sync the database::
bin/django-trunk syncdb
python example/manage.py migrate
- Run the development server and visit the admin at http://127.0.0.1:8000/admin/::
bin/django-trunk runserver
python example/manage.py runserver
Now create a flatage and open it to see some of the templatetags in action.
Don't hesitate to use the admin to edit the permission objects.
Full docs coming soon.
Please use https://github.com/jezdez/django-authority/issues/ for issues and bug reports.
Please use https://github.com/jazzband/django-authority/issues/ for issues and bug reports.
Documentation
=============
@ -52,6 +58,66 @@ html version using the setup.py::
Changelog:
==========
0.15 (unreleased):
------------------
* Moved CI to GitHub Actions.
* Add Django 3.0 and 3.1 support.
* Add Python 3.6 and 3.8 support.
0.14 (2020-02-07):
------------------
* Add Django 2.2 support
* Add Python 3.7 support
* Various fixes around the test harness.
* Use Django's own method of auto-loading permissions modules.
* Fix Django admin incompatibility regarding a method removed years ago.
* Removed unused compatibility code.
* Fix BasePermission.assign for group permissions.
0.13.1 (2018-01-28):
--------------------
* Minor fixes to the documentation and versioning.
0.13 (2018-01-28):
------------------
* Added support for Django 1.11
* Drop Support for Python 3.3
* Fixed a bug with template loader
0.12 (2017-01-10):
------------------
* Added support for Django 1.10
0.11 (2016-07-17):
------------------
* Added Migration in order to support Django 1.8
* Dropped Support for Django 1.7 and lower
* Remove SQL Migration Files
* Documentation Updates
* Fix linter issues
0.10 (2015-12-14):
------------------
* Fixed a bug with BasePermissionForm and django 1.8
0.9 (2015-11-11):
-----------------
* Added support for Django 1.7 and 1.8
* Dropped support for Django 1.3
0.8 (2013-12-20):
-----------------
@ -87,7 +153,9 @@ Changelog:
* Added ability to override form class in ``add_permission`` view.
* Added easy way to assign permissions via a permission instance, e.g.::
* Added easy way to assign permissions via a permission instance, e.g.:
.. code-block:: python
from django.contrib.auth.models import User
from mysite.articles.permissions import ArticlePermission
@ -115,13 +183,17 @@ Changelog:
* The templatetags have also been refactored to be easier to customize
which required a change in the template tag signature:
Old::
Old:
.. code-block:: html+django
{% permission_form flatpage %}
{% permission_form flatpage "flatpage_permission.top_secret" %}
{% permission_form OBJ PERMISSION_LABEL.CHECK_NAME %}
New::
New:
.. code-block:: html+django
{% permission_form for flatpage %}
{% permission_form for flatpage using "flatpage_permission.top_secret" %}
@ -140,4 +212,4 @@ Changelog:
allows to request permissions, but also add them (only for users with
the 'authority.add_permission' Django permission).
.. _`migrations/`: https://github.com/jezdez/django-authority/tree/master/migrations
.. _`migrations/`: https://github.com/jazzbands/django-authority/tree/master/migrations

View file

@ -1,8 +1,14 @@
import sys
from authority.sites import site, get_check, get_choices_for, register, unregister
from pkg_resources import get_distribution, DistributionNotFound
try:
__version__ = get_distribution("django-authority").version
except DistributionNotFound:
# package is not installed
pass
LOADING = False
def autodiscover():
"""
Goes and imports the permissions submodule of every app in INSTALLED_APPS
@ -13,19 +19,6 @@ def autodiscover():
return
LOADING = True
import imp
from django.conf import settings
from django.utils.module_loading import autodiscover_modules
for app in settings.INSTALLED_APPS:
try:
__import__(app)
app_path = sys.modules[app].__path__
except AttributeError:
continue
try:
imp.find_module('permissions', app_path)
except ImportError:
continue
__import__("%s.permissions" % app)
app_path = sys.modules["%s.permissions" % app]
LOADING = False
autodiscover_modules("permissions")

View file

@ -1,12 +1,12 @@
from django import forms, template
from django import forms
from django.http import HttpResponseRedirect
from django.utils.translation import ugettext, ungettext, ugettext_lazy as _
from django.shortcuts import render_to_response
from django.shortcuts import render
from django.utils.safestring import mark_safe
from django.forms.formsets import all_valid
from django.contrib import admin
from django.contrib.admin import helpers
from django.contrib.contenttypes import generic
from django.contrib.admin import actions, helpers
from django.contrib.contenttypes.admin import GenericTabularInline
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
@ -15,47 +15,48 @@ try:
except ImportError:
from django.utils.encoding import force_unicode as force_text
try:
from django.contrib.admin import actions
except ImportError:
actions = False
from authority.models import Permission
from authority.widgets import GenericForeignKeyRawIdWidget
from authority import get_choices_for
from authority.utils import get_choices_for
class PermissionInline(generic.GenericTabularInline):
class PermissionInline(GenericTabularInline):
model = Permission
raw_id_fields = ('user', 'group', 'creator')
raw_id_fields = ("user", "group", "creator")
extra = 1
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'codename':
if db_field.name == "codename":
perm_choices = get_choices_for(self.parent_model)
kwargs['label'] = _('permission')
kwargs['widget'] = forms.Select(choices=perm_choices)
return db_field.formfield(**kwargs)
kwargs["label"] = _("permission")
kwargs["widget"] = forms.Select(choices=perm_choices)
return super(PermissionInline, self).formfield_for_dbfield(db_field, **kwargs)
class ActionPermissionInline(PermissionInline):
raw_id_fields = ()
template = 'admin/edit_inline/action_tabular.html'
template = "admin/edit_inline/action_tabular.html"
class ActionErrorList(forms.util.ErrorList):
class ActionErrorList(forms.utils.ErrorList):
def __init__(self, inline_formsets):
super(ActionErrorList, self).__init__()
for inline_formset in inline_formsets:
self.extend(inline_formset.non_form_errors())
for errors_in_inline_form in inline_formset.errors:
self.extend(errors_in_inline_form.values())
def edit_permissions(modeladmin, request, queryset):
opts = modeladmin.model._meta
app_label = opts.app_label
# Check that the user has the permission to edit permissions
if not (request.user.is_superuser or
request.user.has_perm('authority.change_permission') or
request.user.has_perm('authority.change_foreign_permissions')):
if not (
request.user.is_superuser
or request.user.has_perm("authority.change_permission")
or request.user.has_perm("authority.change_foreign_permissions")
):
raise PermissionDenied
inline = ActionPermissionInline(queryset.model, modeladmin.admin_site)
@ -66,10 +67,11 @@ def edit_permissions(modeladmin, request, queryset):
prefix = "%s-%s" % (FormSet.get_default_prefix(), obj.pk)
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1:
prefix = "%s-%s-%s" % (prefix, prefixes[prefix])
if request.POST.get('post'):
formset = FormSet(data=request.POST, files=request.FILES,
instance=obj, prefix=prefix)
prefix = "%s-%s" % (prefix, prefixes[prefix])
if request.POST.get("post"):
formset = FormSet(
data=request.POST, files=request.FILES, instance=obj, prefix=prefix
)
else:
formset = FormSet(instance=obj, prefix=prefix)
formsets.append(formset)
@ -82,81 +84,105 @@ def edit_permissions(modeladmin, request, queryset):
inline_admin_formsets.append(inline_admin_formset)
media = media + inline_admin_formset.media
ordered_objects = opts.get_ordered_objects()
if request.POST.get('post'):
if request.POST.get("post"):
if all_valid(formsets):
for formset in formsets:
formset.save()
else:
modeladmin.message_user(
request,
"; ".join(
err.as_text() for formset in formsets for err in formset.errors
),
)
# redirect to full request path to make sure we keep filter
return HttpResponseRedirect(request.get_full_path())
context = {
'errors': ActionErrorList(formsets),
'title': ugettext('Permissions for %s') % force_text(opts.verbose_name_plural),
'inline_admin_formsets': inline_admin_formsets,
'app_label': app_label,
'change': True,
'ordered_objects': ordered_objects,
'form_url': mark_safe(''),
'opts': opts,
'target_opts': queryset.model._meta,
'content_type_id': ContentType.objects.get_for_model(queryset.model).id,
'save_as': False,
'save_on_top': False,
'is_popup': False,
'media': mark_safe(media),
'show_delete': False,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'queryset': queryset,
"errors": ActionErrorList(formsets),
"title": ugettext("Permissions for %s") % force_text(opts.verbose_name_plural),
"inline_admin_formsets": inline_admin_formsets,
"app_label": app_label,
"change": True,
"form_url": mark_safe(""),
"opts": opts,
"target_opts": queryset.model._meta,
"content_type_id": ContentType.objects.get_for_model(queryset.model).id,
"save_as": False,
"save_on_top": False,
"is_popup": False,
"media": mark_safe(media),
"show_delete": False,
"action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
"queryset": queryset,
"object_name": force_text(opts.verbose_name),
}
template_name = getattr(modeladmin, 'permission_change_form_template', [
"admin/%s/%s/permission_change_form.html" % (app_label, opts.object_name.lower()),
"admin/%s/permission_change_form.html" % app_label,
"admin/permission_change_form.html"
])
return render_to_response(template_name, context,
context_instance=template.RequestContext(request))
edit_permissions.short_description = _("Edit permissions for selected %(verbose_name_plural)s")
template_name = getattr(
modeladmin,
"permission_change_form_template",
[
"admin/%s/%s/permission_change_form.html"
% (app_label, opts.object_name.lower()),
"admin/%s/permission_change_form.html" % app_label,
"admin/permission_change_form.html",
],
)
return render(request, template_name, context)
edit_permissions.short_description = _(
"Edit permissions for selected %(verbose_name_plural)s"
)
class PermissionAdmin(admin.ModelAdmin):
list_display = ('codename', 'content_type', 'user', 'group', 'approved')
list_filter = ('approved', 'content_type')
search_fields = ('user__username', 'group__name', 'codename')
raw_id_fields = ('user', 'group', 'creator')
generic_fields = ('content_object',)
actions = ['approve_permissions']
list_display = ("codename", "content_type", "user", "group", "approved")
list_filter = ("approved", "content_type")
search_fields = ("user__username", "group__name", "codename")
raw_id_fields = ("user", "group", "creator")
generic_fields = ("content_object",)
actions = ["approve_permissions"]
fieldsets = (
(None, {'fields': ('codename', ('content_type', 'object_id'))}),
(_('Permitted'), {'fields': ('approved', 'user', 'group')}),
(_('Creation'), {'fields': ('creator', 'date_requested', 'date_approved')}),
(None, {"fields": ("codename", ("content_type", "object_id"))}),
(_("Permitted"), {"fields": ("approved", "user", "group")}),
(_("Creation"), {"fields": ("creator", "date_requested", "date_approved")}),
)
def formfield_for_dbfield(self, db_field, **kwargs):
# For generic foreign keys marked as generic_fields we use a special widget
if db_field.name in [f.fk_field for f in self.model._meta.virtual_fields if f.name in self.generic_fields]:
names = [
f.fk_field
for f in self.model._meta.virtual_fields
if f.name in self.generic_fields
]
if db_field.name in names:
for gfk in self.model._meta.virtual_fields:
if gfk.fk_field == db_field.name:
return db_field.formfield(
widget=GenericForeignKeyRawIdWidget(
gfk.ct_field, self.admin_site._registry.keys()))
kwargs["widget"] = GenericForeignKeyRawIdWidget(
gfk.ct_field, self.admin_site._registry.keys()
)
break
return super(PermissionAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def queryset(self, request):
user = request.user
if (user.is_superuser or
user.has_perm('permissions.change_foreign_permissions')):
if user.is_superuser or user.has_perm("permissions.change_foreign_permissions"):
return super(PermissionAdmin, self).queryset(request)
return super(PermissionAdmin, self).queryset(request).filter(creator=user)
def approve_permissions(self, request, queryset):
for permission in queryset:
permission.approve(request.user)
message = ungettext("%(count)d permission successfully approved.",
"%(count)d permissions successfully approved.", len(queryset))
self.message_user(request, message % {'count': len(queryset)})
message = ungettext(
"%(count)d permission successfully approved.",
"%(count)d permissions successfully approved.",
len(queryset),
)
self.message_user(request, message % {"count": len(queryset)})
approve_permissions.short_description = _("Approve selected permissions")
admin.site.register(Permission, PermissionAdmin)
if actions:

View file

@ -2,25 +2,34 @@ import inspect
from django.http import HttpResponseRedirect
from django.utils.http import urlquote
from django.utils.functional import wraps
from django.db.models import Model, get_model
from django.db.models import Model
from django.apps import apps
from django.shortcuts import get_object_or_404
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from authority import get_check
from authority.utils import get_check
from authority.views import permission_denied
try:
basestring
except NameError:
basestring = str
def permission_required(perm, *lookup_variables, **kwargs):
"""
Decorator for views that checks whether a user has a particular permission
enabled, redirecting to the log-in page if necessary.
"""
login_url = kwargs.pop('login_url', settings.LOGIN_URL)
redirect_field_name = kwargs.pop('redirect_field_name', REDIRECT_FIELD_NAME)
redirect_to_login = kwargs.pop('redirect_to_login', True)
login_url = kwargs.pop("login_url", settings.LOGIN_URL)
redirect_field_name = kwargs.pop("redirect_field_name", REDIRECT_FIELD_NAME)
redirect_to_login = kwargs.pop("redirect_to_login", True)
def decorate(view_func):
def decorated(request, *args, **kwargs):
if request.user.is_authenticated():
if request.user.is_authenticated:
params = []
for lookup_variable in lookup_variables:
if isinstance(lookup_variable, basestring):
@ -34,16 +43,19 @@ def permission_required(perm, *lookup_variables, **kwargs):
if value is None:
continue
if isinstance(model, basestring):
model_class = get_model(*model.split("."))
model_class = apps.get_model(*model.split("."))
else:
model_class = model
if model_class is None:
raise ValueError(
"The given argument '%s' is not a valid model." % model)
if (inspect.isclass(model_class) and
not issubclass(model_class, Model)):
"The given argument '%s' is not a valid model." % model
)
if inspect.isclass(model_class) and not issubclass(
model_class, Model
):
raise ValueError(
'The argument %s needs to be a model.' % model)
"The argument %s needs to be a model." % model
)
obj = get_object_or_404(model_class, **{lookup: value})
params.append(obj)
check = get_check(request.user, perm)
@ -55,15 +67,18 @@ def permission_required(perm, *lookup_variables, **kwargs):
if redirect_to_login:
path = urlquote(request.get_full_path())
tup = login_url, redirect_field_name, path
return HttpResponseRedirect('%s?%s=%s' % tup)
return HttpResponseRedirect("%s?%s=%s" % tup)
return permission_denied(request)
return wraps(view_func)(decorated)
return decorate
def permission_required_or_403(perm, *args, **kwargs):
"""
Decorator that wraps the permission_required decorator and returns a
permission denied (403) page instead of redirecting to the login URL.
"""
kwargs['redirect_to_login'] = False
kwargs["redirect_to_login"] = False
return permission_required(perm, *args, **kwargs)

View file

@ -1,12 +1,14 @@
class AuthorityException(Exception):
pass
class NotAModel(AuthorityException):
def __init__(self, object):
super(NotAModel, self).__init__(
"Not a model class or instance")
super(NotAModel, self).__init__("Not a model class or instance")
class UnsavedModelInstance(AuthorityException):
def __init__(self, object):
super(UnsavedModelInstance, self).__init__(
"Model instance has no pk, was it saved?")
"Model instance has no pk, was it saved?"
)

View file

@ -1,20 +0,0 @@
[
{
"pk": 1,
"model": "auth.user",
"fields": {
"username": "jezdez",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2009-11-02 03:06:19",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2009-11-02 03:06:19"
}
}
]

View file

@ -1,20 +1,20 @@
[
{
"pk": 1,
"model": "users.user",
"pk": 1,
"model": "users.user",
"fields": {
"first_name": "Jez",
"last_name": "Dez",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2009-11-02 03:06:19",
"groups": [],
"user_permissions": [],
"password": "",
"email": "jezdez@github.com",
"first_name": "Jez",
"last_name": "Dez",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2009-11-02 03:06:19",
"groups": [],
"user_permissions": [],
"password": "",
"email": "jezdez@github.com",
"date_joined": "2009-11-02 03:06:19",
"greeting_message": "Hello customer user model"
"greeting_message": "Hello customer user model"
}
}
]

View file

@ -1,29 +1,33 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import mark_safe
from authority import permissions, get_choices_for
from authority import permissions
from authority.utils import get_choices_for
from authority.models import Permission
from authority.utils import User
User = get_user_model()
class BasePermissionForm(forms.ModelForm):
codename = forms.CharField(label=_('Permission'))
codename = forms.CharField(label=_("Permission"))
class Meta:
model = Permission
exclude = []
def __init__(self, perm=None, obj=None, approved=False, *args, **kwargs):
self.perm = perm
self.obj = obj
self.approved = approved
if obj and perm:
self.base_fields['codename'].widget = forms.HiddenInput()
self.base_fields["codename"].widget = forms.HiddenInput()
elif obj and (not perm or not approved):
perms = get_choices_for(self.obj)
self.base_fields['codename'].widget = forms.Select(choices=perms)
self.base_fields["codename"].widget = forms.Select(choices=perms)
super(BasePermissionForm, self).__init__(*args, **kwargs)
def save(self, request, commit=True, *args, **kwargs):
@ -34,15 +38,16 @@ class BasePermissionForm(forms.ModelForm):
self.instance.approved = self.approved
return super(BasePermissionForm, self).save(commit)
class UserPermissionForm(BasePermissionForm):
user = forms.CharField(label=_('User'))
user = forms.CharField(label=_("User"))
class Meta(BasePermissionForm.Meta):
fields = ('user',)
fields = ("user",)
def __init__(self, *args, **kwargs):
if not kwargs.get('approved', False):
self.base_fields['user'].widget = forms.HiddenInput()
if not kwargs.get("approved", False):
self.base_fields["user"].widget = forms.HiddenInput()
super(UserPermissionForm, self).__init__(*args, **kwargs)
def clean_user(self):
@ -51,34 +56,41 @@ class UserPermissionForm(BasePermissionForm):
user = User.objects.get(username__iexact=username)
except User.DoesNotExist:
raise forms.ValidationError(
mark_safe(_("A user with that username does not exist.")))
mark_safe(_("A user with that username does not exist."))
)
check = permissions.BasePermission(user=user)
error_msg = None
if user.is_superuser:
error_msg = _("The user %(user)s do not need to request "
"access to any permission as it is a super user.")
error_msg = _(
"The user %(user)s do not need to request "
"access to any permission as it is a super user."
)
elif check.has_perm(self.perm, self.obj):
error_msg = _("The user %(user)s already has the permission "
"'%(perm)s' for %(object_name)s '%(obj)s'")
error_msg = _(
"The user %(user)s already has the permission "
"'%(perm)s' for %(object_name)s '%(obj)s'"
)
elif check.requested_perm(self.perm, self.obj):
error_msg = _("The user %(user)s already requested the permission"
" '%(perm)s' for %(object_name)s '%(obj)s'")
error_msg = _(
"The user %(user)s already requested the permission"
" '%(perm)s' for %(object_name)s '%(obj)s'"
)
if error_msg:
error_msg = error_msg % {
'object_name': self.obj._meta.object_name.lower(),
'perm': self.perm,
'obj': self.obj,
'user': user,
"object_name": self.obj._meta.object_name.lower(),
"perm": self.perm,
"obj": self.obj,
"user": user,
}
raise forms.ValidationError(mark_safe(error_msg))
return user
class GroupPermissionForm(BasePermissionForm):
group = forms.CharField(label=_('Group'))
group = forms.CharField(label=_("Group"))
class Meta(BasePermissionForm.Meta):
fields = ('group',)
fields = ("group",)
def clean_group(self):
groupname = self.cleaned_data["group"]
@ -86,13 +98,21 @@ class GroupPermissionForm(BasePermissionForm):
group = Group.objects.get(name__iexact=groupname)
except Group.DoesNotExist:
raise forms.ValidationError(
mark_safe(_("A group with that name does not exist.")))
mark_safe(_("A group with that name does not exist."))
)
check = permissions.BasePermission(group=group)
if check.has_perm(self.perm, self.obj):
raise forms.ValidationError(mark_safe(
_("This group already has the permission '%(perm)s' for %(object_name)s '%(obj)s'") % {
'perm': self.perm,
'object_name': self.obj._meta.object_name.lower(),
'obj': self.obj,
}))
raise forms.ValidationError(
mark_safe(
_(
"This group already has the permission '%(perm)s' "
"for %(object_name)s '%(obj)s'"
)
% {
"perm": self.perm,
"object_name": self.obj._meta.object_name.lower(),
"obj": self.obj,
}
)
)
return group

View file

@ -4,7 +4,6 @@ from django.contrib.contenttypes.models import ContentType
class PermissionManager(models.Manager):
def get_content_type(self, obj):
return ContentType.objects.get_for_model(obj)
@ -12,40 +11,41 @@ class PermissionManager(models.Manager):
return self.filter(content_type=self.get_content_type(obj))
def for_object(self, obj, approved=True):
return self.get_for_model(obj).select_related(
'user', 'creator', 'group', 'content_type'
).filter(object_id=obj.id, approved=approved)
return (
self.get_for_model(obj)
.select_related("user", "creator", "group", "content_type")
.filter(object_id=obj.id, approved=approved)
)
def for_user(self, user, obj, check_groups=True):
perms = self.get_for_model(obj)
if not check_groups:
return perms.select_related('user', 'creator').filter(user=user)
return perms.select_related("user", "creator").filter(user=user)
# Hacking user to user__pk to workaround deepcopy bug:
# http://bugs.python.org/issue2460
# Which is triggered by django's deepcopy which backports that fix in
# Django 1.2
return perms.select_related('user', 'user__groups', 'creator').filter(
Q(user__pk=user.pk) | Q(group__in=user.groups.all()))
return (
perms.select_related("user", "creator")
.prefetch_related("user__groups")
.filter(Q(user__pk=user.pk) | Q(group__in=user.groups.all()))
)
def user_permissions(
self, user, perm, obj, approved=True, check_groups=True):
return self.for_user(
user,
obj,
check_groups,
).filter(
codename=perm,
approved=approved,
def user_permissions(self, user, perm, obj, approved=True, check_groups=True):
return self.for_user(user, obj, check_groups,).filter(
codename=perm, approved=approved,
)
def group_permissions(self, group, perm, obj, approved=True):
"""
Get objects that have Group perm permission on
"""
return self.get_for_model(obj).select_related(
'user', 'group', 'creator').filter(group=group, codename=perm,
approved=approved)
return (
self.get_for_model(obj)
.select_related("user", "group", "creator")
.filter(group=group, codename=perm, approved=approved)
)
def delete_objects_permissions(self, obj):
"""

View file

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import datetime
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
("auth", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("contenttypes", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Permission",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("codename", models.CharField(max_length=100, verbose_name="codename")),
("object_id", models.PositiveIntegerField()),
(
"approved",
models.BooleanField(
default=False,
help_text="Designates whether the permission has been approved and treated as active. Unselect this instead of deleting permissions.",
verbose_name="approved",
),
),
(
"date_requested",
models.DateTimeField(
default=datetime.datetime.now, verbose_name="date requested"
),
),
(
"date_approved",
models.DateTimeField(
null=True, verbose_name="date approved", blank=True
),
),
(
"content_type",
models.ForeignKey(
related_name="row_permissions",
to="contenttypes.ContentType",
on_delete=models.CASCADE,
),
),
(
"creator",
models.ForeignKey(
related_name="created_permissions",
blank=True,
to=settings.AUTH_USER_MODEL,
null=True,
on_delete=models.CASCADE,
),
),
(
"group",
models.ForeignKey(
blank=True, to="auth.Group", null=True, on_delete=models.CASCADE
),
),
(
"user",
models.ForeignKey(
related_name="granted_permissions",
blank=True,
to=settings.AUTH_USER_MODEL,
null=True,
on_delete=models.CASCADE,
),
),
],
options={
"verbose_name": "permission",
"verbose_name_plural": "permissions",
"permissions": (
("change_foreign_permissions", "Can change foreign permissions"),
("delete_foreign_permissions", "Can delete foreign permissions"),
("approve_permission_requests", "Can approve permission requests"),
),
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name="permission",
unique_together=set(
[("codename", "object_id", "content_type", "user", "group")]
),
),
]

View file

View file

@ -2,12 +2,13 @@ from datetime import datetime
from django.conf import settings
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.contrib.auth.models import User, Group
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.auth.models import Group
from django.utils.translation import ugettext_lazy as _
from authority.managers import PermissionManager
from authority.utils import User
USER_MODEL = getattr(settings, "AUTH_USER_MODEL", "auth.User")
class Permission(models.Model):
@ -16,19 +17,41 @@ class Permission(models.Model):
This kind of permission is associated with a user/group and an object
of any content type.
"""
codename = models.CharField(_('codename'), max_length=100)
content_type = models.ForeignKey(ContentType, related_name="row_permissions")
codename = models.CharField(_("codename"), max_length=100)
content_type = models.ForeignKey(
ContentType, related_name="row_permissions", on_delete=models.CASCADE
)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
content_object = GenericForeignKey("content_type", "object_id")
user = models.ForeignKey(User, null=True, blank=True, related_name='granted_permissions')
group = models.ForeignKey(Group, null=True, blank=True)
creator = models.ForeignKey(User, null=True, blank=True, related_name='created_permissions')
user = models.ForeignKey(
USER_MODEL,
null=True,
blank=True,
related_name="granted_permissions",
on_delete=models.CASCADE,
)
group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.CASCADE)
creator = models.ForeignKey(
USER_MODEL,
null=True,
blank=True,
related_name="created_permissions",
on_delete=models.CASCADE,
)
approved = models.BooleanField(_('approved'), default=False, help_text=_("Designates whether the permission has been approved and treated as active. Unselect this instead of deleting permissions."))
approved = models.BooleanField(
_("approved"),
default=False,
help_text=_(
"Designates whether the permission has been approved and treated as active. "
"Unselect this instead of deleting permissions."
),
)
date_requested = models.DateTimeField(_('date requested'), default=datetime.now)
date_approved = models.DateTimeField(_('date approved'), blank=True, null=True)
date_requested = models.DateTimeField(_("date requested"), default=datetime.now)
date_approved = models.DateTimeField(_("date approved"), blank=True, null=True)
objects = PermissionManager()
@ -37,12 +60,12 @@ class Permission(models.Model):
class Meta:
unique_together = ("codename", "object_id", "content_type", "user", "group")
verbose_name = _('permission')
verbose_name_plural = _('permissions')
verbose_name = _("permission")
verbose_name_plural = _("permissions")
permissions = (
('change_foreign_permissions', 'Can change foreign permissions'),
('delete_foreign_permissions', 'Can delete foreign permissions'),
('approve_permission_requests', 'Can approve permission requests'),
("change_foreign_permissions", "Can change foreign permissions"),
("delete_foreign_permissions", "Can delete foreign permissions"),
("approve_permission_requests", "Can approve permission requests"),
)
def save(self, *args, **kwargs):

View file

@ -14,9 +14,9 @@ class PermissionMetaclass(type):
Used to generate the default set of permission checks "add", "change" and
"delete".
"""
def __new__(cls, name, bases, attrs):
new_class = super(
PermissionMetaclass, cls).__new__(cls, name, bases, attrs)
new_class = super(PermissionMetaclass, cls).__new__(cls, name, bases, attrs)
if not new_class.label:
new_class.label = "%s_permission" % new_class.__name__.lower()
new_class.label = slugify(new_class.label)
@ -31,11 +31,12 @@ class BasePermission(object):
"""
Base Permission class to be used to define app permissions.
"""
__metaclass__ = PermissionMetaclass
checks = ()
label = None
generic_checks = ['add', 'browse', 'change', 'delete']
generic_checks = ["add", "browse", "change", "delete"]
def __init__(self, user=None, group=None, *args, **kwargs):
self.user = user
@ -48,10 +49,7 @@ class BasePermission(object):
"""
if not self.user:
return {}, {}
group_pks = set(self.user.groups.values_list(
'pk',
flat=True,
))
group_pks = set(self.user.groups.values_list("pk", flat=True,))
perms = Permission.objects.filter(
Q(user__pk=self.user.pk) | Q(group__pk__in=group_pks),
)
@ -59,22 +57,26 @@ class BasePermission(object):
group_permissions = {}
for perm in perms:
if perm.user_id == self.user.pk:
user_permissions[(
perm.object_id,
perm.content_type_id,
perm.codename,
perm.approved,
)] = True
user_permissions[
(
perm.object_id,
perm.content_type_id,
perm.codename,
perm.approved,
)
] = True
# If the user has the permission do for something, but perm.user !=
# self.user then by definition that permission came from the
# group.
else:
group_permissions[(
perm.object_id,
perm.content_type_id,
perm.codename,
perm.approved,
)] = True
group_permissions[
(
perm.object_id,
perm.content_type_id,
perm.codename,
perm.approved,
)
] = True
return user_permissions, group_permissions
def _get_group_cached_perms(self):
@ -83,17 +85,12 @@ class BasePermission(object):
"""
if not self.group:
return {}
perms = Permission.objects.filter(
group=self.group,
)
perms = Permission.objects.filter(group=self.group,)
group_permissions = {}
for perm in perms:
group_permissions[(
perm.object_id,
perm.content_type_id,
perm.codename,
perm.approved,
)] = True
group_permissions[
(perm.object_id, perm.content_type_id, perm.codename, perm.approved,)
] = True
return group_permissions
def _prime_user_perm_caches(self):
@ -123,11 +120,7 @@ class BasePermission(object):
# Check to see if the cache has been primed.
if not self.user:
return {}
cache_filled = getattr(
self.user,
'_authority_perm_cache_filled',
False,
)
cache_filled = getattr(self.user, "_authority_perm_cache_filled", False,)
if cache_filled:
# Don't really like the name for this, but this matches how Django
# does it.
@ -145,11 +138,7 @@ class BasePermission(object):
# Check to see if the cache has been primed.
if not self.group:
return {}
cache_filled = getattr(
self.group,
'_authority_perm_cache_filled',
False,
)
cache_filled = getattr(self.group, "_authority_perm_cache_filled", False,)
if cache_filled:
# Don't really like the name for this, but this matches how Django
# does it.
@ -167,11 +156,7 @@ class BasePermission(object):
# Check to see if the cache has been primed.
if not self.user:
return {}
cache_filled = getattr(
self.user,
'_authority_perm_cache_filled',
False,
)
cache_filled = getattr(self.user, "_authority_perm_cache_filled", False,)
if cache_filled:
return self.user._authority_group_perm_cache
@ -194,9 +179,7 @@ class BasePermission(object):
@property
def use_smart_cache(self):
# AUTHORITY_USE_SMART_CACHE defaults to False to maintain backwards
# compatibility.
use_smart_cache = getattr(settings, 'AUTHORITY_USE_SMART_CACHE', True)
use_smart_cache = getattr(settings, "AUTHORITY_USE_SMART_CACHE", True)
return (self.user or self.group) and use_smart_cache
def has_user_perms(self, perm, obj, approved, check_groups=True):
@ -212,12 +195,7 @@ class BasePermission(object):
def _user_has_perms(cached_perms):
# Check to see if the permission is in the cache.
return cached_perms.get((
obj.pk,
content_type_pk,
perm,
approved,
))
return cached_perms.get((obj.pk, content_type_pk, perm, approved,))
# Check to see if the permission is in the cache.
if _user_has_perms(self._user_perm_cache):
@ -229,15 +207,13 @@ class BasePermission(object):
return False
# Actually hit the DB, no smart cache used.
return Permission.objects.user_permissions(
self.user,
perm,
obj,
approved,
check_groups,
).filter(
object_id=obj.pk,
).exists()
return (
Permission.objects.user_permissions(
self.user, perm, obj, approved, check_groups,
)
.filter(object_id=obj.pk,)
.exists()
)
def has_group_perms(self, perm, obj, approved):
"""
@ -251,24 +227,17 @@ class BasePermission(object):
def _group_has_perms(cached_perms):
# Check to see if the permission is in the cache.
return cached_perms.get((
obj.pk,
content_type_pk,
perm,
approved,
))
return cached_perms.get((obj.pk, content_type_pk, perm, approved,))
# Check to see if the permission is in the cache.
return _group_has_perms(self._group_perm_cache)
# Actually hit the DB, no smart cache used.
return Permission.objects.group_permissions(
self.group,
perm, obj,
approved,
).filter(
object_id=obj.pk,
).exists()
return (
Permission.objects.group_permissions(self.group, perm, obj, approved,)
.filter(object_id=obj.pk,)
.exists()
)
def has_perm(self, perm, obj, check_groups=True, approved=True):
"""
@ -307,25 +276,20 @@ class BasePermission(object):
return perms
def get_django_codename(
self, check, model_or_instance, generic=False, without_left=False):
self, check, model_or_instance, generic=False, without_left=False
):
if without_left:
perm = check
else:
perm = '%s.%s' % (model_or_instance._meta.app_label, check.lower())
perm = "%s.%s" % (model_or_instance._meta.app_label, check.lower())
if generic:
perm = '%s_%s' % (
perm,
model_or_instance._meta.object_name.lower(),
)
perm = "%s_%s" % (perm, model_or_instance._meta.object_name.lower(),)
return perm
def get_codename(self, check, model_or_instance, generic=False):
perm = '%s.%s' % (self.label, check.lower())
perm = "%s.%s" % (self.label, check.lower())
if generic:
perm = '%s_%s' % (
perm,
model_or_instance._meta.object_name.lower(),
)
perm = "%s_%s" % (perm, model_or_instance._meta.object_name.lower(),)
return perm
def assign(self, check=None, content_object=None, generic=False):
@ -347,7 +311,7 @@ class BasePermission(object):
content_objects = content_object
if not check:
checks = self.generic_checks + getattr(self, 'checks', [])
checks = self.generic_checks + getattr(self, "checks", [])
elif not isinstance(check, (list, tuple)):
checks = (check,)
else:
@ -366,14 +330,11 @@ class BasePermission(object):
for check in checks:
if isinstance(content_object, Model):
# make an authority per object permission
codename = self.get_codename(
check,
content_object,
generic,
)
codename = self.get_codename(check, content_object, generic,)
try:
perm = Permission.objects.get(
user=self.user,
group=self.group,
codename=codename,
approved=True,
content_type=content_type,
@ -382,6 +343,7 @@ class BasePermission(object):
except Permission.DoesNotExist:
perm = Permission.objects.create(
user=self.user,
group=self.group,
content_object=content_object,
codename=codename,
approved=True,
@ -392,21 +354,16 @@ class BasePermission(object):
elif isinstance(content_object, ModelBase):
# make a Django permission
codename = self.get_django_codename(
check,
content_object,
generic,
without_left=True,
check, content_object, generic, without_left=True,
)
try:
perm = DjangoPermission.objects.get(codename=codename)
except DjangoPermission.DoesNotExist:
name = check
if '_' in name:
name = name[0:name.find('_')]
if "_" in name:
name = name[0 : name.find("_")]
perm = DjangoPermission(
name=name,
codename=codename,
content_type=content_type,
name=name, codename=codename, content_type=content_type,
)
perm.save()
self.user.user_permissions.add(perm)

View file

@ -1,4 +1,5 @@
from inspect import getmembers, ismethod
from django.apps import apps
from django.db import models
from django.db.models.base import ModelBase
from django.utils.translation import ugettext_lazy as _
@ -6,16 +7,20 @@ from django.core.exceptions import ImproperlyConfigured
from authority.permissions import BasePermission
class AlreadyRegistered(Exception):
pass
class NotRegistered(Exception):
pass
class PermissionSite(object):
"""
A dictionary that contains permission instances and their labels.
"""
_registry = {}
_choices = {}
@ -29,7 +34,7 @@ class PermissionSite(object):
return [perm for perm in self._registry.values() if perm.model == model]
def get_check(self, user, label):
perm_label, check_name = label.split('.')
perm_label, check_name = label.split(".")
perm_cls = self.get_permission_by_label(perm_label)
if perm_cls is None:
return None
@ -49,8 +54,8 @@ class PermissionSite(object):
for perm in self.get_permissions_by_model(model_cls):
for name, check in getmembers(perm, ismethod):
if name in perm.checks:
signature = '%s.%s' % (perm.label, name)
label = getattr(check, 'short_description', signature)
signature = "%s.%s" % (perm.label, name)
label = getattr(check, "short_description", signature)
choices.append((signature, label))
self._choices[model_cls] = choices
return choices
@ -64,17 +69,23 @@ class PermissionSite(object):
if permission_class.label in self.get_labels():
raise ImproperlyConfigured(
"The name of %s conflicts with %s" % (permission_class,
self.get_permission_by_label(permission_class.label)))
"The name of %s conflicts with %s"
% (
permission_class,
self.get_permission_by_label(permission_class.label),
)
)
for model in model_or_iterable:
if model in self._registry:
raise AlreadyRegistered(
'The model %s is already registered' % model.__name__)
"The model %s is already registered" % model.__name__
)
if options:
options['__module__'] = __name__
permission_class = type("%sPermission" % model.__name__,
(permission_class,), options)
options["__module__"] = __name__
permission_class = type(
"%sPermission" % model.__name__, (permission_class,), options
)
permission_class.model = model
self.setup(model, permission_class)
@ -85,7 +96,7 @@ class PermissionSite(object):
model_or_iterable = [model_or_iterable]
for model in model_or_iterable:
if model not in self._registry:
raise NotRegistered('The model %s is not registered' % model.__name__)
raise NotRegistered("The model %s is not registered" % model.__name__)
del self._registry[model]
def setup(self, model, permission):
@ -94,10 +105,12 @@ class PermissionSite(object):
if check_func is not None:
func = self.create_check(check_name, check_func)
func.__name__ = check_name
func.short_description = getattr(check_func, 'short_description',
_("%(object_name)s permission '%(check)s'") % {
'object_name': model._meta.object_name,
'check': check_name})
func.short_description = getattr(
check_func,
"short_description",
_("%(object_name)s permission '%(check)s'")
% {"object_name": model._meta.object_name, "check": check_name},
)
setattr(permission, check_name, func)
else:
permission.generic_checks.append(check_name)
@ -106,11 +119,12 @@ class PermissionSite(object):
object_name = model._meta.object_name
func_name = "%s_%s" % (check_name, object_name.lower())
func.short_description = _("Can %(check)s this %(object_name)s") % {
'object_name': model._meta.object_name.lower(),
'check': check_name}
"object_name": model._meta.object_name.lower(),
"check": check_name,
}
func.check_name = check_name
if func_name not in permission.checks:
permission.checks = (list(permission.checks) + [func_name])
permission.checks = list(permission.checks) + [func_name]
setattr(permission, func_name, func)
setattr(model, "permissions", PermissionDescriptor())
@ -120,15 +134,19 @@ class PermissionSite(object):
if check_func and not granted:
return check_func(self, *args, **kwargs)
return granted
return check
class PermissionDescriptor(object):
def get_content_type(self, obj=None):
ContentType = models.get_model("contenttypes", "contenttype")
ContentType = apps.get_model("contenttypes", "contenttype")
if obj:
return ContentType.objects.get_for_model(obj)
else:
raise Exception("Invalid arguments given to PermissionDescriptor.get_content_type")
raise Exception(
"Invalid arguments given to PermissionDescriptor.get_content_type"
)
def __get__(self, instance, owner):
if instance is None:
@ -136,6 +154,7 @@ class PermissionDescriptor(object):
ct = self.get_content_type(instance)
return ct.row_permissions.all()
site = PermissionSite()
get_check = site.get_check
get_choices_for = site.get_choices_for

View file

@ -19,7 +19,7 @@
{% if inline_admin_form.form.non_field_errors %}
<tr><td colspan="{{ inline_admin_form.field_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
{% endif %}
<tr class="{% cycle row1,row2 %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}">
<tr class="{% cycle 'row1' 'row2' %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}">
<td class="original">
{% if inline_admin_form.original or inline_admin_form.show_url %}<p>

View file

@ -2,20 +2,20 @@
{% load i18n admin_modify admin_static %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="../../../jsi18n/"></script>
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
{{ media }}
{% endblock %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}"/>{% endblock %}
{% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %}
{% block coltype %}colM{% endblock %}
{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
{% block breadcrumbs %}{% if not is_popup %}
<div class="breadcrumbs">
<a href="../../">{% trans "Home" %}</a> &rsaquo;
<a href="../">{{ app_label|capfirst|escape }}</a> &rsaquo;
<a href="../">{{ app_label|capfirst|escape }}</a> &rsaquo;
<a href="./">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
{% trans "Permissions" %}
</div>
@ -23,6 +23,7 @@
{% block content %}<div id="content-main">
<form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">
{% csrf_token %}
<div>
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
{% if save_on_top %}{% submit_row %}{% endif %}
@ -44,7 +45,7 @@
{% block after_related_objects %}{% endblock %}
<div class="submit-row">
<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" {{ onclick_attrib }}/>
<input type="submit" value="{% trans 'Save' %}" class="default" name="_save_action" {{ onclick_attrib }}/>
</div>
</div>

View file

@ -1,39 +1,46 @@
from django import template
from django.core.urlresolvers import reverse
from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse
from django.contrib.auth.models import AnonymousUser
from django.core.urlresolvers import reverse
from authority import get_check
from authority.utils import get_check
from authority import permissions
from authority.models import Permission
from authority.forms import UserPermissionForm
from authority.utils import User
User = get_user_model()
register = template.Library()
@register.simple_tag
def url_for_obj(view_name, obj):
return reverse(view_name, kwargs={
'app_label': obj._meta.app_label,
'module_name': obj._meta.module_name,
'pk': obj.pk})
return reverse(
view_name,
kwargs={
"app_label": obj._meta.app_label,
"module_name": obj._meta.module_name,
"pk": obj.pk,
},
)
@register.simple_tag
def add_url_for_obj(obj):
return url_for_obj('authority-add-permission', obj)
return url_for_obj("authority-add-permission", obj)
@register.simple_tag
def request_url_for_obj(obj):
return url_for_obj('authority-add-permission-request', obj)
return url_for_obj("authority-add-permission-request", obj)
class ResolverNode(template.Node):
"""
A small wrapper that adds a convenient resolve method.
"""
def resolve(self, var, context):
"""Resolves a variable out of context if it's not in quotes"""
if var is None:
@ -46,7 +53,7 @@ class ResolverNode(template.Node):
@classmethod
def next_bit_for(cls, bits, key, if_none=None):
try:
return bits[bits.index(key)+1]
return bits[bits.index(key) + 1]
except ValueError:
return if_none
@ -55,25 +62,27 @@ class PermissionComparisonNode(ResolverNode):
"""
Implements a node to provide an "if user/group has permission on object"
"""
@classmethod
def handle_token(cls, parser, token):
bits = token.contents.split()
if 5 < len(bits) < 3:
raise template.TemplateSyntaxError("'%s' tag takes three, "
"four or five arguments" % bits[0])
end_tag = 'endifhasperm'
nodelist_true = parser.parse(('else', end_tag))
raise template.TemplateSyntaxError(
"'%s' tag takes three, " "four or five arguments" % bits[0]
)
end_tag = "endifhasperm"
nodelist_true = parser.parse(("else", end_tag))
token = parser.next_token()
if token.contents == 'else': # there is an 'else' clause in the tag
if token.contents == "else": # there is an 'else' clause in the tag
nodelist_false = parser.parse((end_tag,))
parser.delete_first_token()
else:
nodelist_false = template.NodeList()
if len(bits) == 3: # this tag requires at most 2 objects . None is given
if len(bits) == 3: # this tag requires at most 2 objects . None is given
objs = (None, None)
elif len(bits) == 4:# one is given
elif len(bits) == 4: # one is given
objs = (bits[3], None)
else: #two are given
else: # two are given
objs = (bits[3], bits[4])
return cls(bits[2], bits[1], nodelist_true, nodelist_false, *objs)
@ -102,15 +111,16 @@ class PermissionComparisonNode(ResolverNode):
return self.nodelist_true.render(context)
# If the app couldn't be found
except (ImproperlyConfigured, ImportError):
return ''
return ""
# If either variable fails to resolve, return nothing.
except template.VariableDoesNotExist:
return ''
return ""
# If the types don't permit comparison, return nothing.
except (TypeError, AttributeError):
return ''
return ""
return self.nodelist_false.render(context)
@register.tag
def ifhasperm(parser, token):
"""
@ -124,7 +134,7 @@ def ifhasperm(parser, token):
meh
{% endifhasperm %}
{% if hasperm "poll_permission.change_poll" request.user %}
{% ifhasperm "poll_permission.change_poll" request.user %}
lalala
{% else %}
meh
@ -135,16 +145,14 @@ def ifhasperm(parser, token):
class PermissionFormNode(ResolverNode):
@classmethod
def handle_token(cls, parser, token, approved):
bits = token.contents.split()
tag_name = bits[0]
kwargs = {
'obj': cls.next_bit_for(bits, 'for'),
'perm': cls.next_bit_for(bits, 'using', None),
'template_name': cls.next_bit_for(bits, 'with', ''),
'approved': approved,
"obj": cls.next_bit_for(bits, "for"),
"perm": cls.next_bit_for(bits, "using", None),
"template_name": cls.next_bit_for(bits, "with", ""),
"approved": approved,
}
return cls(**kwargs)
@ -158,33 +166,40 @@ class PermissionFormNode(ResolverNode):
obj = self.resolve(self.obj, context)
perm = self.resolve(self.perm, context)
if self.template_name:
template_name = [self.resolve(obj, context) for obj in self.template_name.split(',')]
template_name = [
self.resolve(o, context) for o in self.template_name.split(",")
]
else:
template_name = 'authority/permission_form.html'
request = context['request']
template_name = "authority/permission_form.html"
request = context["request"]
extra_context = {}
if self.approved:
if (request.user.is_authenticated() and
request.user.has_perm('authority.add_permission')):
if request.user.is_authenticated and request.user.has_perm(
"authority.add_permission"
):
extra_context = {
'form_url': url_for_obj('authority-add-permission', obj),
'next': request.build_absolute_uri(),
'approved': self.approved,
'form': UserPermissionForm(perm, obj, approved=self.approved,
initial=dict(codename=perm)),
"form_url": url_for_obj("authority-add-permission", obj),
"next": request.build_absolute_uri(),
"approved": self.approved,
"form": UserPermissionForm(
perm, obj, approved=self.approved, initial=dict(codename=perm)
),
}
else:
if request.user.is_authenticated() and not request.user.is_superuser:
if request.user.is_authenticated and not request.user.is_superuser:
extra_context = {
'form_url': url_for_obj('authority-add-permission-request', obj),
'next': request.build_absolute_uri(),
'approved': self.approved,
'form': UserPermissionForm(perm, obj,
approved=self.approved, initial=dict(
codename=perm, user=request.user.username)),
"form_url": url_for_obj("authority-add-permission-request", obj),
"next": request.build_absolute_uri(),
"approved": self.approved,
"form": UserPermissionForm(
perm,
obj,
approved=self.approved,
initial=dict(codename=perm, user=request.user.username),
),
}
return template.loader.render_to_string(template_name, extra_context,
context_instance=template.RequestContext(request))
return template.loader.render_to_string(template_name, extra_context, request)
@register.tag
def permission_form(parser, token):
@ -200,6 +215,7 @@ def permission_form(parser, token):
"""
return PermissionFormNode.handle_token(parser, token, approved=True)
@register.tag
def permission_request_form(parser, token):
"""
@ -209,23 +225,23 @@ def permission_request_form(parser, token):
Syntax::
{% permission_request_form for OBJ and PERMISSION_LABEL.CHECK_NAME [with TEMPLATE] %}
{% permission_request_form for lesson using "lesson_permission.add_lesson" with "authority/permission_request_form.html" %}
{% permission_request_form for lesson using "lesson_permission.add_lesson"
with "authority/permission_request_form.html" %}
"""
return PermissionFormNode.handle_token(parser, token, approved=False)
class PermissionsForObjectNode(ResolverNode):
@classmethod
def handle_token(cls, parser, token, approved, name):
bits = token.contents.split()
tag_name = bits[0]
kwargs = {
'obj': cls.next_bit_for(bits, tag_name),
'user': cls.next_bit_for(bits, 'for'),
'var_name': cls.next_bit_for(bits, 'as', name),
'approved': approved,
"obj": cls.next_bit_for(bits, tag_name),
"user": cls.next_bit_for(bits, "for"),
"var_name": cls.next_bit_for(bits, "as", name),
"approved": approved,
}
return cls(**kwargs)
@ -246,14 +262,15 @@ class PermissionsForObjectNode(ResolverNode):
if isinstance(user, User):
perms = perms.filter(user=user)
context[var_name] = perms
return ''
return ""
@register.tag
def get_permissions(parser, token):
"""
Retrieves all permissions associated with the given obj and user
and assigns the result to a context variable.
Syntax::
{% get_permissions obj %}
@ -265,15 +282,17 @@ def get_permissions(parser, token):
{% get_permissions obj for request.user as "my_permissions" %}
"""
return PermissionsForObjectNode.handle_token(parser, token, approved=True,
name='"permissions"')
return PermissionsForObjectNode.handle_token(
parser, token, approved=True, name='"permissions"'
)
@register.tag
def get_permission_requests(parser, token):
"""
Retrieves all permissions requests associated with the given obj and user
and assigns the result to a context variable.
Syntax::
{% get_permission_requests obj %}
@ -285,22 +304,22 @@ def get_permission_requests(parser, token):
{% get_permission_requests obj for request.user as "my_permissions" %}
"""
return PermissionsForObjectNode.handle_token(parser, token,
approved=False,
name='"permission_requests"')
return PermissionsForObjectNode.handle_token(
parser, token, approved=False, name='"permission_requests"'
)
class PermissionForObjectNode(ResolverNode):
@classmethod
def handle_token(cls, parser, token, approved, name):
bits = token.contents.split()
tag_name = bits[0]
kwargs = {
'perm': cls.next_bit_for(bits, tag_name),
'user': cls.next_bit_for(bits, 'for'),
'objs': cls.next_bit_for(bits, 'and'),
'var_name': cls.next_bit_for(bits, 'as', name),
'approved': approved,
"perm": cls.next_bit_for(bits, tag_name),
"user": cls.next_bit_for(bits, "for"),
"objs": cls.next_bit_for(bits, "and"),
"var_name": cls.next_bit_for(bits, "as", name),
"approved": approved,
}
return cls(**kwargs)
@ -312,7 +331,7 @@ class PermissionForObjectNode(ResolverNode):
self.approved = approved
def render(self, context):
objs = [self.resolve(obj, context) for obj in self.objs.split(',')]
objs = [self.resolve(obj, context) for obj in self.objs.split(",")]
var_name = self.resolve(self.var_name, context)
perm = self.resolve(self.perm, context)
user = self.resolve(self.user, context)
@ -329,7 +348,8 @@ class PermissionForObjectNode(ResolverNode):
if granted:
break
context[var_name] = granted
return ''
return ""
@register.tag
def get_permission(parser, token):
@ -341,9 +361,11 @@ def get_permission(parser, token):
{% get_permission PERMISSION_LABEL.CHECK_NAME for USER and *OBJS [as VARNAME] %}
{% get_permission "poll_permission.change_poll" for request.user and poll as "is_allowed" %}
{% get_permission "poll_permission.change_poll" for request.user and poll,second_poll as "is_allowed" %}
{% get_permission "poll_permission.change_poll"
for request.user and poll as "is_allowed" %}
{% get_permission "poll_permission.change_poll"
for request.user and poll,second_poll as "is_allowed" %}
{% if is_allowed %}
I've got ze power to change ze pollllllzzz. Muahahaa.
{% else %}
@ -351,9 +373,10 @@ def get_permission(parser, token):
{% endif %}
"""
return PermissionForObjectNode.handle_token(parser, token,
approved=True,
name='"permission"')
return PermissionForObjectNode.handle_token(
parser, token, approved=True, name='"permission"'
)
@register.tag
def get_permission_request(parser, token):
@ -365,9 +388,11 @@ def get_permission_request(parser, token):
{% get_permission_request PERMISSION_LABEL.CHECK_NAME for USER and *OBJS [as VARNAME] %}
{% get_permission_request "poll_permission.change_poll" for request.user and poll as "asked_for_permissio" %}
{% get_permission_request "poll_permission.change_poll" for request.user and poll,second_poll as "asked_for_permissio" %}
{% get_permission_request "poll_permission.change_poll"
for request.user and poll as "asked_for_permissio" %}
{% get_permission_request "poll_permission.change_poll"
for request.user and poll,second_poll as "asked_for_permissio" %}
{% if asked_for_permissio %}
Dude, you already asked for permission!
{% else %}
@ -375,59 +400,67 @@ def get_permission_request(parser, token):
{% endif %}
"""
return PermissionForObjectNode.handle_token(parser, token,
approved=False,
name='"permission_request"')
return PermissionForObjectNode.handle_token(
parser, token, approved=False, name='"permission_request"'
)
def base_link(context, perm, view_name):
return {
'next': context['request'].build_absolute_uri(),
'url': reverse(view_name, kwargs={'permission_pk': perm.pk,}),
"next": context["request"].build_absolute_uri(),
"url": reverse(view_name, kwargs={"permission_pk": perm.pk}),
}
@register.inclusion_tag('authority/permission_delete_link.html', takes_context=True)
@register.inclusion_tag("authority/permission_delete_link.html", takes_context=True)
def permission_delete_link(context, perm):
"""
Renders a html link to the delete view of the given permission. Returns
no content if the request-user has no permission to delete foreign
permissions.
"""
user = context['request'].user
if user.is_authenticated():
if user.has_perm('authority.delete_foreign_permissions') \
or user.pk == perm.creator.pk:
return base_link(context, perm, 'authority-delete-permission')
return {'url': None}
user = context["request"].user
if user.is_authenticated:
if (
user.has_perm("authority.delete_foreign_permissions")
or user.pk == perm.creator.pk
):
return base_link(context, perm, "authority-delete-permission")
return {"url": None}
@register.inclusion_tag('authority/permission_request_delete_link.html', takes_context=True)
@register.inclusion_tag(
"authority/permission_request_delete_link.html", takes_context=True
)
def permission_request_delete_link(context, perm):
"""
Renders a html link to the delete view of the given permission request.
Renders a html link to the delete view of the given permission request.
Returns no content if the request-user has no permission to delete foreign
permissions.
"""
user = context['request'].user
if user.is_authenticated():
link_kwargs = base_link(context, perm,
'authority-delete-permission-request')
if user.has_perm('authority.delete_permission'):
link_kwargs['is_requestor'] = False
user = context["request"].user
if user.is_authenticated:
link_kwargs = base_link(context, perm, "authority-delete-permission-request")
if user.has_perm("authority.delete_permission"):
link_kwargs["is_requestor"] = False
return link_kwargs
if not perm.approved and perm.user == user:
link_kwargs['is_requestor'] = True
link_kwargs["is_requestor"] = True
return link_kwargs
return {'url': None}
return {"url": None}
@register.inclusion_tag('authority/permission_request_approve_link.html', takes_context=True)
@register.inclusion_tag(
"authority/permission_request_approve_link.html", takes_context=True
)
def permission_request_approve_link(context, perm):
"""
Renders a html link to the approve view of the given permission request.
Renders a html link to the approve view of the given permission request.
Returns no content if the request-user has no permission to delete foreign
permissions.
"""
user = context['request'].user
if user.is_authenticated():
if user.has_perm('authority.approve_permission_requests'):
return base_link(context, perm,
'authority-approve-permission-request')
return {'url': None}
user = context["request"].user
if user.is_authenticated:
if user.has_perm("authority.approve_permission_requests"):
return base_link(context, perm, "authority-approve-permission-request")
return {"url": None}

View file

@ -1,36 +1,40 @@
from django import VERSION
from django.conf import settings
from django.contrib.auth.models import Permission as DjangoPermission
from django.contrib.auth.models import Group
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission as DjangoPermission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Q
from django.test import TestCase
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
import authority
from authority import permissions
from authority.models import Permission
from authority.exceptions import NotAModel, UnsavedModelInstance
from authority.utils import User
# Load the form
from authority.forms import UserPermissionForm # noqa
if VERSION >= (1, 5):
FIXTURES = ['tests_custom.json']
QUERY = Q(email="jezdez@github.com")
else:
FIXTURES = ['tests.json']
QUERY = Q(username="jezdez")
User = get_user_model()
FIXTURES = ["tests_custom.json"]
QUERY = Q(email="jezdez@github.com")
class UserPermission(permissions.BasePermission):
checks = ('browse',)
label = 'user_permission'
authority.register(User, UserPermission)
checks = ("browse",)
label = "user_permission"
authority.utils.register(User, UserPermission)
class GroupPermission(permissions.BasePermission):
checks = ('browse',)
label = 'group_permission'
authority.register(Group, GroupPermission)
checks = ("browse",)
label = "group_permission"
authority.utils.register(Group, GroupPermission)
class DjangoPermissionChecksTestCase(TestCase):
@ -45,6 +49,7 @@ class DjangoPermissionChecksTestCase(TestCase):
This permissions are given in the test case and not in the fixture, for
later reference.
"""
fixtures = FIXTURES
def setUp(self):
@ -58,7 +63,7 @@ class DjangoPermissionChecksTestCase(TestCase):
def test_add(self):
# setup
perm = DjangoPermission.objects.get(codename='add_user')
perm = DjangoPermission.objects.get(codename="add_user")
self.user.user_permissions.add(perm)
# test
@ -68,8 +73,8 @@ class DjangoPermissionChecksTestCase(TestCase):
perm = Permission(
user=self.user,
content_object=self.user,
codename='user_permission.delete_user',
approved=True
codename="user_permission.delete_user",
approved=True,
)
perm.save()
@ -85,24 +90,61 @@ class AssignBehaviourTest(TestCase):
- permission delete_user for him (test_delete),
- all existing codenames permissions: a/b/c/d (test_all),
"""
fixtures = FIXTURES
def setUp(self):
self.user = User.objects.get(QUERY)
self.group1, _ = Group.objects.get_or_create(name="Test Group 1")
self.group2, _ = Group.objects.get_or_create(name="Test Group 2")
self.group3, _ = Group.objects.get_or_create(name="Test Group 2")
self.check = UserPermission(self.user)
def test_add(self):
result = self.check.assign(check='add_user')
result = self.check.assign(check="add_user")
self.assertTrue(isinstance(result[0], DjangoPermission))
self.assertTrue(self.check.add_user())
def test_delete(self):
result = self.check.assign(
content_object=self.user,
check='delete_user',
def test_assign_to_group(self):
result = UserPermission(group=self.group1).assign(
check="delete_user", content_object=self.user
)
self.assertIsInstance(result, list)
self.assertIsInstance(result[0], Permission)
self.assertTrue(UserPermission(group=self.group1).delete_user(self.user))
def test_assign_to_group_does_not_overwrite_other_group_permission(self):
UserPermission(group=self.group1).assign(
check="delete_user", content_object=self.user
)
UserPermission(group=self.group2).assign(
check="delete_user", content_object=self.user
)
self.assertTrue(UserPermission(group=self.group2).delete_user(self.user))
self.assertTrue(UserPermission(group=self.group1).delete_user(self.user))
def test_assign_to_group_does_not_fail_when_two_group_perms_exist(self):
for group in self.group1, self.group2:
perm = Permission(
group=group,
content_object=self.user,
codename="user_permission.delete_user",
approved=True,
)
perm.save()
try:
UserPermission(group=self.group3).assign(
check="delete_user", content_object=self.user
)
except MultipleObjectsReturned:
self.fail("assign() should not have raised this exception")
def test_delete(self):
result = self.check.assign(content_object=self.user, check="delete_user",)
self.assertTrue(isinstance(result[0], Permission))
self.assertFalse(self.check.delete_user())
self.assertTrue(self.check.delete_user(self.user))
@ -122,6 +164,7 @@ class GenericAssignBehaviourTest(TestCase):
- permission add (test_add),
- permission delete for him (test_delete),
"""
fixtures = FIXTURES
def setUp(self):
@ -129,16 +172,14 @@ class GenericAssignBehaviourTest(TestCase):
self.check = UserPermission(self.user)
def test_add(self):
result = self.check.assign(check='add', generic=True)
result = self.check.assign(check="add", generic=True)
self.assertTrue(isinstance(result[0], DjangoPermission))
self.assertTrue(self.check.add_user())
def test_delete(self):
result = self.check.assign(
content_object=self.user,
check='delete',
generic=True,
content_object=self.user, check="delete", generic=True,
)
self.assertTrue(isinstance(result[0], Permission))
@ -151,6 +192,7 @@ class AssignExceptionsTest(TestCase):
Tests that exceptions are thrown if assign() was called with inconsistent
arguments.
"""
fixtures = FIXTURES
def setUp(self):
@ -166,7 +208,7 @@ class AssignExceptionsTest(TestCase):
def test_not_model_content_object(self):
try:
self.check.assign(content_object='fail')
self.check.assign(content_object="fail")
except NotAModel:
return True
self.fail()
@ -176,6 +218,7 @@ class SmartCachingTestCase(TestCase):
"""
The base test case for all tests that have to do with smart caching.
"""
fixtures = FIXTURES
def setUp(self):
@ -200,21 +243,14 @@ class SmartCachingTestCase(TestCase):
# This is what the old, pre-cache system would check to see if a user
# had a given permission.
return Permission.objects.user_permissions(
self.user,
'foo',
self.user,
approved=True,
check_groups=True,
self.user, "foo", self.user, approved=True, check_groups=True,
)
def _old_group_permission_check(self):
# This is what the old, pre-cache system would check to see if a user
# had a given permission.
return Permission.objects.group_permissions(
self.group,
'foo',
self.group,
approved=True,
self.group, "foo", self.group, approved=True,
)
@ -239,20 +275,13 @@ class PerformanceTest(SmartCachingTestCase):
for _ in range(5):
# Need to assert it so the query actually gets executed.
assert not self.user_check.has_user_perms(
'foo',
self.user,
True,
False,
"foo", self.user, True, False,
)
def test_group_has_perms(self):
with self.assertNumQueries(2):
for _ in range(5):
assert not self.group_check.has_group_perms(
'foo',
self.group,
True,
)
assert not self.group_check.has_group_perms("foo", self.group, True,)
def test_has_user_perms_check_group(self):
# Regardless of the number groups permissions, it should only take one
@ -260,10 +289,7 @@ class PerformanceTest(SmartCachingTestCase):
# Content type and permissions (2 queries)
with self.assertNumQueries(3):
self.user_check.has_user_perms(
'foo',
self.user,
approved=True,
check_groups=True,
"foo", self.user, approved=True, check_groups=True,
)
def test_invalidate_user_permissions_cache(self):
@ -275,10 +301,7 @@ class PerformanceTest(SmartCachingTestCase):
with self.assertNumQueries(6):
for _ in range(5):
assert not self.user_check.has_user_perms(
'foo',
self.user,
True,
False,
"foo", self.user, True, False,
)
# Invalidate the cache to show that a query will be generated when
@ -289,10 +312,7 @@ class PerformanceTest(SmartCachingTestCase):
# One query to re generate the cache.
for _ in range(5):
assert not self.user_check.has_user_perms(
'foo',
self.user,
True,
False,
"foo", self.user, True, False,
)
def test_invalidate_group_permissions_cache(self):
@ -302,11 +322,7 @@ class PerformanceTest(SmartCachingTestCase):
# will need to do one query to get content type and one to get
with self.assertNumQueries(4):
for _ in range(5):
assert not self.group_check.has_group_perms(
'foo',
self.group,
True,
)
assert not self.group_check.has_group_perms("foo", self.group, True,)
# Invalidate the cache to show that a query will be generated when
# checking perms again.
@ -315,18 +331,14 @@ class PerformanceTest(SmartCachingTestCase):
# One query to re generate the cache.
for _ in range(5):
assert not self.group_check.has_group_perms(
'foo',
self.group,
True,
)
assert not self.group_check.has_group_perms("foo", self.group, True,)
def test_has_user_perms_check_group_multiple(self):
# Create a permission with just a group.
Permission.objects.create(
content_type=Permission.objects.get_content_type(User),
object_id=self.user.pk,
codename='foo',
codename="foo",
group=self.group,
approved=True,
)
@ -335,17 +347,17 @@ class PerformanceTest(SmartCachingTestCase):
# Check the number of queries.
with self.assertNumQueries(2):
assert self.user_check.has_user_perms('foo', self.user, True, True)
assert self.user_check.has_user_perms("foo", self.user, True, True)
# Create a second group.
new_group = Group.objects.create(name='new_group')
new_group = Group.objects.create(name="new_group")
new_group.user_set.add(self.user)
# Create a permission object for it.
Permission.objects.create(
content_type=Permission.objects.get_content_type(User),
object_id=self.user.pk,
codename='foo',
codename="foo",
group=new_group,
approved=True,
)
@ -354,7 +366,7 @@ class PerformanceTest(SmartCachingTestCase):
# Make sure it is the same number of queries.
with self.assertNumQueries(2):
assert self.user_check.has_user_perms('foo', self.user, True, True)
assert self.user_check.has_user_perms("foo", self.user, True, True)
class GroupPermissionCacheTestCase(SmartCachingTestCase):
@ -369,10 +381,7 @@ class GroupPermissionCacheTestCase(SmartCachingTestCase):
# Use the new cached user perms to show that the user does not have the
# perms.
can_foo_with_group = self.user_check.has_user_perms(
'foo',
self.user,
approved=True,
check_groups=True,
"foo", self.user, approved=True, check_groups=True,
)
self.assertFalse(can_foo_with_group)
@ -380,7 +389,7 @@ class GroupPermissionCacheTestCase(SmartCachingTestCase):
perm = Permission.objects.create(
content_type=Permission.objects.get_content_type(User),
object_id=self.user.pk,
codename='foo',
codename="foo",
group=self.group,
approved=True,
)
@ -392,10 +401,7 @@ class GroupPermissionCacheTestCase(SmartCachingTestCase):
# Invalidate the cache.
self.user_check.invalidate_permissions_cache()
can_foo_with_group = self.user_check.has_user_perms(
'foo',
self.user,
approved=True,
check_groups=True,
"foo", self.user, approved=True, check_groups=True,
)
self.assertTrue(can_foo_with_group)
@ -403,9 +409,7 @@ class GroupPermissionCacheTestCase(SmartCachingTestCase):
# Make sure calling has_user_perms on a permission that does not have a
# user does not throw any errors.
can_foo_with_group = self.group_check.has_group_perms(
'foo',
self.user,
approved=True,
"foo", self.user, approved=True,
)
self.assertFalse(can_foo_with_group)
@ -416,7 +420,7 @@ class GroupPermissionCacheTestCase(SmartCachingTestCase):
perm = Permission.objects.create(
content_type=Permission.objects.get_content_type(Group),
object_id=self.group.pk,
codename='foo',
codename="foo",
group=self.group,
approved=True,
)
@ -429,8 +433,21 @@ class GroupPermissionCacheTestCase(SmartCachingTestCase):
self.group_check.invalidate_permissions_cache()
can_foo_with_group = self.group_check.has_group_perms(
'foo',
self.group,
approved=True,
"foo", self.group, approved=True,
)
self.assertTrue(can_foo_with_group)
class AddPermissionTestCase(TestCase):
def test_add_permission_permission_denied_is_403(self):
user = get_user_model().objects.create(username="foo", email="foo@example.com",)
user.set_password("pw")
user.save()
assert self.client.login(username="foo@example.com", password="pw")
url = reverse(
"authority-add-permission-request",
kwargs={"app_label": "foo", "module_name": "Bar", "pk": 1,},
)
r = self.client.get(url)
self.assertEqual(r.status_code, 403)

View file

@ -1,31 +1,40 @@
try:
from django.conf.urls import *
except ImportError: # django < 1.4
from django.conf.urls.defaults import *
urlpatterns = patterns('authority.views',
url(r'^permission/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$',
view='add_permission',
name="authority-add-permission",
kwargs={'approved': True}
),
url(r'^permission/delete/(?P<permission_pk>\d+)/$',
view='delete_permission',
name="authority-delete-permission",
kwargs={'approved': True}
),
url(r'^request/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$',
view='add_permission',
name="authority-add-permission-request",
kwargs={'approved': False}
),
url(r'^request/approve/(?P<permission_pk>\d+)/$',
view='approve_permission_request',
name="authority-approve-permission-request"
),
url(r'^request/delete/(?P<permission_pk>\d+)/$',
view='delete_permission',
name="authority-delete-permission-request",
kwargs={'approved': False}
),
from django.conf.urls import url
from authority.views import (
add_permission,
delete_permission,
approve_permission_request,
delete_permission,
)
urlpatterns = [
url(
r"^permission/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$",
view=add_permission,
name="authority-add-permission",
kwargs={"approved": True},
),
url(
r"^permission/delete/(?P<permission_pk>\d+)/$",
view=delete_permission,
name="authority-delete-permission",
kwargs={"approved": True},
),
url(
r"^request/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$",
view=add_permission,
name="authority-add-permission-request",
kwargs={"approved": False},
),
url(
r"^request/approve/(?P<permission_pk>\d+)/$",
view=approve_permission_request,
name="authority-approve-permission-request",
),
url(
r"^request/delete/(?P<permission_pk>\d+)/$",
view=delete_permission,
name="authority-delete-permission-request",
kwargs={"approved": False},
),
]

View file

@ -1,11 +1,7 @@
from django.contrib import auth
def get_user_class():
if hasattr(auth, "get_user_model"):
return auth.get_user_model()
else:
return auth.models.User
User = get_user_class()
from authority.sites import (
site,
get_check,
get_choices_for,
register,
unregister,
) # noqa

View file

@ -1,10 +1,7 @@
from datetime import datetime
from django.shortcuts import render_to_response, get_object_or_404
from django.views.decorators.http import require_POST
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.db.models.loading import get_model
from django.apps import apps
from django.utils.translation import ugettext as _
from django.template.context import RequestContext
from django.template import loader
from django.contrib.auth.decorators import login_required
@ -12,85 +9,108 @@ from authority.models import Permission
from authority.forms import UserPermissionForm
from authority.templatetags.permissions import url_for_obj
def get_next(request, obj=None):
next = request.REQUEST.get('next')
next = request.REQUEST.get("next")
if not next:
if obj and hasattr(obj, 'get_absolute_url'):
if obj and hasattr(obj, "get_absolute_url"):
next = obj.get_absolute_url()
else:
next = '/'
next = "/"
return next
@login_required
def add_permission(request, app_label, module_name, pk, approved=False,
template_name = 'authority/permission_form.html',
extra_context={}, form_class=UserPermissionForm):
codename = request.POST.get('codename', None)
model = get_model(app_label, module_name)
if model is None:
def add_permission(
request,
app_label,
module_name,
pk,
approved=False,
template_name="authority/permission_form.html",
extra_context=None,
form_class=UserPermissionForm,
):
codename = request.POST.get("codename", None)
try:
model = apps.get_model(app_label, module_name)
except LookupError:
return permission_denied(request)
obj = get_object_or_404(model, pk=pk)
next = get_next(request, obj)
if approved:
if not request.user.has_perm('authority.add_permission'):
if not request.user.has_perm("authority.add_permission"):
return HttpResponseRedirect(
url_for_obj('authority-add-permission-request', obj))
view_name = 'authority-add-permission'
url_for_obj("authority-add-permission-request", obj)
)
view_name = "authority-add-permission"
else:
view_name = 'authority-add-permission-request'
if request.method == 'POST':
view_name = "authority-add-permission-request"
if request.method == "POST":
if codename is None:
return HttpResponseForbidden(next)
form = form_class(data=request.POST, obj=obj, approved=approved,
perm=codename, initial=dict(codename=codename))
form = form_class(
data=request.POST,
obj=obj,
approved=approved,
perm=codename,
initial=dict(codename=codename),
)
if not approved:
# Limit permission request to current user
form.data['user'] = request.user
form.data["user"] = request.user
if form.is_valid():
permission = form.save(request)
form.save(request)
request.user.message_set.create(
message=_('You added a permission request.'))
message=_("You added a permission request.")
)
return HttpResponseRedirect(next)
else:
form = form_class(obj=obj, approved=approved, perm=codename,
initial=dict(codename=codename))
form = form_class(
obj=obj, approved=approved, perm=codename, initial=dict(codename=codename)
)
context = {
'form': form,
'form_url': url_for_obj(view_name, obj),
'next': next,
'perm': codename,
'approved': approved,
"form": form,
"form_url": url_for_obj(view_name, obj),
"next": next,
"perm": codename,
"approved": approved,
}
context.update(extra_context)
return render_to_response(template_name, context,
context_instance=RequestContext(request))
if extra_context:
context.update(extra_context)
return render(request, template_name, context)
@login_required
def approve_permission_request(request, permission_pk):
requested_permission = get_object_or_404(Permission, pk=permission_pk)
if request.user.has_perm('authority.approve_permission_requests'):
if request.user.has_perm("authority.approve_permission_requests"):
requested_permission.approve(request.user)
request.user.message_set.create(
message=_('You approved the permission request.'))
message=_("You approved the permission request.")
)
next = get_next(request, requested_permission)
return HttpResponseRedirect(next)
@login_required
def delete_permission(request, permission_pk, approved):
permission = get_object_or_404(Permission, pk=permission_pk,
approved=approved)
if (request.user.has_perm('authority.delete_foreign_permissions')
or request.user == permission.creator):
permission = get_object_or_404(Permission, pk=permission_pk, approved=approved)
if (
request.user.has_perm("authority.delete_foreign_permissions")
or request.user == permission.creator
):
permission.delete()
if approved:
msg = _('You removed the permission.')
msg = _("You removed the permission.")
else:
msg = _('You removed the permission request.')
msg = _("You removed the permission request.")
request.user.message_set.create(message=msg)
next = get_next(request)
return HttpResponseRedirect(next)
def permission_denied(request, template_name=None, extra_context={}):
def permission_denied(request, template_name=None, extra_context=None):
"""
Default 403 handler.
@ -100,10 +120,14 @@ def permission_denied(request, template_name=None, extra_context={}):
The path of the requested URL (e.g., '/app/pages/bad_page/')
"""
if template_name is None:
template_name = ('403.html', 'authority/403.html')
template_name = ("403.html", "authority/403.html")
context = {
'request_path': request.path,
"request_path": request.path,
}
context.update(extra_context)
return HttpResponseForbidden(loader.render_to_string(template_name, context,
context_instance=RequestContext(request)))
if extra_context:
context.update(extra_context)
return HttpResponseForbidden(
loader.render_to_string(
template_name=template_name, context=context, request=request,
)
)

View file

@ -4,6 +4,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
generic_script = """
<script type="text/javascript">
function showGenericRelatedObjectLookupPopup(ct_select, triggering_link, url_base) {
@ -17,6 +18,7 @@ function showGenericRelatedObjectLookupPopup(ct_select, triggering_link, url_bas
</script>
"""
class GenericForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
def __init__(self, ct_field, cts=[], attrs=None):
self.ct_field = ct_field
@ -26,28 +28,57 @@ class GenericForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
def render(self, name, value, attrs=None):
if attrs is None:
attrs = {}
related_url = '../../../'
related_url = "../../../"
params = self.url_parameters()
if params:
url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.iteritems()])
url = "?" + "&amp;".join(["%s=%s" % (k, v) for k, v in params.iteritems()])
else:
url = ''
if 'class' not in attrs:
attrs['class'] = 'vForeignKeyRawIdAdminField'
url = ""
if "class" not in attrs:
attrs["class"] = "vForeignKeyRawIdAdminField"
output = [forms.TextInput.render(self, name, value, attrs)]
output.append("""%(generic_script)s
<a href="%(related)s%(url)s" class="related-lookup" id="lookup_id_%(name)s" onclick="return showGenericRelatedObjectLookupPopup(document.getElementById('id_%(ct_field)s'), this, '%(related)s%(url)s');"> """
% {'generic_script': generic_script, 'related': related_url, 'url': url, 'name': name, 'ct_field': self.ct_field})
output.append('<img src="%s/admin/img/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.STATIC_URL, _('Lookup')))
output.append(
"""%(generic_script)s
<a href="%(related)s%(url)s"
class="related-lookup"
id="lookup_id_%(name)s"
onclick="return showGenericRelatedObjectLookupPopup(
document.getElementById('id_%(ct_field)s'), this, '%(related)s%(url)s');">
"""
% {
"generic_script": generic_script,
"related": related_url,
"url": url,
"name": name,
"ct_field": self.ct_field,
}
)
output.append(
'<img src="%s/admin/img/selector-search.gif" width="16" height="16" alt="%s" /></a>'
% (settings.STATIC_URL, _("Lookup"))
)
from django.contrib.contenttypes.models import ContentType
content_types = """
<script type="text/javascript">
var content_types = new Array();
%s
</script>
""" % ('\n'.join(["content_types[%s] = '%s/%s/';" % (ContentType.objects.get_for_model(ct).id, ct._meta.app_label, ct._meta.object_name.lower()) for ct in self.cts]))
return mark_safe(u''.join(output) + content_types)
""" % (
"\n".join(
[
"content_types[%s] = '%s/%s/';"
% (
ContentType.objects.get_for_model(ct).id,
ct._meta.app_label,
ct._meta.object_name.lower(),
)
for ct in self.cts
]
)
)
return mark_safe(u"".join(output) + content_types)
def url_parameters(self):
return {}

View file

@ -1,84 +0,0 @@
##############################################################################
#
# Copyright (c) 2006 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Bootstrap a buildout-based project
Simply run this script in a directory containing a buildout.cfg.
The script accepts buildout command-line options, so you can
use the -c option to specify an alternate configuration file.
$Id$
"""
import os, shutil, sys, tempfile, urllib2
tmpeggs = tempfile.mkdtemp()
is_jython = sys.platform.startswith('java')
try:
import pkg_resources
except ImportError:
ez = {}
exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
).read() in ez
ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
import pkg_resources
if sys.platform == 'win32':
def quote(c):
if ' ' in c:
return '"%s"' % c # work around spawn lamosity on windows
else:
return c
else:
def quote (c):
return c
cmd = 'from setuptools.command.easy_install import main; main()'
ws = pkg_resources.working_set
if len(sys.argv) > 2 and sys.argv[1] == '--version':
VERSION = ' == %s' % sys.argv[2]
args = sys.argv[3:] + ['bootstrap']
else:
VERSION = ''
args = sys.argv[1:] + ['bootstrap']
if is_jython:
import subprocess
assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
quote(tmpeggs), 'zc.buildout' + VERSION],
env=dict(os.environ,
PYTHONPATH=
ws.find(pkg_resources.Requirement.parse('setuptools')).location
),
).wait() == 0
else:
assert os.spawnle(
os.P_WAIT, sys.executable, quote (sys.executable),
'-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION,
dict(os.environ,
PYTHONPATH=
ws.find(pkg_resources.Requirement.parse('setuptools')).location
),
) == 0
ws.add_entry(tmpeggs)
ws.require('zc.buildout' + VERSION)
import zc.buildout.buildout
zc.buildout.buildout.main(args)
shutil.rmtree(tmpeggs)

View file

@ -1,27 +0,0 @@
[buildout]
parts =
python
django-1.0.X
django-1.1.X
develop = .
eggs =
django-authority
[python]
recipe = zc.recipe.egg
interpreter = python
eggs = ${buildout:eggs}
[django-1.0.X]
recipe = djangorecipe
version = 1.0.4
projectegg = example
settings = development
eggs = ${buildout:eggs}
[django-1.1.X]
recipe = djangorecipe
version = 1.1.1
projectegg = example
settings = development
eggs = ${buildout:eggs}

View file

@ -28,7 +28,7 @@ Syntax::
Example::
{% if hasperm "poll_permission.change_poll" request.user %}
{% ifhasperm "poll_permission.change_poll" request.user %}
lalala
{% else %}
meh

View file

@ -11,13 +11,13 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
from pkg_resources import get_distribution
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.append(os.path.abspath('.'))
#sys.path.append(os.path.join(os.path.dirname(__file__), '../src/'))
# sys.path.append(os.path.abspath('.'))
# sys.path.append(os.path.join(os.path.dirname(__file__), '../src/'))
# -- General configuration -----------------------------------------------------
@ -26,118 +26,117 @@ import sys, os
extensions = []
# Add any paths that contain templates here, relative to this directory.
#templates_path = ['.templates']
# templates_path = ['.templates']
# The suffix of source filenames.
source_suffix = '.txt'
source_suffix = ".txt"
# The encoding of source files.
#source_encoding = 'utf-8'
# source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'django-authority'
copyright = u'2009, the django-authority team'
project = u"django-authority"
copyright = u"2009-2020, Jannis Leidel"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.8'
# The full version, including alpha/beta/rc tags.
release = '0.8dev'
release = get_distribution("django-authority").version
# The short X.Y version.
version = ".".join(release.split(".")[:2])
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['build']
exclude_trees = ["build"]
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'nature'
html_theme = "nature"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['.theme']
html_theme_path = [".theme"]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = '.static/logo.png'
html_logo = ".static/logo.png"
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = 'favicon.png'
html_favicon = "favicon.png"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['.static']
html_static_path = [".static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
html_use_modindex = True
@ -146,7 +145,7 @@ html_use_modindex = True
html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = False
@ -154,43 +153,48 @@ html_show_sourcelink = False
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'django-authoritydoc'
htmlhelp_basename = "django-authoritydoc"
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'django-authority.tex', u'django-authority Documentation',
u'The django-authority team', 'manual'),
(
"index",
"django-authority.tex",
u"django-authority Documentation",
u"The django-authority team",
"manual",
),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
# latex_use_modindex = True

View file

@ -23,12 +23,12 @@ Let's start with an example::
import authority
from authority import permissions
from django.contrib.flatpages.models import Flatpage
from django.contrib.flatpages.models import FlatPage
class FlatpagePermission(permissions.BasePermission):
label = 'flatpage_permission'
authority.register(Flatpage, FlatpagePermission)
authority.register(FlatPage, FlatpagePermission)
Let's have a look at the code above. First of, if you want to create a new
permission you have to subclass it from the BasePermission class::
@ -45,14 +45,14 @@ Next, you need to name this permission using the ``label`` attribute::
And finally you need to register the permission with the pool of all other
permissions::
authority.register(Flatpage, FlatpagePermission)
authority.register(FlatPage, FlatpagePermission)
The syntax of this is simple::
authority.register(<model>, <permission_class>)
While this is not much code, you already wrapped Django's basic permissions
(add_flatpage, change_flatpage, delete_flatpage) for the model ``Flatpage``
(add_flatpage, change_flatpage, delete_flatpage) for the model ``FlatPage``
and you are ready to use it within your templates or code:
.. note:: See `Django's basic permissions`_ how Django creates this permissions for you.

View file

@ -36,7 +36,7 @@ A custom permission is a simple method of the permission class::
Note that we first added the name of your custom permission to the ``checks``
attribute, like in :ref:`create-per-object-permission`::
checks = (my_custom_check',)
checks = ('my_custom_check',)
The permission itself is a simple function that accepts an arbitrary number of
arguments. A permission class should always return a boolean whether the

View file

@ -27,9 +27,9 @@ Development version
===================
The latest development version is located on it's `Github account`_. You
can checkout the package using the Mercurial_ scm::
can checkout the package using the Git_ scm::
git clone https://github.com/jezdez/django-authority.git
git clone https://github.com/jazzband/django-authority
Then install it manually::
@ -39,5 +39,5 @@ Then install it manually::
.. warning:: The development version is not fully tested and may contain
bugs, so we prefer to use the latest package from pypi.
.. _Github account: https://github.com/jezdez/django-authority/
.. _Mercurial: http://www.selenic.com/mercurial/
.. _Github account: https://github.com/jazzband/django-authority/
.. _Git: http://gitscm.org/

View file

@ -14,4 +14,4 @@ For more specific issues and bug reports please use the `issue tracker`_ on
django-authority's Github page.
.. _google group: http://groups.google.com/group/django-authority
.. _issue tracker: https://github.com/jezdez/django-authority/issues/
.. _issue tracker: https://github.com/jazzband/django-authority/issues/

View file

@ -1,4 +1,4 @@
from example.settings import *
DEBUG=True
TEMPLATE_DEBUG=DEBUG
DEBUG = True
TEMPLATE_DEBUG = DEBUG

View file

@ -3,5 +3,6 @@ from django.utils.translation import ugettext_lazy as _
from authority.forms import UserPermissionForm
class SpecialUserPermissionForm(UserPermissionForm):
user = forms.CharField(label=_('Special user'), widget=forms.Textarea())
user = forms.CharField(label=_("Special user"), widget=forms.Textarea())

View file

@ -4,10 +4,11 @@ from django.utils.translation import ugettext_lazy as _
import authority
from authority.permissions import BasePermission
class FlatPagePermission(BasePermission):
"""
This class contains a bunch of checks:
1. the default checks 'add_flatpage', 'browse_flatpage',
'change_flatpage' and 'delete_flatpage'
2. the custom checks:
@ -40,13 +41,16 @@ class FlatPagePermission(BasePermission):
{% endifhasperm %}
"""
label = 'flatpage_permission'
checks = ('review', 'top_secret')
label = "flatpage_permission"
checks = ("review", "top_secret")
def top_secret(self, flatpage=None, lala=None):
if flatpage and flatpage.registration_required:
return self.browse_flatpage(obj=flatpage)
return False
top_secret.short_description=_('Is allowed to see top secret flatpages')
authority.register(FlatPage, FlatPagePermission)
top_secret.short_description = _("Is allowed to see top secret flatpages")
authority.sites.register(FlatPage, FlatPagePermission)

View file

@ -1,23 +0,0 @@
"""
This file demonstrates two different styles of tests (one doctest and one
unittest). These will both pass when you run "manage.py test".
Replace these with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.failUnlessEqual(1 + 1, 2)
__test__ = {"doctest": """
Another way to test that 1 + 1 is equal to 2.
>>> 1 + 1 == 2
True
"""}

View file

@ -8,8 +8,9 @@ from authority.decorators import permission_required, permission_required_or_403
# @permission_required('flatpage_permission.top_secret',
# (FlatPage, 'url__contains', 'url'), (FlatPage, 'url__contains', 'lala'))
# use this to return a 403 page:
@permission_required_or_403('flatpage_permission.top_secret',
(FlatPage, 'url__contains', 'url'), 'lala')
@permission_required_or_403(
"flatpage_permission.top_secret", (FlatPage, "url__contains", "url"), "lala"
)
def top_secret(request, url, lala=None):
"""
A wrapping view that performs the permission check given in the decorator

View file

@ -7,13 +7,16 @@ from django.core.management import execute_from_command_line
try:
import settings as settings_mod # Assumed to be in the same directory.
except ImportError:
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.stderr.write(
"Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n"
% __file__
)
sys.exit(1)
sys.path.insert(0, settings_mod.PROJECT_ROOT)
sys.path.insert(0, settings_mod.PROJECT_ROOT + '/../')
sys.path.insert(0, settings_mod.PROJECT_ROOT + "/../")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", 'example.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
if __name__ == "__main__":
execute_from_command_line(sys.argv)

View file

@ -1,2 +1 @@
from example.settings import *

View file

@ -11,86 +11,87 @@ ADMINS = (
MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(PROJECT_ROOT, 'example.db'),
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(PROJECT_ROOT, "example.db"),
"USER": "",
"PASSWORD": "",
"HOST": "",
"PORT": "",
"TEST": {"NAME": ":memory:", "ENGINE": "django.db.backends.sqlite3",},
}
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache",}}
TIME_ZONE = 'America/Chicago'
TIME_ZONE = "America/Chicago"
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media')
MEDIA_ROOT = os.path.join(PROJECT_ROOT, "media")
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = '/media/'
MEDIA_URL = "/media/"
# Don't share this with anybody.
SECRET_KEY = 'ljlv2lb2d&)#by6th=!v=03-c^(o4lop92i@z4b3f1&ve0yx6d'
SECRET_KEY = "ljlv2lb2d&)#by6th=!v=03-c^(o4lop92i@z4b3f1&ve0yx6d"
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
#'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
MIDDLEWARE = (
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
# 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
)
INTERNAL_IPS = ('127.0.0.1',)
INTERNAL_IPS = ("127.0.0.1",)
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.core.context_processors.request',
)
TEMPLATE_CONTEXT_PROCESSORS = ()
ROOT_URLCONF = 'example.urls'
ROOT_URLCONF = "example.urls"
SITE_ID = 1
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.flatpages',
'django.contrib.admin',
'authority',
'example.exampleapp',
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.flatpages",
"django.contrib.messages",
"django.contrib.admin",
"authority",
"example.exampleapp",
)
if VERSION >= (1, 5):
INSTALLED_APPS = INSTALLED_APPS + ('example.users',)
AUTH_USER_MODEL = 'users.User'
INSTALLED_APPS = INSTALLED_APPS + ("example.users",)
AUTH_USER_MODEL = "users.User"
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.load_template_source',
'django.template.loaders.app_directories.load_template_source',
)
TEMPLATE_DIRS = (
os.path.join(PROJECT_ROOT, "templates"),
)
# Use local_settings.py for things to override privately
try:
from local_settings import * # noqa
except ImportError:
pass
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
# insert your TEMPLATE_DIRS here
],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
# Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
# list if you haven't customized them:
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
],
},
},
]

View file

@ -1,39 +1,44 @@
try:
from django.conf.urls import patterns, include, handler500, url
except ImportError: # django < 1.4
from django.conf.urls.defaults import patterns, include, handler500, url
import django.contrib.auth.views
from django.conf.urls import include, handler500, url
from django.conf import settings
from django.contrib import admin
import authority
admin.autodiscover()
authority.autodiscover()
handler500 # Pyflakes
import authority.views
import authority.urls
import example.exampleapp.views
from exampleapp.forms import SpecialUserPermissionForm
urlpatterns = patterns('',
(r'^admin/(.*)', admin.site.root),
#('^admin/', include(admin.site.urls)),
url(r'^authority/permission/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$',
view='authority.views.add_permission',
authority.autodiscover()
handler500 # flake8
urlpatterns = (
url(
r"^authority/permission/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$", # noqa
view=authority.views.add_permission,
name="authority-add-permission",
kwargs={'approved': True, 'form_class': SpecialUserPermissionForm}
kwargs={"approved": True, "form_class": SpecialUserPermissionForm},
),
url(r'^request/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$',
view='authority.views.add_permission',
url(
r"^request/add/(?P<app_label>[\w\-]+)/(?P<module_name>[\w\-]+)/(?P<pk>\d+)/$", # noqa
view=authority.views.add_permission,
name="authority-add-permission-request",
kwargs={'approved': False, 'form_class': SpecialUserPermissionForm}
kwargs={"approved": False, "form_class": SpecialUserPermissionForm},
),
url(r"^authority/", include(authority.urls)),
url(r"^accounts/login/$", django.contrib.auth.views.LoginView.as_view()),
url(
r"^(?P<url>[\/0-9A-Za-z]+)$",
example.exampleapp.views.top_secret,
{"lala": "oh yeah!"},
),
(r'^authority/', include('authority.urls')),
(r'^accounts/login/$', 'django.contrib.auth.views.login'),
url(r'^(?P<url>[\/0-9A-Za-z]+)$', 'example.exampleapp.views.top_secret', {'lala': 'oh yeah!'}),
)
if settings.DEBUG:
urlpatterns += patterns('',
(r'^media/(?P<path>.*)$', 'django.views.static.serve', {
'document_root': settings.MEDIA_ROOT,
}),
urlpatterns += (
url(
r"^media/(?P<path>.*)$",
django.views.static.serve,
{"document_root": settings.MEDIA_ROOT,},
),
)

View file

@ -1,5 +1,6 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from example.users.User
from example.users.models import User
admin.site.register(User, UserAdmin)

View file

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-07-12 16:01
from __future__ import unicode_literals
import django.contrib.auth.models
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0008_alter_user_username_max_length"),
]
operations = [
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
("username", models.CharField(max_length=100)),
("first_name", models.CharField(max_length=50)),
("last_name", models.CharField(max_length=50)),
("email", models.EmailField(max_length=254, unique=True)),
("greeting_message", models.TextField()),
("is_staff", models.BooleanField(default=False)),
("is_active", models.BooleanField(default=True)),
(
"date_joined",
models.DateTimeField(default=django.utils.timezone.now),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.Group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.Permission",
verbose_name="user permissions",
),
),
],
options={"abstract": False,},
managers=[("objects", django.contrib.auth.models.UserManager()),],
),
]

View file

View file

@ -1,12 +1,14 @@
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.contrib.auth.models import UserManager
from django.db import models
from django.utils import timezone
class User(AbstractBaseUser, PermissionsMixin):
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'last_name']
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["first_name", "last_name"]
username = models.CharField(max_length=100)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField(unique=True)
@ -14,3 +16,5 @@ class User(AbstractBaseUser, PermissionsMixin):
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(default=timezone.now)
objects = UserManager()

View file

@ -1,16 +0,0 @@
BEGIN;
-- Application: authority
-- Model: Permission
ALTER TABLE `authority_permission`
ADD `date_requested` DATETIME;
ALTER TABLE `authority_permission`
ADD `approved` BOOL;
ALTER TABLE `authority_permission`
ADD `date_approved` DATETIME;
ALTER TABLE `authority_permission`
MODIFY `object_id` INTEGER UNSIGNED;
UPDATE `authority_permission`
SET `approved` = TRUE;
UPDATE `authority_permission`
SET `date_approved` = NOW();
COMMIT;

View file

@ -1,14 +0,0 @@
BEGIN;
-- Application: authority
-- Model: Permission
ALTER TABLE "authority_permission"
ADD "date_requested" timestamp with time zone;
ALTER TABLE "authority_permission"
ADD "approved" boolean;
ALTER TABLE "authority_permission"
ADD "date_approved" timestamp with time zone;
UPDATE "authority_permission"
SET "approved" = True;
UPDATE "authority_permission"
SET "date_approved" = NOW();
COMMIT;

View file

@ -1,14 +0,0 @@
BEGIN;
-- Application: authority
-- Model: Permission
ALTER TABLE "authority_permission"
ADD "date_requested" datetime;
ALTER TABLE "authority_permission"
ADD "approved" bool;
ALTER TABLE "authority_permission"
ADD "date_approved" datetime;
UPDATE "authority_permission"
SET "approved" = 1;
UPDATE "authority_permission"
SET "date_approved" = DATETIME("NOW");
COMMIT;

View file

@ -8,3 +8,6 @@ all_files = 1
[upload_docs]
upload-dir = docs/build/html
[wheel]
universal = 1

View file

@ -5,39 +5,38 @@ from setuptools import setup, find_packages
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
name='django-authority',
version='0.8',
name="django-authority",
use_scm_version=True,
description=(
"A Django app that provides generic per-object-permissions "
"for Django's auth app."
),
long_description=read('README.rst'),
author='Jannis Leidel',
author_email='jannis@leidel.info',
license='BSD',
url='https://github.com/jezdez/django-authority/',
packages=find_packages(),
long_description=read("README.rst"),
long_description_content_type="text/x-rst",
author="Jannis Leidel",
author_email="jannis@leidel.info",
license="BSD",
url="https://github.com/jazzband/django-authority/",
packages=find_packages(exclude=("example", "example.*")),
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.3',
'Framework :: Django',
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Framework :: Django",
],
install_requires=['django'],
package_data = {
'authority': [
'fixtures/test.json',
'templates/authority/*.html',
'templates/admin/edit_inline/action_tabular.html',
'templates/admin/permission_change_form.html',
]
},
install_requires=["django"],
setup_requires=["setuptools_scm"],
include_package_data=True,
zip_safe=False,
)

39
tox.ini Normal file
View file

@ -0,0 +1,39 @@
[tox]
skipsdist = True
usedevelop = True
minversion = 1.8
envlist =
py27-dj111
py37-dj{111,22}
{py36,py37,py38}-dj{30,31}
py37-check
[testenv]
usedevelop = true
commands =
coverage run -a example/manage.py test authority exampleapp
coverage report
coverage xml
deps =
coverage
dj111: Django>=1.11,<2.0
dj22: Django>=2.2,<2.3
dj30: Django>=3.0,<3.1
dj31: Django>=3.1,<3.2
[testenv:py37-check]
deps =
twine
wheel
commands =
python setup.py sdist bdist_wheel
twine check dist/*
[gh-actions]
python =
2.7: py27
3.6: py36
3.7: py37
3.8: py38