Compare commits

...

105 commits
0.7.0 ... main

Author SHA1 Message Date
Jannis Leidel
7770da8a49
Merge pull request #479 from jazzband/jazzband/sync/default
Jazzband: Synced file(s) with jazzband/.github
2021-10-22 17:46:32 +02:00
jazzband-bot
bbfa3daa0c Jazzband: Created local 'CODE_OF_CONDUCT.md' from remote 'CODE_OF_CONDUCT.md' 2021-10-21 14:33:45 +00:00
Asif Saif Uddin
933489a491 remove python 2 style codes 2021-10-17 17:09:23 +06:00
Asif Saif Uddin
2a5156005b remove python 2 style codes 2021-10-17 17:09:23 +06:00
Asif Saif Uddin
ca28d88c88 remove python 2 style super call from utils 2021-10-17 17:09:23 +06:00
Asif Saif Uddin
9227439625 remove python 2 style super call and explicit object inheritance from viewmixins 2021-10-17 17:09:23 +06:00
Asif Saif Uddin
78112a85bd remove python 2 style super call and explicit object inheritance 2021-10-17 17:09:23 +06:00
Asif Saif Uddin
951c7e5bcf
update python 3 (#477)
* update python 3

* modify django-filter to 2.4.0
2021-10-17 14:21:02 +06:00
Asif Saif Uddin
5608ae91ea add django 3.2 on the matrix and remove 3.0 as EOL 2021-10-17 13:24:17 +06:00
Jannis Leidel
8cee4de75d
Merge pull request #475 from jazzband/master-to-main
Rename Django's dev branch to main.
2021-03-09 18:36:51 +01:00
Jannis Leidel
f5ee0d12c3
Fix for recent Django. 2021-03-09 13:14:40 +01:00
Jannis Leidel
245b5911e6
More Django fixes. 2021-03-09 12:44:05 +01:00
Jannis Leidel
39c6d80b7b
Don't run tests against Django main on 3.7. 2021-03-09 12:38:08 +01:00
Jannis Leidel
2c6e5f004a
Fix compat issue. 2021-03-09 12:33:11 +01:00
Jannis Leidel
f893867652
Rename own develop branch to main. 2021-03-09 12:29:02 +01:00
Jannis Leidel
9025d59308
Rename Django's dev branch to main.
More information: https://groups.google.com/g/django-developers/c/tctDuKUGosc/
Refs: https://github.com/django/django/pull/14048
2021-03-09 12:25:05 +01:00
Jannis Leidel
5fa2670643
Fix badges. 2020-11-25 21:12:03 +01:00
Jannis Leidel
acc36d9e3e
Merge pull request #474 from jazzband/gha
Migrate to GitHub Actions.
2020-11-25 20:40:15 +01:00
Jannis Leidel
d76590de4c
Enable Jazzband releases. 2020-11-25 20:32:29 +01:00
Jannis Leidel
eba98f9cae
Add coverage tracking. 2020-11-25 20:25:26 +01:00
Jannis Leidel
65c7f21730
Initial GitHub Actions test workflow. 2020-11-25 20:20:05 +01:00
Kamil Gałuszka
c59a19f336 feat: upgrade Django and Python to supported versions 2020-11-21 22:16:43 +06:00
Tim Gates
49f569cc6c
docs: Fix simple typo, taht -> that (#471)
There is a small typo in djadmin2/filters.py.

Should read `that` rather than `taht`.
2020-03-04 16:06:19 +01:00
Asif Saif Uddin
08867d7e13
updated django rest framework 2019-03-18 01:21:42 +06:00
Asif Saif Uddin
c724c332c6 django-filter rest backend global 2018-11-02 21:27:04 +06:00
Asif Saif Uddin
fbd1ab5931 static tag 2018-11-02 21:27:04 +06:00
Asif Saif Uddin
74788d96ff inlineformset deprecation warning fixed 2018-11-02 21:27:04 +06:00
Asif Saif Uddin
09ed44fe56 regex depr 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
5425e37524 static 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
ccdbfdd013 settings changes 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
93a7a213df settings changes 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
673b0f8eb6 change setenv 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
fbd3caf0e3 admin tests only 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
77ed1eb117 disable example apps testing 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
58f38b55cd static 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
bfe2a8b207 pinned django-filter to 1.1.0 for now 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
26727eb2a5 update required packages 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
ddc1e39cea stop flake8 check on travis 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
7ea3d7fc28 updated requirements version 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
439f250423 updated travis matrix 2018-11-02 15:44:25 +06:00
Asif Saif Uddin
69d0387b6d repalce staticfiles with static 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
32f5c93c84 added paginate_by attr to example blogListView 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
6473fbea4d fixed typo 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
c056ad816e django-filters to installed apps 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
769bd37a9a django-filters widget changes revert 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
9d7286b5aa django-filters widget 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
a7be23c4d9 django-filters 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
3c20e75a0d django-filters 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
bdfcc5a65b revert 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
18c8c95443 class based Login and logout views 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
563a330db1 fixed deprecation warning in core 2018-10-23 18:27:41 +06:00
Asif Saif Uddin
78e18161dc
Merge pull request #464 from jazzband/master
fixed static
2018-10-23 15:00:53 +06:00
Asif Saif Uddin
85a5d3b291 fixed deprecation warning in example blog filters test 2018-10-23 14:48:35 +06:00
Asif Saif Uddin
e8bde774cf fixed deprecation warning in views tests 2018-10-23 14:43:45 +06:00
Asif Saif Uddin
f60fb8530c fixed deprecation warning in utils tests 2018-10-23 14:42:16 +06:00
Asif Saif Uddin
18d0d18edd fixed deprecation warning in types tests 2018-10-23 14:41:54 +06:00
Asif Saif Uddin
c4462f79d2 fixed deprecation warning in core tests 2018-10-23 14:37:39 +06:00
Asif Saif Uddin
ffb7f2d595 fixed deprecation warning in admin2tags tests 2018-10-23 14:35:06 +06:00
Asif Saif Uddin
09545a663e fixed pep8 errors 2018-10-23 14:33:21 +06:00
Asif Saif Uddin
0cbaf7f089 fixed deprecation warning it test_actions 2018-10-23 14:32:57 +06:00
Asif Saif Uddin
b285164379 fixed pep8 errors 2018-10-23 13:42:09 +06:00
Asif Saif Uddin
7a754a3469 fixed missing imports 2018-10-23 13:38:37 +06:00
Asif Saif Uddin
2f1959b43e fixed static 2018-10-23 13:05:17 +06:00
Asif Saif Uddin (Auvi)
2473fa3069
fix deprecation warnings (#463)
* fix deprecation warning

* fix deprecation warning

* fix deprecation warning of django filter

* fix deprecation warning of django filter widget

* undo widget

* fixed field_name deprecation warning

* fixed pagination error

* fixed pagination error
2018-09-17 13:58:30 +06:00
Asif Saif Uddin (Auvi)
f212393331
Merge pull request #462 from auvipy/up
Update
2018-09-03 16:12:45 +06:00
Asif Saif Uddin
8636f6ce98 template dir settings 2018-09-03 14:54:37 +06:00
Asif Saif Uddin
6ae2354d2a removed SessionAuthenticationMiddleware 2018-09-03 14:42:29 +06:00
Asif Saif Uddin
8169c11e31 fixed url erros 2018-09-03 14:25:27 +06:00
Asif Saif Uddin
2091f9649d fixed is_authenticated error 2018-09-03 14:13:10 +06:00
Asif Saif Uddin
6263f2a340 fixed lint error 2018-09-03 13:45:27 +06:00
Asif Saif Uddin
647260dfb7 fixed migrations on_delete argument warning 2018-09-03 13:38:39 +06:00
Asif Saif Uddin
5f0c3c323d fixed url includes deprecation warning 2018-09-03 13:37:47 +06:00
Asif Saif Uddin
75c7d4180a updated tox with newer versions 2018-09-03 11:38:31 +06:00
Asif Saif Uddin
68eb9c7c59 updated urls reverse import 2018-09-03 11:31:48 +06:00
Asif Saif Uddin
394b04c2f9 updated authors 2018-09-03 11:20:28 +06:00
Asif Saif Uddin
5d4ba598d7 updated travis matrix 2018-09-03 11:15:12 +06:00
Asif Saif Uddin
d44f64f3fd update django-fillter to 1.1 2018-09-03 11:11:41 +06:00
Asif Saif Uddin
26fc9ff235 update readme 2018-09-03 11:11:14 +06:00
Asif Saif Uddin
cfe9c1ffa2 update setup.py 2018-09-03 11:07:32 +06:00
Asif Saifuddin Auvi
606118cb04
Merge pull request #458 from auvipy/up
start work to support 1.11 and 2.0
2018-05-11 02:02:17 +06:00
Asif Saifuddin Auvi
27421081d4 fixed reverse reverse_lazy import errors 2018-05-11 01:23:23 +06:00
Asif Saifuddin Auvi
90713245bc added .pytest_cache to gitignore 2018-05-11 01:21:51 +06:00
Asif Saifuddin Auvi
b01ff0b2f6 style fix 2018-05-11 01:17:37 +06:00
Asif Saifuddin Auvi
f86f5a1ee2 add on_delete positional arg to related fields 2018-05-11 00:49:17 +06:00
Asif Saifuddin Auvi
be3a67b9c5 add on_delete positional arg to related fields 2018-05-11 00:36:28 +06:00
Asif Saifuddin Auvi
4a715d95c0 add on_delete positional arg to related fields 2018-05-11 00:34:03 +06:00
Asif Saifuddin Auvi
33b41fa311 add on_delete positional arg to related fields 2018-05-10 23:58:25 +06:00
Asif Saifuddin Auvi
e3822dee39 fix pep8 errors 2018-05-10 23:23:37 +06:00
Asif Saifuddin Auvi
e7200b7b65 update MIDDLEWARE settings 2018-05-10 23:23:18 +06:00
Asif Saifuddin Auvi
cce430242c fix pep8 errors 2018-05-10 23:15:01 +06:00
Asif Saifuddin Auvi
2c73396391 fix typo in tox 2018-05-10 23:05:17 +06:00
Asif Saifuddin Auvi
c8a4153a4d revert 2018-05-10 22:54:47 +06:00
Asif Saifuddin Auvi
9db0b200ff remove init.py from from tests folder to fix import issues 2018-05-10 22:42:49 +06:00
Asif Saifuddin Auvi
aac3e1062b modify travis 2018-05-10 22:31:01 +06:00
Asif Saifuddin Auvi
c34a0dab19 fixed flake8 errors 2018-05-10 22:22:00 +06:00
Asif Saifuddin Auvi
cf822c745b updated travis 2018-05-10 22:15:00 +06:00
Asif Saifuddin Auvi
28702ef165 dropped dj 1.8 & 1.9 from tox and added 2.0 2018-05-10 22:09:01 +06:00
Asif Saifuddin Auvi
18fae8d5ed update dependencies 2018-05-10 21:58:56 +06:00
Asif Saifuddin Auvi
dcb43cdb02 added dj1.11 in tox 2017-03-20 17:10:42 +06:00
Asif Saifuddin Auvi
4aa18f3a8d added dj1.11 to travis 2017-03-20 16:33:00 +06:00
Asif Saifuddin Auvi
f8bd4be24c added dj1.11 to travis 2017-03-20 16:32:33 +06:00
Asif Saifuddin Auvi
1054fb25fd added dj1.11 to tox 2017-03-20 16:27:50 +06:00
Asif Saifuddin Auvi
2b5c180792 update 0.7.1 tag for release 2017-01-10 23:43:07 +06:00
Andrews Medina
a95e211d65 add compatibility with last version of deps (#454) 2016-11-29 23:16:39 +06:00
Asif Saifuddin Auvi
b20ec951c3 update minor versions (#453)
and check what fails to fix them incrementally for 0.7.1 release
2016-11-27 17:37:29 +06:00
82 changed files with 755 additions and 679 deletions

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-admin2'
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-admin2/upload

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

@ -0,0 +1,47 @@
name: Test
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
max-parallel: 5
matrix:
python-version: [3.5, 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 }}

1
.gitignore vendored
View file

@ -65,3 +65,4 @@ media
.idea/ .idea/
.cache .cache
.pytest_cache

View file

@ -1,46 +0,0 @@
sudo: false
language: python
cache:
directories:
- "~/.cache/pip"
python:
- '2.7'
- '3.3'
- '3.4'
- '3.5'
env:
- DJANGO=1.8
- DJANGO=1.9
- DJANGO=1.10
- DJANGO=master
matrix:
exclude:
- python: '3.3'
env: DJANGO=1.9
- python: '3.3'
env: DJANGO=1.10
- python: '3.3'
env: DJANGO=master
allow_failures:
- python: '2.7'
env: DJANGO=master
- python: '3.4'
env: DJANGO=master
- python: '3.5'
env: DJANGO=master
install:
- pip install tox
script:
- tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-$DJANGO
deploy:
provider: pypi
user: jazzband
distributions: sdist bdist_wheel
password:
secure: UQh5R++1zOsJm31oIL0w4YsIyP1A83kv/EdpwhuXqs8HZB+Mpo5h2ylu/qVioTaiXvYEVfO1myY57du7YvC+u6oOxD8tyduBOyZMJXa15+/kdVbvjlRWJ/c8wGgmjlOAun209g9WoTAMVX8t1+thHEJqWxx07nlnPCAJSmdms+w=
on:
tags: true
repo: jazzband/django-admin2
# only do the PyPI release for exactly one scenario of the test matrix:
condition: "$DJANGO = 1.9"
python: 2.7

View file

@ -4,6 +4,7 @@ CONTRIBUTORS
Project Lead Project Lead
------------ ------------
* Asif Saif Uddin (@auvipy)
* Daniel Greenfeld (@pydanny / <pydanny@gmail.com>) * Daniel Greenfeld (@pydanny / <pydanny@gmail.com>)
Translation Managers Translation Managers
@ -11,6 +12,7 @@ Translation Managers
* Henri Colas (@NotSqrt) * Henri Colas (@NotSqrt)
* Danilo Bargen (@dbrgn) * Danilo Bargen (@dbrgn)
* Asif Saif Uddin (@auvipy)
Developers Developers
---------- ----------

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/

View file

@ -5,14 +5,12 @@ django-admin2
.. image:: https://jazzband.co/static/img/badge.svg .. image:: https://jazzband.co/static/img/badge.svg
:target: https://jazzband.co/ :target: https://jazzband.co/
:alt: Jazzband :alt: Jazzband
.. image:: https://travis-ci.org/jazzband/django-admin2.png .. image:: https://github.com/jazzband/django-admin2/workflows/Test/badge.svg
:alt: Build Status :target: https://github.com/jazzband/django-admin2/actions
:target: https://travis-ci.org/jazzband/django-admin2 :alt: GitHub Actions
.. image:: https://coveralls.io/repos/github/jazzband/django-admin2/badge.svg?branch=develop .. image:: https://codecov.io/gh/jazzband/django-admin2/branch/main/graph/badge.svg?token=PcC594rhI4
:alt: Coverage Status :target: https://codecov.io/gh/jazzband/django-admin2
:target: https://coveralls.io/github/jazzband/django-admin2?branch=develop :alt: Code coverage
.. image:: https://badges.gitter.im/Join Chat.svg
:target: https://gitter.im/pydanny/django-admin2?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
One of the most useful parts of ``django.contrib.admin`` is the ability to One of the most useful parts of ``django.contrib.admin`` is the ability to
configure various views that touch and alter data. django-admin2 is a complete configure various views that touch and alter data. django-admin2 is a complete
@ -32,23 +30,23 @@ Features
Screenshots Screenshots
=========== ===========
.. image:: https://github.com/jazzband/django-admin2/raw/develop/screenshots/Site_administration.png .. image:: https://github.com/jazzband/django-admin2/raw/main/screenshots/Site_administration.png
:width: 722px :width: 722px
:alt: Site administration :alt: Site administration
:align: center :align: center
:target: https://github.com/jazzband/django-admin2/raw/develop/screenshots/Site_administration.png :target: https://github.com/jazzband/django-admin2/raw/main/screenshots/Site_administration.png
.. image:: https://github.com/jazzband/django-admin2/raw/develop/screenshots/Select_user.png .. image:: https://github.com/jazzband/django-admin2/raw/main/screenshots/Select_user.png
:width: 722px :width: 722px
:alt: Select user :alt: Select user
:align: center :align: center
:target: https://github.com/jazzband/django-admin2/raw/develop/screenshots/Select_user.png :target: https://github.com/jazzband/django-admin2/raw/main/screenshots/Select_user.png
Requirements Requirements
============ ============
* Django 1.7+ * Django 2.2+
* Python 2.7+ or Python 3.3+ * Python 3.5+
* django-braces_ * django-braces_
* django-extra-views_ * django-extra-views_
* django-rest-framework_ * django-rest-framework_

View file

@ -1,8 +1,4 @@
# -*- coding: utf-8 -*- __version__ = '0.7.1'
from __future__ import division, absolute_import, unicode_literals
__version__ = '0.7.0'
__author__ = 'Daniel Greenfeld & Contributors' __author__ = 'Daniel Greenfeld & Contributors'

View file

@ -1,12 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.contrib import messages from django.contrib import messages
from django.db import router from django.db import router
from django.utils.encoding import force_text from django.utils.encoding import force_str
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from django.utils.translation import ugettext_lazy, ungettext, pgettext_lazy from django.utils.translation import gettext_lazy, ngettext, pgettext_lazy
from django.views.generic import TemplateView from django.views.generic import TemplateView
from . import permissions, utils from . import permissions, utils
@ -26,7 +23,7 @@ class BaseListAction(Admin2ModelMixin, TemplateView):
permission_classes = (permissions.IsStaffPermission,) permission_classes = (permissions.IsStaffPermission,)
empty_message = ugettext_lazy( empty_message = gettext_lazy(
'Items must be selected in order to perform actions ' 'Items must be selected in order to perform actions '
'on them. No items have been changed.' 'on them. No items have been changed.'
) )
@ -50,9 +47,9 @@ class BaseListAction(Admin2ModelMixin, TemplateView):
objects_name = options.verbose_name objects_name = options.verbose_name
else: else:
objects_name = options.verbose_name_plural objects_name = options.verbose_name_plural
self.objects_name = force_text(objects_name) self.objects_name = force_str(objects_name)
super(BaseListAction, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def get_queryset(self): def get_queryset(self):
""" Replaced `get_queryset` from `Admin2ModelMixin`""" """ Replaced `get_queryset` from `Admin2ModelMixin`"""
@ -88,12 +85,12 @@ class BaseListAction(Admin2ModelMixin, TemplateView):
""" Utility method when you want to display nested objects """ Utility method when you want to display nested objects
(such as during a bulk update/delete) (such as during a bulk update/delete)
""" """
context = super(BaseListAction, self).get_context_data() context = super().get_context_data()
def _format_callback(obj): def _format_callback(obj):
opts = utils.model_options(obj) opts = utils.model_options(obj)
return '%s: %s' % (force_text(capfirst(opts.verbose_name)), return '%s: %s' % (force_str(capfirst(opts.verbose_name)),
force_text(obj)) force_str(obj))
using = router.db_for_write(self.model) using = router.db_for_write(self.model)
@ -111,7 +108,7 @@ class BaseListAction(Admin2ModelMixin, TemplateView):
def get(self, request): def get(self, request):
if self.item_count > 0: if self.item_count > 0:
return super(BaseListAction, self).get(request) return super().get(request)
message = _(self.empty_message) message = _(self.empty_message)
messages.add_message(request, messages.INFO, message) messages.add_message(request, messages.INFO, message)
@ -122,7 +119,7 @@ class BaseListAction(Admin2ModelMixin, TemplateView):
if self.process_queryset() is None: if self.process_queryset() is None:
# objects_name should already be pluralized, see __init__ # objects_name should already be pluralized, see __init__
message = ungettext( message = ngettext(
self.success_message, self.success_message,
self.success_message_plural, self.success_message_plural,
self.item_count self.item_count
@ -146,7 +143,7 @@ class DeleteSelectedAction(BaseListAction):
default_template_name = "actions/delete_selected_confirmation.html" default_template_name = "actions/delete_selected_confirmation.html"
description = ugettext_lazy("Delete selected items") description = gettext_lazy("Delete selected items")
success_message = pgettext_lazy( success_message = pgettext_lazy(
'singular form', 'singular form',
@ -163,7 +160,7 @@ class DeleteSelectedAction(BaseListAction):
def post(self, request): def post(self, request):
if request.POST.get('confirmed'): if request.POST.get('confirmed'):
super(DeleteSelectedAction, self).post(request) super().post(request)
else: else:
# The user has not confirmed that they want to delete the # The user has not confirmed that they want to delete the
# objects, so render a template asking for their confirmation. # objects, so render a template asking for their confirmation.

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
@ -17,6 +14,7 @@ class GroupSerializer(Admin2APISerializer):
class Meta: class Meta:
model = Group model = Group
fields = '__all__'
class GroupAdmin2(ModelAdmin2): class GroupAdmin2(ModelAdmin2):

View file

@ -1,10 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework import fields, generics, serializers from rest_framework import fields, generics, serializers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse as drf_reverse
from rest_framework.views import APIView from rest_framework.views import APIView
from . import utils from . import utils
@ -20,7 +17,7 @@ class Admin2APISerializer(serializers.HyperlinkedModelSerializer):
__unicode__ = fields.ReadOnlyField(source='__str__') __unicode__ = fields.ReadOnlyField(source='__str__')
def get_extra_kwargs(self): def get_extra_kwargs(self):
extra_kwargs = super(Admin2APISerializer, self).get_extra_kwargs() extra_kwargs = super().get_extra_kwargs()
extra_kwargs.update({ extra_kwargs.update({
'url': {'view_name': self._get_default_view_name(self.Meta.model)} 'url': {'view_name': self._get_default_view_name(self.Meta.model)}
}) })
@ -56,9 +53,10 @@ class Admin2APIMixin(Admin2Mixin):
class Meta: class Meta:
model = model_class model = model_class
fields = '__all__'
return ModelAPISerilizer return ModelAPISerilizer
return super(Admin2APIMixin, self).get_serializer_class() return super().get_serializer_class()
class IndexAPIView(Admin2APIMixin, APIView): class IndexAPIView(Admin2APIMixin, APIView):
@ -75,7 +73,7 @@ class IndexAPIView(Admin2APIMixin, APIView):
'app_label': model_options.app_label, 'app_label': model_options.app_label,
'model_name': model_options.object_name.lower(), 'model_name': model_options.object_name.lower(),
} }
model_url = reverse( model_url = drf_reverse(
'%(current_app)s:%(app_label)s_%(model_name)s_api_list' % opts, '%(current_app)s:%(app_label)s_%(model_name)s_api_list' % opts,
request=self.request, request=self.request,
format=self.kwargs.get('format')) format=self.kwargs.get('format'))

View file

@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djadmin2.permissions import create_view_permissions from djadmin2.permissions import create_view_permissions
@ -10,5 +10,7 @@ class Djadmin2Config(AppConfig):
verbose_name = _("Django Admin2") verbose_name = _("Django Admin2")
def ready(self): def ready(self):
post_migrate.connect(create_view_permissions, post_migrate.connect(
dispatch_uid="django-admin2.djadmin2.permissions.create_view_permissions") create_view_permissions,
dispatch_uid="django-admin2.djadmin2.permissions.create_view_permissions"
)

View file

@ -78,6 +78,7 @@ def _create_multiwidget(widget_class, copy_attributes=(), init_arguments=()):
return multiwidget return multiwidget
return create_new_multiwidget return create_new_multiwidget
# this dictionary keeps a mapping from django's widget classes to a callable # this dictionary keeps a mapping from django's widget classes to a callable
# that will accept an instance of this class. It will return a new instance of # that will accept an instance of this class. It will return a new instance of
# a corresponding floppyforms widget, with the same semantics -- all relevant # a corresponding floppyforms widget, with the same semantics -- all relevant

View file

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*-:
""" """
WARNING: This file about to undergo major refactoring by @pydanny per WARNING: This file about to undergo major refactoring by @pydanny per
Issue #99. Issue #99.
""" """
from __future__ import division, absolute_import, unicode_literals
from importlib import import_module from importlib import import_module
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.urls import re_path
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from . import apiviews from . import apiviews
@ -17,7 +14,7 @@ from . import utils
from . import views from . import views
class Admin2(object): class Admin2:
""" """
The base Admin2 object. The base Admin2 object.
It keeps a registry of all registered Models and collects the urls of their It keeps a registry of all registered Models and collects the urls of their
@ -26,12 +23,13 @@ class Admin2(object):
It also provides an index view that serves as an entry point to the It also provides an index view that serves as an entry point to the
admin site. admin site.
""" """
index_view = views.IndexView index_view = views.IndexView
login_view = views.LoginView login_view = views.LoginView
app_index_view = views.AppIndexView app_index_view = views.AppIndexView
api_index_view = apiviews.IndexAPIView api_index_view = apiviews.IndexAPIView
def __init__(self, name='admin2'): def __init__(self, name="admin2"):
self.registry = {} self.registry = {}
self.apps = {} self.apps = {}
self.app_verbose_names = {} self.app_verbose_names = {}
@ -51,7 +49,8 @@ class Admin2(object):
""" """
if model in self.registry: if model in self.registry:
raise ImproperlyConfigured( raise ImproperlyConfigured(
'%s is already registered in django-admin2' % model) "%s is already registered in django-admin2" % model
)
if not model_admin: if not model_admin:
model_admin = types.ModelAdmin2 model_admin = types.ModelAdmin2
self.registry[model] = model_admin(model, admin=self, **kwargs) self.registry[model] = model_admin(model, admin=self, **kwargs)
@ -74,7 +73,8 @@ class Admin2(object):
del self.registry[model] del self.registry[model]
except KeyError: except KeyError:
raise ImproperlyConfigured( raise ImproperlyConfigured(
'%s was never registered in django-admin2' % model) "%s was never registered in django-admin2" % model
)
# Remove the model from the apps registry # Remove the model from the apps registry
# Get the app label # Get the app label
@ -96,7 +96,8 @@ class Admin2(object):
""" """
if app_label in self.app_verbose_names: if app_label in self.app_verbose_names:
raise ImproperlyConfigured( raise ImproperlyConfigured(
'%s is already registered in django-admin2' % app_label) "%s is already registered in django-admin2" % app_label
)
self.app_verbose_names[app_label] = app_verbose_name self.app_verbose_names[app_label] = app_verbose_name
@ -112,7 +113,8 @@ class Admin2(object):
del self.app_verbose_names[app_label] del self.app_verbose_names[app_label]
except KeyError: except KeyError:
raise ImproperlyConfigured( raise ImproperlyConfigured(
'%s app label was never registered in django-admin2' % app_label) "%s app label was never registered in django-admin2" % app_label
)
def autodiscover(self): def autodiscover(self):
""" """
@ -123,7 +125,7 @@ class Admin2(object):
try: try:
import_module("%s.admin2" % app_name) import_module("%s.admin2" % app_name)
except ImportError as e: except ImportError as e:
if str(e).startswith("No module named") and 'admin2' in str(e): if str(e).startswith("No module named") and "admin2" in str(e):
continue continue
raise e raise e
@ -135,71 +137,74 @@ class Admin2(object):
for object_admin in self.registry.values(): for object_admin in self.registry.values():
if object_admin.name == name: if object_admin.name == name:
return object_admin return object_admin
raise ValueError( raise ValueError("No object admin found with name {}".format(repr(name)))
u'No object admin found with name {}'.format(repr(name)))
def get_index_kwargs(self): def get_index_kwargs(self):
return { return {
'registry': self.registry, "registry": self.registry,
'app_verbose_names': self.app_verbose_names, "app_verbose_names": self.app_verbose_names,
'apps': self.apps, "apps": self.apps,
'login_view': self.login_view, "login_view": self.login_view,
} }
def get_app_index_kwargs(self): def get_app_index_kwargs(self):
return { return {
'registry': self.registry, "registry": self.registry,
'app_verbose_names': self.app_verbose_names, "app_verbose_names": self.app_verbose_names,
'apps': self.apps, "apps": self.apps,
} }
def get_api_index_kwargs(self): def get_api_index_kwargs(self):
return { return {
'registry': self.registry, "registry": self.registry,
'app_verbose_names': self.app_verbose_names, "app_verbose_names": self.app_verbose_names,
'apps': self.apps, "apps": self.apps,
} }
def get_urls(self): def get_urls(self):
urlpatterns = [ urlpatterns = [
url(regex=r'^$', re_path(
r"^$",
view=self.index_view.as_view(**self.get_index_kwargs()), view=self.index_view.as_view(**self.get_index_kwargs()),
name='dashboard' name="dashboard",
), ),
url(regex='^auth/user/(?P<pk>\d+)/update/password/$', re_path(
r"^auth/user/(?P<pk>\d+)/update/password/$",
view=views.PasswordChangeView.as_view(), view=views.PasswordChangeView.as_view(),
name='password_change' name="password_change",
), ),
url(regex='^password_change_done/$', re_path(
"^password_change_done/$",
view=views.PasswordChangeDoneView.as_view(), view=views.PasswordChangeDoneView.as_view(),
name='password_change_done' name="password_change_done",
), ),
url(regex='^logout/$', re_path("^logout/$", view=views.LogoutView.as_view(), name="logout"),
view=views.LogoutView.as_view(), re_path(
name='logout' r"^(?P<app_label>\w+)/$",
), view=self.app_index_view.as_view(**self.get_app_index_kwargs()),
url(regex=r'^(?P<app_label>\w+)/$', name="app_index",
view=self.app_index_view.as_view( ),
**self.get_app_index_kwargs()), re_path(
name='app_index' r"^api/v0/$",
), view=self.api_index_view.as_view(**self.get_api_index_kwargs()),
url(regex=r'^api/v0/$', name="api_index",
view=self.api_index_view.as_view( ),
**self.get_api_index_kwargs()),
name='api_index'
),
] ]
for model, model_admin in self.registry.items(): for model, model_admin in self.registry.items():
model_options = utils.model_options(model) model_options = utils.model_options(model)
urlpatterns += [ urlpatterns += [
url('^{}/{}/'.format( re_path(
model_options.app_label, "^{}/{}/".format(
model_options.object_name.lower()), model_options.app_label, model_options.object_name.lower()
include(model_admin.urls)), ),
url('^api/v0/{}/{}/'.format( model_admin.urls,
model_options.app_label, ),
model_options.object_name.lower()), re_path(
include(model_admin.api_urls)), "^api/v0/{}/{}/".format(
model_options.app_label, model_options.object_name.lower()
),
model_admin.api_urls,
),
] ]
return urlpatterns return urlpatterns

View file

@ -1,18 +1,14 @@
# -*- coding: utf-8 -*- import collections.abc
from __future__ import division, absolute_import, unicode_literals
import collections
from itertools import chain from itertools import chain
import django_filters import django_filters
from django import forms from django import forms
from django.forms import widgets as django_widgets from django.forms import widgets as django_widgets
from django.forms.utils import flatatt from django.forms.utils import flatatt
from django.utils import six from django.utils.encoding import force_str
from django.utils.encoding import force_text
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy from django.utils.translation import gettext_lazy
from .utils import type_str from .utils import type_str
@ -24,15 +20,16 @@ class NumericDateFilter(django_filters.DateFilter):
class ChoicesAsLinksWidget(django_widgets.Select): class ChoicesAsLinksWidget(django_widgets.Select):
"""Select form widget taht renders links for choices """Select form widget that renders links for choices
instead of select element with options. instead of select element with options.
""" """
def render(self, name, value, attrs=None, choices=()): def render(self, name, value, attrs=None, choices=()):
links = [] links = []
for choice_value, choice_label in chain(self.choices, choices): for choice_value, choice_label in chain(self.choices, choices):
links.append(format_html( links.append(format_html(
LINK_TEMPLATE, LINK_TEMPLATE,
name, choice_value, flatatt(attrs), force_text(choice_label), name, choice_value, flatatt(attrs), force_str(choice_label),
)) ))
return mark_safe(u"<br />".join(links)) return mark_safe(u"<br />".join(links))
@ -44,11 +41,12 @@ class NullBooleanLinksWidget(
def __init__(self, attrs=None, choices=()): def __init__(self, attrs=None, choices=()):
super(ChoicesAsLinksWidget, self).__init__(attrs) super(ChoicesAsLinksWidget, self).__init__(attrs)
self.choices = [ self.choices = [
('1', ugettext_lazy('Unknown')), ('1', gettext_lazy('Unknown')),
('2', ugettext_lazy('Yes')), ('2', gettext_lazy('Yes')),
('3', ugettext_lazy('No')), ('3', gettext_lazy('No')),
] ]
#: Maps `django_filter`'s field filters types to our #: Maps `django_filter`'s field filters types to our
#: custom form widget. #: custom form widget.
FILTER_TYPE_TO_WIDGET = { FILTER_TYPE_TO_WIDGET = {
@ -68,7 +66,7 @@ def build_list_filter(request, model_admin, queryset):
`request.GET` and `queryset`. `request.GET` and `queryset`.
""" """
# if ``list_filter`` is not iterable return it right away # if ``list_filter`` is not iterable return it right away
if not isinstance(model_admin.list_filter, collections.Iterable): if not isinstance(model_admin.list_filter, collections.abc.Iterable):
return model_admin.list_filter( return model_admin.list_filter(
request.GET, request.GET,
queryset=queryset, queryset=queryset,
@ -76,7 +74,7 @@ def build_list_filter(request, model_admin, queryset):
# otherwise build :mod:`django_filters.FilterSet` # otherwise build :mod:`django_filters.FilterSet`
filters = [] filters = []
for field_filter in model_admin.list_filter: for field_filter in model_admin.list_filter:
if isinstance(field_filter, six.string_types): if isinstance(field_filter, str):
filters.append(get_filter_for_field_name( filters.append(get_filter_for_field_name(
queryset.model, queryset.model,
field_filter, field_filter,
@ -85,7 +83,7 @@ def build_list_filter(request, model_admin, queryset):
filters.append(field_filter) filters.append(field_filter)
filterset_dict = {} filterset_dict = {}
for field_filter in filters: for field_filter in filters:
filterset_dict[field_filter.name] = field_filter filterset_dict[field_filter.field_name] = field_filter
fields = list(filterset_dict.keys()) fields = list(filterset_dict.keys())
filterset_dict['Meta'] = type( filterset_dict['Meta'] = type(
type_str('Meta'), type_str('Meta'),
@ -101,18 +99,23 @@ def build_list_filter(request, model_admin, queryset):
def build_date_filter(request, model_admin, queryset, field_name="published_date"): def build_date_filter(request, model_admin, queryset, field_name="published_date"):
filterset_dict = { filterset_dict = {
"year": NumericDateFilter( "year": NumericDateFilter(
name=field_name, field_name=field_name,
lookup_type="year", lookup_expr="year",
), ),
"month": NumericDateFilter( "month": NumericDateFilter(
name=field_name, field_name=field_name,
lookup_type="month", lookup_expr="month",
), ),
"day": NumericDateFilter( "day": NumericDateFilter(
name=field_name, field_name=field_name,
lookup_type="day", lookup_expr="day",
) )
} }
filterset_dict["Meta"] = type(
type_str('Meta'),
(object, ),
{"model": queryset.model, "fields": [field_name]},
)
return type( return type(
type_str('%sDateFilterSet' % queryset.model.__name__), type_str('%sDateFilterSet' % queryset.model.__name__),
@ -128,8 +131,9 @@ def get_filter_for_field_name(model, field_name):
django_filters.filterset.get_model_field(model, field_name,), django_filters.filterset.get_model_field(model, field_name,),
field_name, field_name,
) )
print("EXTRA!!!!")
print(filter_.extra)
filter_.widget = FILTER_TYPE_TO_WIDGET.get( filter_.widget = FILTER_TYPE_TO_WIDGET.get(
filter_.__class__, filter_.__class__
filter_.widget,
) )
return filter_ return filter_

View file

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django import forms from django import forms
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
# Translators : %(username)s will be replaced by the username_field name # Translators : %(username)s will be replaced by the username_field name
@ -59,7 +56,7 @@ class Admin2UserCreationForm(UserCreationForm):
class Admin2UserChangeForm(UserChangeForm): class Admin2UserChangeForm(UserChangeForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Admin2UserChangeForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
print(self.fields['password'].help_text) print(self.fields['password'].help_text)
self.fields['password'].help_text = _("Raw passwords are not stored, so there is no way to see this user's password, but you can change the password using <a href=\"%s\">this form</a>." % self.get_update_password_url()) self.fields['password'].help_text = _("Raw passwords are not stored, so there is no way to see this user's password, but you can change the password using <a href=\"%s\">this form</a>." % self.get_update_password_url())

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
from django.conf import settings from django.conf import settings
@ -22,8 +19,16 @@ class Migration(migrations.Migration):
('object_repr', models.CharField(max_length=200, verbose_name='object repr')), ('object_repr', models.CharField(max_length=200, verbose_name='object repr')),
('action_flag', models.PositiveSmallIntegerField(verbose_name='action flag')), ('action_flag', models.PositiveSmallIntegerField(verbose_name='action flag')),
('change_message', models.TextField(verbose_name='change message', blank=True)), ('change_message', models.TextField(verbose_name='change message', blank=True)),
('content_type', models.ForeignKey(related_name='log_entries', null=True, blank=True, to='contenttypes.ContentType')), ('content_type', models.ForeignKey(
('user', models.ForeignKey(related_name='log_entries', to=settings.AUTH_USER_MODEL)), related_name='log_entries',
null=True,
blank=True,
to='contenttypes.ContentType',
on_delete=models.CASCADE)),
('user', models.ForeignKey(
related_name='log_entries',
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
], ],
options={ options={
'verbose_name': 'log entry', 'verbose_name': 'log entry',

View file

@ -1,14 +1,9 @@
# -*- coding: utf-8 -*-
""" Boilerplate for now, will serve a purpose soon! """ """ Boilerplate for now, will serve a purpose soon! """
from __future__ import division, absolute_import, unicode_literals
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.utils.encoding import force_text from django.utils.encoding import force_str
from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import gettext, gettext_lazy as _
from django.utils.encoding import smart_text
from django.utils.translation import ugettext, ugettext_lazy as _
from .utils import quote from .utils import quote
@ -17,12 +12,11 @@ class LogEntryManager(models.Manager):
def log_action(self, user_id, obj, action_flag, change_message=''): def log_action(self, user_id, obj, action_flag, change_message=''):
content_type_id = ContentType.objects.get_for_model(obj).id content_type_id = ContentType.objects.get_for_model(obj).id
e = self.model(None, None, user_id, content_type_id, e = self.model(None, None, user_id, content_type_id,
smart_text(obj.id), force_text(obj)[:200], force_str(obj.id), force_str(obj)[:200],
action_flag, change_message) action_flag, change_message)
e.save() e.save()
@python_2_unicode_compatible
class LogEntry(models.Model): class LogEntry(models.Model):
ADDITION = 1 ADDITION = 1
CHANGE = 2 CHANGE = 2
@ -30,9 +24,11 @@ class LogEntry(models.Model):
action_time = models.DateTimeField(_('action time'), auto_now=True) action_time = models.DateTimeField(_('action time'), auto_now=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, user = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='log_entries') related_name='log_entries',
on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, blank=True, null=True, content_type = models.ForeignKey(ContentType, blank=True, null=True,
related_name='log_entries') related_name='log_entries',
on_delete=models.CASCADE)
object_id = models.TextField(_('object id'), blank=True, null=True) object_id = models.TextField(_('object id'), blank=True, null=True)
object_repr = models.CharField(_('object repr'), max_length=200) object_repr = models.CharField(_('object repr'), max_length=200)
action_flag = models.PositiveSmallIntegerField(_('action flag')) action_flag = models.PositiveSmallIntegerField(_('action flag'))
@ -46,22 +42,22 @@ class LogEntry(models.Model):
ordering = ('-action_time',) ordering = ('-action_time',)
def __repr__(self): def __repr__(self):
return smart_text(self.action_time) return force_str(self.action_time)
def __str__(self): def __str__(self):
if self.action_flag == self.ADDITION: if self.action_flag == self.ADDITION:
return ugettext('Added "%(object)s".') % { return gettext('Added "%(object)s".') % {
'object': self.object_repr} 'object': self.object_repr}
elif self.action_flag == self.CHANGE: elif self.action_flag == self.CHANGE:
return ugettext('Changed "%(object)s" - %(changes)s') % { return gettext('Changed "%(object)s" - %(changes)s') % {
'object': self.object_repr, 'object': self.object_repr,
'changes': self.change_message, 'changes': self.change_message,
} }
elif self.action_flag == self.DELETION: elif self.action_flag == self.DELETION:
return ugettext('Deleted "%(object)s."') % { return gettext('Deleted "%(object)s."') % {
'object': self.object_repr} 'object': self.object_repr}
return ugettext('LogEntry Object') return gettext('LogEntry Object')
def is_addition(self): def is_addition(self):
return self.action_flag == self.ADDITION return self.action_flag == self.ADDITION

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
""" """
djadmin2's permission handling. The permission classes have the same API as djadmin2's permission handling. The permission classes have the same API as
the permission handling classes of the django-rest-framework. That way, we can the permission handling classes of the django-rest-framework. That way, we can
@ -15,8 +14,6 @@ interface:
The permission classes are then just fancy wrappers of these basic checks of The permission classes are then just fancy wrappers of these basic checks of
which it can hold multiple. which it can hold multiple.
""" """
from __future__ import division, absolute_import, unicode_literals
import logging import logging
import re import re
@ -25,8 +22,7 @@ from django.db.utils import DEFAULT_DB_ALIAS
from django.apps import apps from django.apps import apps
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import router from django.db import router
from django.utils import six from django.utils.encoding import force_str
from django.utils.encoding import python_2_unicode_compatible, force_text
logger = logging.getLogger('djadmin2') logger = logging.getLogger('djadmin2')
@ -35,7 +31,7 @@ def is_authenticated(request, view, obj=None):
''' '''
Checks if the current user is authenticated. Checks if the current user is authenticated.
''' '''
return request.user.is_authenticated() return request.user.is_authenticated
def is_staff(request, view, obj=None): def is_staff(request, view, obj=None):
@ -95,7 +91,7 @@ def model_permission(permission):
return has_permission return has_permission
class BasePermission(object): class BasePermission:
''' '''
Provides a base class with a common API. It implements a compatible Provides a base class with a common API. It implements a compatible
interface to django-rest-framework permission backends. interface to django-rest-framework permission backends.
@ -191,8 +187,7 @@ class ModelDeletePermission(BasePermission):
permissions = (model_permission('{app_label}.delete_{model_name}'),) permissions = (model_permission('{app_label}.delete_{model_name}'),)
@python_2_unicode_compatible class TemplatePermissionChecker:
class TemplatePermissionChecker(object):
''' '''
Can be used in the template like: Can be used in the template like:
@ -259,7 +254,7 @@ class TemplatePermissionChecker(object):
needs an interface beeing implemented like suggested in: needs an interface beeing implemented like suggested in:
https://github.com/twoscoops/django-admin2/issues/142 https://github.com/twoscoops/django-admin2/issues/142
''' '''
_has_named_permission_regex = re.compile('^has_(?P<name>\w+)_permission$') _has_named_permission_regex = re.compile('^has_(?P<name>\\w+)_permission$')
view_name_mapping = { view_name_mapping = {
'view': 'detail_view', 'view': 'detail_view',
@ -286,7 +281,7 @@ class TemplatePermissionChecker(object):
Return a clone of the permission wrapper with a new model_admin bind Return a clone of the permission wrapper with a new model_admin bind
to it. to it.
''' '''
if isinstance(admin, six.string_types): if isinstance(admin, str):
try: try:
admin = self._model_admin.admin.get_admin_by_name(admin) admin = self._model_admin.admin.get_admin_by_name(admin)
except ValueError: except ValueError:
@ -300,7 +295,7 @@ class TemplatePermissionChecker(object):
''' '''
Return a clone of the permission wrapper with a new view bind to it. Return a clone of the permission wrapper with a new view bind to it.
''' '''
if isinstance(view, six.string_types): if isinstance(view, str):
if view not in self.view_name_mapping: if view not in self.view_name_mapping:
return '' return ''
view_name = self.view_name_mapping[view] view_name = self.view_name_mapping[view]
@ -365,7 +360,7 @@ class TemplatePermissionChecker(object):
def __str__(self): def __str__(self):
if self._view is None: if self._view is None:
return '' return ''
return force_text(bool(self)) return force_str(bool(self))
def create_view_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): # noqa def create_view_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): # noqa

View file

@ -1,17 +1,14 @@
# -*- coding: utf-8 -*-
""" """
There are currently a few renderers that come directly with django-admin2. They There are currently a few renderers that come directly with django-admin2. They
are used by default for some field types. are used by default for some field types.
""" """
from __future__ import division, absolute_import, unicode_literals
import os.path import os.path
from datetime import date, time, datetime from datetime import date, time, datetime
from django.db import models from django.db import models
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils import formats, timezone from django.utils import formats, timezone
from django.utils.encoding import force_text from django.utils.encoding import force_str
from djadmin2 import settings from djadmin2 import settings
@ -65,7 +62,7 @@ def title_renderer(value, field):
:rtype: unicode or str :rtype: unicode or str
""" """
return force_text(value).title() return force_str(value).title()
def number_renderer(value, field): def number_renderer(value, field):

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.conf import settings from django.conf import settings

View file

@ -1,11 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from numbers import Number from numbers import Number
from datetime import date, time, datetime from datetime import date, time, datetime
from django import template from django import template
from django.db.models.fields import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from .. import utils, renderers, models, settings from .. import utils, renderers, models, settings

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,5 +1,5 @@
{% extends "djadmin2theme_bootstrap3/base.html" %} {% extends "djadmin2theme_bootstrap3/base.html" %}
{% load i18n staticfiles admin2_tags %} {% load i18n static admin2_tags %}
{% block navbar %}{% endblock navbar %} {% block navbar %}{% endblock navbar %}
{% block breacrumbs %}{% endblock breacrumbs %} {% block breacrumbs %}{% endblock breacrumbs %}

View file

@ -5,7 +5,7 @@ from ..actions import get_description
from .models import Thing from .models import Thing
class TestAction(object): class TestAction:
description = "Test Action Class" description = "Test Action Class"
@ -23,19 +23,19 @@ class ActionTest(TestCase):
TestAction, TestAction,
test_function, test_function,
]) ])
self.assertEquals( self.assertEqual(
get_description( get_description(
self.admin2.registry[Thing].list_actions[0] self.admin2.registry[Thing].list_actions[0]
), ),
'Delete selected items' 'Delete selected items'
) )
self.assertEquals( self.assertEqual(
get_description( get_description(
self.admin2.registry[Thing].list_actions[1] self.admin2.registry[Thing].list_actions[1]
), ),
'Test Action Class' 'Test Action Class'
) )
self.assertEquals( self.assertEqual(
get_description( get_description(
self.admin2.registry[Thing].list_actions[2] self.admin2.registry[Thing].list_actions[2]
), ),

View file

@ -22,65 +22,65 @@ class TagsTests(TestCase):
self.instance = TagsTestsModel() self.instance = TagsTestsModel()
def test_admin2_urlname(self): def test_admin2_urlname(self):
self.assertEquals( self.assertEqual(
"admin2:None_None_index", "admin2:None_None_index",
admin2_tags.admin2_urlname(IndexView, "index") admin2_tags.admin2_urlname(IndexView, "index")
) )
def test_model_verbose_name_as_model_class(self): def test_model_verbose_name_as_model_class(self):
self.assertEquals( self.assertEqual(
TagsTestsModel._meta.verbose_name, TagsTestsModel._meta.verbose_name,
admin2_tags.model_verbose_name(TagsTestsModel) admin2_tags.model_verbose_name(TagsTestsModel)
) )
def test_model_verbose_name_as_model_instance(self): def test_model_verbose_name_as_model_instance(self):
self.assertEquals( self.assertEqual(
self.instance._meta.verbose_name, self.instance._meta.verbose_name,
admin2_tags.model_verbose_name(self.instance) admin2_tags.model_verbose_name(self.instance)
) )
def test_model_verbose_name_plural_as_model_class(self): def test_model_verbose_name_plural_as_model_class(self):
self.assertEquals( self.assertEqual(
TagsTestsModel._meta.verbose_name_plural, TagsTestsModel._meta.verbose_name_plural,
admin2_tags.model_verbose_name_plural(TagsTestsModel) admin2_tags.model_verbose_name_plural(TagsTestsModel)
) )
def test_model_verbose_name_plural_as_model_instance(self): def test_model_verbose_name_plural_as_model_instance(self):
self.assertEquals( self.assertEqual(
self.instance._meta.verbose_name_plural, self.instance._meta.verbose_name_plural,
admin2_tags.model_verbose_name_plural(self.instance) admin2_tags.model_verbose_name_plural(self.instance)
) )
def test_model_field_verbose_name_autogenerated(self): def test_model_field_verbose_name_autogenerated(self):
self.assertEquals( self.assertEqual(
'field1', 'field1',
admin2_tags.model_attr_verbose_name(self.instance, 'field1') admin2_tags.model_attr_verbose_name(self.instance, 'field1')
) )
def test_model_field_verbose_name_overridden(self): def test_model_field_verbose_name_overridden(self):
self.assertEquals( self.assertEqual(
'second field', 'second field',
admin2_tags.model_attr_verbose_name(self.instance, 'field2') admin2_tags.model_attr_verbose_name(self.instance, 'field2')
) )
def test_model_method_verbose_name(self): def test_model_method_verbose_name(self):
self.assertEquals( self.assertEqual(
'Published recently?', 'Published recently?',
admin2_tags.model_attr_verbose_name(self.instance, 'was_published_recently') admin2_tags.model_attr_verbose_name(self.instance, 'was_published_recently')
) )
def test_formset_visible_fieldlist(self): def test_formset_visible_fieldlist(self):
formset = TagsTestFormSet() formset = TagsTestFormSet()
self.assertEquals( self.assertEqual(
admin2_tags.formset_visible_fieldlist(formset), admin2_tags.formset_visible_fieldlist(formset),
[u'Visible 1', u'Visible 2'] ['Visible 1', 'Visible 2']
) )
def test_verbose_name_for(self): def test_verbose_name_for(self):
app_verbose_names = { app_verbose_names = {
u'app_one_label': 'App One Verbose Name', 'app_one_label': 'App One Verbose Name',
} }
self.assertEquals( self.assertEqual(
"App One Verbose Name", "App One Verbose Name",
admin2_tags.verbose_name_for(app_verbose_names, 'app_one_label') admin2_tags.verbose_name_for(app_verbose_names, 'app_one_label')
) )

View file

@ -1,8 +1,9 @@
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls import reverse
from djadmin2.site import djadmin2_site from djadmin2.site import djadmin2_site
from ..admin2 import UserAdmin2 from ..admin2 import UserAdmin2

View file

@ -33,7 +33,7 @@ class Admin2Test(TestCase):
def test_register_app_verbose_name(self): def test_register_app_verbose_name(self):
self.admin2.register_app_verbose_name(APP_LABEL, APP_VERBOSE_NAME) self.admin2.register_app_verbose_name(APP_LABEL, APP_VERBOSE_NAME)
self.assertEquals( self.assertEqual(
self.admin2.app_verbose_names[APP_LABEL], self.admin2.app_verbose_names[APP_LABEL],
APP_VERBOSE_NAME APP_VERBOSE_NAME
) )
@ -61,7 +61,7 @@ class Admin2Test(TestCase):
def test_get_urls(self): def test_get_urls(self):
self.admin2.register(SmallThing) self.admin2.register(SmallThing)
self.assertEquals(8, len(self.admin2.get_urls())) self.assertEqual(8, len(self.admin2.get_urls()))
def test_default_entries(self): def test_default_entries(self):
expected_default_models = (User, Group, Site) expected_default_models = (User, Group, Site)

View file

@ -1,11 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
import datetime as dt import datetime as dt
from decimal import Decimal from decimal import Decimal
from django.test import TestCase from django.test import TestCase
from django.utils import six
from django.utils.translation import activate from django.utils.translation import activate
from .. import renderers from .. import renderers
@ -106,10 +102,7 @@ class NumberRendererTest(TestCase):
def testEndlessFloat(self): def testEndlessFloat(self):
out = self.renderer(1.0 / 3, None) out = self.renderer(1.0 / 3, None)
if six.PY2: self.assertEqual('0.3333333333333333', out)
self.assertEqual('0.333333333333', out)
else:
self.assertEqual('0.3333333333333333', out)
def testPlainDecimal(self): def testPlainDecimal(self):
number = '0.123456789123456789123456789' number = '0.123456789123456789123456789'

View file

@ -6,7 +6,7 @@ from ..core import Admin2
from .models import BigThing from .models import BigThing
class ModelAdmin(object): class ModelAdmin:
model_admin_attributes = ['a', 'b', 'c'] model_admin_attributes = ['a', 'b', 'c']
a = 1 # covered a = 1 # covered
b = 2 # covered b = 2 # covered
@ -31,9 +31,9 @@ class ImmutableAdminFactoryTests(TestCase):
del self.immutable_admin.a del self.immutable_admin.a
def test_attributes(self): def test_attributes(self):
self.assertEquals(self.immutable_admin.a, 1) self.assertEqual(self.immutable_admin.a, 1)
self.assertEquals(self.immutable_admin.b, 2) self.assertEqual(self.immutable_admin.b, 2)
self.assertEquals(self.immutable_admin.c, 3) self.assertEqual(self.immutable_admin.c, 3)
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
# 'ImmutableAdmin' object has no attribute 'd' # 'ImmutableAdmin' object has no attribute 'd'
self.immutable_admin.d self.immutable_admin.d
@ -73,7 +73,7 @@ class ModelAdminTest(TestCase):
admin_instance.get_urls() admin_instance.get_urls()
except TypeError as e: except TypeError as e:
message = u"Cannot instantiate admin view " \ message = "Cannot instantiate admin view " \
'"ModelAdmin2.None". The error that got raised was: ' \ '"ModelAdmin2.None". The error that got raised was: ' \
"'NoneType' object has no attribute 'as_view'" "'NoneType' object has no attribute 'as_view'"
self.assertEqual(e.args[0], message) self.assertEqual(e.args[0], message)

View file

@ -1,5 +1,4 @@
from django.test import TestCase from django.test import TestCase
from django.utils import six
from .. import utils from .. import utils
from ..views import IndexView from ..views import IndexView
@ -12,14 +11,14 @@ class UtilsTest(TestCase):
self.instance = UtilsTestModel() self.instance = UtilsTestModel()
def test_as_model_class(self): def test_as_model_class(self):
self.assertEquals( self.assertEqual(
UtilsTestModel._meta, UtilsTestModel._meta,
utils.model_options(UtilsTestModel) utils.model_options(UtilsTestModel)
) )
UtilsTestModel._meta.verbose_name = "Utils Test Model is singular" UtilsTestModel._meta.verbose_name = "Utils Test Model is singular"
UtilsTestModel._meta.verbose_name_plural = "Utils Test Model are " +\ UtilsTestModel._meta.verbose_name_plural = "Utils Test Model are " +\
" plural" " plural"
self.assertEquals( self.assertEqual(
UtilsTestModel._meta, UtilsTestModel._meta,
utils.model_options(UtilsTestModel) utils.model_options(UtilsTestModel)
) )
@ -27,14 +26,14 @@ class UtilsTest(TestCase):
UtilsTestModel._meta.verbose_name_plural = "Utils Test Models" UtilsTestModel._meta.verbose_name_plural = "Utils Test Models"
def test_as_model_instance(self): def test_as_model_instance(self):
self.assertEquals( self.assertEqual(
self.instance._meta, self.instance._meta,
utils.model_options(self.instance) utils.model_options(self.instance)
) )
self.instance._meta.verbose_name = "Utils Test Model is singular" self.instance._meta.verbose_name = "Utils Test Model is singular"
self.instance._meta.verbose_name_plural = "Utils Test Model are " +\ self.instance._meta.verbose_name_plural = "Utils Test Model are " +\
" plural" " plural"
self.assertEquals( self.assertEqual(
self.instance._meta, self.instance._meta,
utils.model_options(self.instance) utils.model_options(self.instance)
) )
@ -42,117 +41,111 @@ class UtilsTest(TestCase):
self.instance._meta.verbose_name_plural = "Utils Test Models" self.instance._meta.verbose_name_plural = "Utils Test Models"
def test_admin2_urlname(self): def test_admin2_urlname(self):
self.assertEquals( self.assertEqual(
"admin2:None_None_index", "admin2:None_None_index",
utils.admin2_urlname(IndexView, "index") utils.admin2_urlname(IndexView, "index")
) )
def test_model_app_label_as_model_class(self): def test_model_app_label_as_model_class(self):
self.assertEquals( self.assertEqual(
UtilsTestModel._meta.app_label, UtilsTestModel._meta.app_label,
utils.model_app_label(UtilsTestModel) utils.model_app_label(UtilsTestModel)
) )
def test_model_app_label_as_model_instance(self): def test_model_app_label_as_model_instance(self):
self.assertEquals( self.assertEqual(
self.instance._meta.app_label, self.instance._meta.app_label,
utils.model_app_label(UtilsTestModel) utils.model_app_label(UtilsTestModel)
) )
def test_model_verbose_name_as_model_class(self): def test_model_verbose_name_as_model_class(self):
self.assertEquals( self.assertEqual(
UtilsTestModel._meta.verbose_name, UtilsTestModel._meta.verbose_name,
utils.model_verbose_name(UtilsTestModel) utils.model_verbose_name(UtilsTestModel)
) )
def test_model_verbose_name_as_model_instance(self): def test_model_verbose_name_as_model_instance(self):
self.assertEquals( self.assertEqual(
self.instance._meta.verbose_name, self.instance._meta.verbose_name,
utils.model_verbose_name(self.instance) utils.model_verbose_name(self.instance)
) )
def test_model_verbose_name_plural_as_model_class(self): def test_model_verbose_name_plural_as_model_class(self):
self.assertEquals( self.assertEqual(
UtilsTestModel._meta.verbose_name_plural, UtilsTestModel._meta.verbose_name_plural,
utils.model_verbose_name_plural(UtilsTestModel) utils.model_verbose_name_plural(UtilsTestModel)
) )
def test_model_verbose_name_plural_as_model_instance(self): def test_model_verbose_name_plural_as_model_instance(self):
self.assertEquals( self.assertEqual(
self.instance._meta.verbose_name_plural, self.instance._meta.verbose_name_plural,
utils.model_verbose_name_plural(self.instance) utils.model_verbose_name_plural(self.instance)
) )
def test_model_field_verbose_name_autogenerated(self): def test_model_field_verbose_name_autogenerated(self):
self.assertEquals( self.assertEqual(
'field1', 'field1',
utils.model_field_verbose_name(self.instance, 'field1') utils.model_field_verbose_name(self.instance, 'field1')
) )
def test_model_field_verbose_name_overridden(self): def test_model_field_verbose_name_overridden(self):
self.assertEquals( self.assertEqual(
'second field', 'second field',
utils.model_field_verbose_name(self.instance, 'field2') utils.model_field_verbose_name(self.instance, 'field2')
) )
def test_model_method_verbose_name(self): def test_model_method_verbose_name(self):
self.assertEquals( self.assertEqual(
'Published recently?', 'Published recently?',
utils.model_method_verbose_name(self.instance, 'was_published_recently') utils.model_method_verbose_name(self.instance, 'was_published_recently')
) )
def test_model_method_verbose_name_fallback(self): def test_model_method_verbose_name_fallback(self):
self.assertEquals( self.assertEqual(
'simple_method', 'simple_method',
utils.model_method_verbose_name(self.instance, 'simple_method') utils.model_method_verbose_name(self.instance, 'simple_method')
) )
def test_app_label_as_model_class(self): def test_app_label_as_model_class(self):
self.assertEquals( self.assertEqual(
UtilsTestModel._meta.app_label, UtilsTestModel._meta.app_label,
utils.model_app_label(UtilsTestModel) utils.model_app_label(UtilsTestModel)
) )
def test_app_label_as_model_instance(self): def test_app_label_as_model_instance(self):
self.assertEquals( self.assertEqual(
self.instance._meta.app_label, self.instance._meta.app_label,
utils.model_app_label(self.instance) utils.model_app_label(self.instance)
) )
def test_get_attr_callable(self): def test_get_attr_callable(self):
class Klass(object): class Klass:
def hello(self): def hello(self):
return "hello" return "hello"
self.assertEquals( self.assertEqual(
utils.get_attr(Klass(), "hello"), utils.get_attr(Klass(), "hello"),
"hello" "hello"
) )
def test_get_attr_str(self): def test_get_attr_str(self):
class Klass(object): class Klass:
def __str__(self): def __str__(self):
return "str" return "str"
def __unicode__(self): def __unicode__(self):
return "unicode" return "unicode"
if six.PY2: self.assertEqual(
self.assertEquals( utils.get_attr(Klass(), "__str__"),
utils.get_attr(Klass(), "__str__"), "str"
"unicode" )
)
else:
self.assertEquals(
utils.get_attr(Klass(), "__str__"),
"str"
)
def test_get_attr(self): def test_get_attr(self):
class Klass(object): class Klass:
attr = "value" attr = "value"
self.assertEquals( self.assertEqual(
utils.get_attr(Klass(), "attr"), utils.get_attr(Klass(), "attr"),
"value" "value"
) )

View file

@ -1,6 +1,8 @@
from django.core.urlresolvers import reverse
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils.encoding import force_text from django.urls import reverse
from django.utils.encoding import force_str
from .. import views from .. import views
@ -11,13 +13,13 @@ class AdminViewTest(TestCase):
self.admin_view = views.AdminView(r'^$', views.ModelListView, name='admin-view') self.admin_view = views.AdminView(r'^$', views.ModelListView, name='admin-view')
def test_url(self): def test_url(self):
self.assertEquals(self.admin_view.url, r'^$') self.assertEqual(self.admin_view.url, r'^$')
def test_view(self): def test_view(self):
self.assertEquals(self.admin_view.view, views.ModelListView) self.assertEqual(self.admin_view.view, views.ModelListView)
def test_name(self): def test_name(self):
self.assertEquals(self.admin_view.name, 'admin-view') self.assertEqual(self.admin_view.name, 'admin-view')
@override_settings(ROOT_URLCONF='djadmin2.tests.urls') @override_settings(ROOT_URLCONF='djadmin2.tests.urls')
@ -25,4 +27,4 @@ class CustomLoginViewTest(TestCase):
def test_view_ok(self): def test_view_ok(self):
response = self.client.get(reverse("admin2:dashboard")) response = self.client.get(reverse("admin2:dashboard"))
self.assertInHTML('<h3 class="panel-title">Custom login view</h3>', force_text(response.content)) self.assertInHTML('<h3 class="panel-title">Custom login view</h3>', force_str(response.content))

View file

@ -1,8 +1,6 @@
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static from django.conf.urls.static import static
from django.urls import re_path
from djadmin2.site import djadmin2_site from djadmin2.site import djadmin2_site
@ -12,9 +10,10 @@ from djadmin2.views import LoginView
class CustomLoginView(LoginView): class CustomLoginView(LoginView):
default_template_name = "custom_login_template.html" default_template_name = "custom_login_template.html"
djadmin2_site.login_view = CustomLoginView djadmin2_site.login_view = CustomLoginView
djadmin2_site.autodiscover() djadmin2_site.autodiscover()
urlpatterns = [ urlpatterns = [
url(r'^admin2/', include(djadmin2_site.urls)), re_path(r'^admin2/', djadmin2_site.urls),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -1,5 +1,5 @@
{% extends "djadmin2theme_bootstrap3/base.html" %} {% extends "djadmin2theme_bootstrap3/base.html" %}
{% load i18n staticfiles admin2_tags %} {% load i18n static admin2_tags %}
{% block navbar %}{% endblock navbar %} {% block navbar %}{% endblock navbar %}
{% block breacrumbs %}{% endblock breacrumbs %} {% block breacrumbs %}{% endblock breacrumbs %}

View file

@ -1,4 +1,4 @@
{% load i18n %}{% load staticfiles %}<!DOCTYPE html> {% load i18n %}{% load static %}<!DOCTYPE html>
<html lang="{{ request.LANGUAGE_CODE }}"> <html lang="{{ request.LANGUAGE_CODE }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">

View file

@ -1,5 +1,5 @@
{% extends "djadmin2theme_bootstrap3/base.html" %} {% extends "djadmin2theme_bootstrap3/base.html" %}
{% load i18n staticfiles admin2_tags %} {% load i18n static admin2_tags %}
{% block title %}{% blocktrans with model_name=model_name %}Select {{ model_name }} to change{% endblocktrans %}{% endblock title %} {% block title %}{% blocktrans with model_name=model_name %}Select {{ model_name }} to change{% endblocktrans %}{% endblock title %}
@ -49,7 +49,7 @@
</li> </li>
{% endif %} {% endif %}
{% for link, date in dates %} {% for link, date in dates %}
<li class="{% ifequal active_day date %}active{% endifequal %}"> <li class="{% if active_day == date %}active{% endif %}">
<a href="{{ link|safe }}">{{ date }}</a> <a href="{{ link|safe }}">{{ date }}</a>
</li> </li>
{% endfor %} {% endfor %}

View file

@ -1,16 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
import logging import logging
import os import os
import sys import sys
from collections import namedtuple from collections import namedtuple
import extra_views import extra_views
from django.conf.urls import url
from django.core.urlresolvers import reverse
from django.forms import modelform_factory from django.forms import modelform_factory
from django.utils.six import with_metaclass from django.urls import re_path, reverse
from . import actions from . import actions
from . import apiviews from . import apiviews
@ -19,14 +14,12 @@ from . import utils
from . import views from . import views
logger = logging.getLogger('djadmin2') logger = logging.getLogger("djadmin2")
class ModelAdminBase2(type): class ModelAdminBase2(type):
def __new__(cls, name, bases, attrs): def __new__(cls, name, bases, attrs):
new_class = super(ModelAdminBase2, cls).__new__(cls, name, new_class = super().__new__(cls, name, bases, attrs)
bases, attrs)
view_list = [] view_list = []
for key, value in attrs.items(): for key, value in attrs.items():
if isinstance(value, views.AdminView): if isinstance(value, views.AdminView):
@ -34,12 +27,12 @@ class ModelAdminBase2(type):
value.name = key value.name = key
view_list.append(value) view_list.append(value)
view_list.extend(getattr(new_class, 'views', [])) view_list.extend(getattr(new_class, "views", []))
new_class.views = view_list new_class.views = view_list
return new_class return new_class
class ModelAdmin2(with_metaclass(ModelAdminBase2)): class ModelAdmin2(metaclass=ModelAdminBase2):
""" """
Adding new ModelAdmin2 attributes: Adding new ModelAdmin2 attributes:
@ -58,9 +51,10 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
This prevents us from easily implementing methods/setters which This prevents us from easily implementing methods/setters which
bypass the blocking features of the ImmutableAdmin. bypass the blocking features of the ImmutableAdmin.
""" """
actions_selection_counter = True actions_selection_counter = True
date_hierarchy = False date_hierarchy = False
list_display = ('__str__',) list_display = ("__str__",)
list_display_links = () list_display_links = ()
list_filter = () list_filter = ()
list_select_related = False list_select_related = False
@ -117,12 +111,20 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
inlines = [] inlines = []
# Views # Views
index_view = views.AdminView(r'^$', views.ModelListView, name='index') index_view = views.AdminView(r"^$", views.ModelListView, name="index")
create_view = views.AdminView(r'^create/$', views.ModelAddFormView, name='create') create_view = views.AdminView(r"^create/$", views.ModelAddFormView, name="create")
update_view = views.AdminView(r'^(?P<pk>[0-9]+)/$', views.ModelEditFormView, name='update') update_view = views.AdminView(
detail_view = views.AdminView(r'^(?P<pk>[0-9]+)/update/$', views.ModelDetailView, name='detail') r"^(?P<pk>[0-9]+)/$", views.ModelEditFormView, name="update"
delete_view = views.AdminView(r'^(?P<pk>[0-9]+)/delete/$', views.ModelDeleteView, name='delete') )
history_view = views.AdminView(r'^(?P<pk>[0-9]+)/history/$', views.ModelHistoryView, name='history') detail_view = views.AdminView(
r"^(?P<pk>[0-9]+)/update/$", views.ModelDetailView, name="detail"
)
delete_view = views.AdminView(
r"^(?P<pk>[0-9]+)/delete/$", views.ModelDeleteView, name="delete"
)
history_view = views.AdminView(
r"^(?P<pk>[0-9]+)/history/$", views.ModelHistoryView, name="history"
)
views = [] views = []
# API configuration # API configuration
@ -141,7 +143,7 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
self.model_name = model_options.object_name.lower() self.model_name = model_options.object_name.lower()
if self.name is None: if self.name is None:
self.name = '{}_{}'.format(self.app_label, self.model_name) self.name = "{}_{}".format(self.app_label, self.model_name)
if self.verbose_name is None: if self.verbose_name is None:
self.verbose_name = model_options.verbose_name self.verbose_name = model_options.verbose_name
@ -150,60 +152,73 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
def get_default_view_kwargs(self): def get_default_view_kwargs(self):
return { return {
'app_label': self.app_label, "app_label": self.app_label,
'model': self.model, "model": self.model,
'model_name': self.model_name, "model_name": self.model_name,
'model_admin': immutable_admin_factory(self), "model_admin": immutable_admin_factory(self),
} }
def get_index_kwargs(self): def get_index_kwargs(self):
kwargs = self.get_default_view_kwargs() kwargs = self.get_default_view_kwargs()
kwargs.update({ kwargs.update(
'paginate_by': self.list_per_page, {
}) "paginate_by": self.list_per_page,
}
)
return kwargs return kwargs
def get_default_api_view_kwargs(self): def get_default_api_view_kwargs(self):
kwargs = self.get_default_view_kwargs() kwargs = self.get_default_view_kwargs()
kwargs.update({ kwargs.update(
'serializer_class': self.api_serializer_class, {
}) "serializer_class": self.api_serializer_class,
}
)
return kwargs return kwargs
def get_prefixed_view_name(self, view_name): def get_prefixed_view_name(self, view_name):
return '{}_{}'.format(self.name, view_name) return "{}_{}".format(self.name, view_name)
def get_create_kwargs(self): def get_create_kwargs(self):
kwargs = self.get_default_view_kwargs() kwargs = self.get_default_view_kwargs()
kwargs.update({ kwargs.update(
'inlines': self.inlines, {
'form_class': (self.create_form_class if "inlines": self.inlines,
self.create_form_class else self.form_class), "form_class": (
}) self.create_form_class
if self.create_form_class
else self.form_class
),
}
)
return kwargs return kwargs
def get_update_kwargs(self): def get_update_kwargs(self):
kwargs = self.get_default_view_kwargs() kwargs = self.get_default_view_kwargs()
form_class = (self.update_form_class if form_class = (
self.update_form_class else self.form_class) self.update_form_class if self.update_form_class else self.form_class
)
if form_class is None: if form_class is None:
form_class = modelform_factory(self.model, fields='__all__') form_class = modelform_factory(self.model, fields="__all__")
kwargs.update({ kwargs.update(
'inlines': self.inlines, {
'form_class': form_class, "inlines": self.inlines,
}) "form_class": form_class,
}
)
return kwargs return kwargs
def get_index_url(self): def get_index_url(self):
return reverse('admin2:{}'.format( return reverse("admin2:{}".format(self.get_prefixed_view_name("index")))
self.get_prefixed_view_name('index')))
def get_api_list_kwargs(self): def get_api_list_kwargs(self):
kwargs = self.get_default_api_view_kwargs() kwargs = self.get_default_api_view_kwargs()
kwargs.update({ kwargs.update(
'queryset': self.model.objects.all(), {
# 'paginate_by': self.list_per_page, "queryset": self.model.objects.all(),
}) # 'paginate_by': self.list_per_page,
}
)
return kwargs return kwargs
def get_api_detail_kwargs(self): def get_api_detail_kwargs(self):
@ -222,34 +237,35 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
trace = sys.exc_info()[2] trace = sys.exc_info()[2]
new_exception = TypeError( new_exception = TypeError(
'Cannot instantiate admin view "{}.{}". ' 'Cannot instantiate admin view "{}.{}". '
'The error that got raised was: {}'.format( "The error that got raised was: {}".format(
self.__class__.__name__, admin_view.name, e)) self.__class__.__name__, admin_view.name, e
)
)
try: try:
raise new_exception.with_traceback(trace) raise new_exception.with_traceback(trace)
except AttributeError: except AttributeError:
raise (new_exception, None, trace) raise (new_exception, None, trace)
pattern_list.append( pattern_list.append(
url( re_path(
regex=admin_view.url, admin_view.url,
view=view_instance, view=view_instance,
name=self.get_prefixed_view_name(admin_view.name) name=self.get_prefixed_view_name(admin_view.name),
) )
) )
return pattern_list return pattern_list
def get_api_urls(self): def get_api_urls(self):
return [ return [
url( re_path(
regex=r'^$', r"^$",
view=self.api_list_view.as_view(**self.get_api_list_kwargs()), view=self.api_list_view.as_view(**self.get_api_list_kwargs()),
name=self.get_prefixed_view_name('api_list'), name=self.get_prefixed_view_name("api_list"),
), ),
url( re_path(
regex=r'^(?P<pk>[0-9]+)/$', r"^(?P<pk>[0-9]+)/$",
view=self.api_detail_view.as_view( view=self.api_detail_view.as_view(**self.get_api_detail_kwargs()),
**self.get_api_detail_kwargs()), name=self.get_prefixed_view_name("api_detail"),
name=self.get_prefixed_view_name('api_detail'),
), ),
] ]
@ -266,12 +282,12 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
actions_dict = {} actions_dict = {}
for cls in type(self).mro()[::-1]: for cls in type(self).mro()[::-1]:
class_actions = getattr(cls, 'list_actions', []) class_actions = getattr(cls, "list_actions", [])
for action in class_actions: for action in class_actions:
actions_dict[action.__name__] = { actions_dict[action.__name__] = {
'name': action.__name__, "name": action.__name__,
'description': actions.get_description(action), "description": actions.get_description(action),
'action_callable': action "action_callable": action,
} }
return actions_dict return actions_dict
@ -279,19 +295,21 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
return self.ordering return self.ordering
class Admin2Inline(extra_views.InlineFormSet): class Admin2Inline(extra_views.InlineFormSetFactory):
""" """
A simple extension of django-extra-view's InlineFormSet that A simple extension of django-extra-view's InlineFormSet that
adds some useful functionality. adds some useful functionality.
""" """
template = None template = None
fields = "__all__"
def construct_formset(self): def construct_formset(self):
""" """
Overrides construct_formset to attach the model class as Overrides construct_formset to attach the model class as
an attribute of the returned formset instance. an attribute of the returned formset instance.
""" """
formset = super(Admin2Inline, self).construct_formset() formset = super().construct_formset()
formset.model = self.inline_model formset.model = self.inline_model
formset.template = self.template formset.template = self.template
return formset return formset
@ -299,12 +317,14 @@ class Admin2Inline(extra_views.InlineFormSet):
class Admin2TabularInline(Admin2Inline): class Admin2TabularInline(Admin2Inline):
template = os.path.join( template = os.path.join(
settings.ADMIN2_THEME_DIRECTORY, 'edit_inlines/tabular.html') settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/tabular.html"
)
class Admin2StackedInline(Admin2Inline): class Admin2StackedInline(Admin2Inline):
template = os.path.join( template = os.path.join(
settings.ADMIN2_THEME_DIRECTORY, 'edit_inlines/stacked.html') settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/stacked.html"
)
def immutable_admin_factory(model_admin): def immutable_admin_factory(model_admin):
@ -318,8 +338,7 @@ def immutable_admin_factory(model_admin):
the result, but hopefully developers attempting that the result, but hopefully developers attempting that
'workaround/hack' will read our documentation. 'workaround/hack' will read our documentation.
""" """
ImmutableAdmin = namedtuple('ImmutableAdmin', ImmutableAdmin = namedtuple("ImmutableAdmin", model_admin.model_admin_attributes)
model_admin.model_admin_attributes, return ImmutableAdmin(
verbose=False) *[getattr(model_admin, x) for x in model_admin.model_admin_attributes]
return ImmutableAdmin(*[getattr( )
model_admin, x) for x in model_admin.model_admin_attributes])

View file

@ -1,12 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from collections import defaultdict from collections import defaultdict
from django.core.exceptions import FieldDoesNotExist
from django.db.models.constants import LOOKUP_SEP
from django.db.models.deletion import Collector, ProtectedError from django.db.models.deletion import Collector, ProtectedError
from django.db.models.sql.constants import QUERY_TERMS from django.utils.encoding import force_bytes, force_str
from django.utils import six
from django.utils.encoding import force_bytes, force_text
def lookup_needs_distinct(opts, lookup_path): def lookup_needs_distinct(opts, lookup_path):
@ -19,20 +16,24 @@ def lookup_needs_distinct(opts, lookup_path):
https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L22 https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L22
""" """
lookup_fields = lookup_path.split('__') lookup_fields = lookup_path.split(LOOKUP_SEP)
# Remove the last item of the lookup path if it is a query term # Go through the fields (following all relations) and look for an m2m.
if lookup_fields[-1] in QUERY_TERMS:
lookup_fields = lookup_fields[:-1]
# Now go through the fields (following all relations) and look for an m2m
for field_name in lookup_fields: for field_name in lookup_fields:
field = opts.get_field(field_name) if field_name == 'pk':
if hasattr(field, 'get_path_info'): field_name = opts.pk.name
# This field is a relation, update opts to follow the relation try:
path_info = field.get_path_info() field = opts.get_field(field_name)
opts = path_info[-1].to_opts except FieldDoesNotExist:
if any(path.m2m for path in path_info): # Ignore query lookups.
# This field is a m2m relation so we know we need to call distinct continue
return True else:
if hasattr(field, 'get_path_info'):
# This field is a relation; update opts to follow the relation.
path_info = field.get_path_info()
opts = path_info[-1].to_opts
if any(path.m2m for path in path_info):
# This field is a m2m relation so distinct must be called.
return True
return False return False
@ -118,7 +119,7 @@ class NestedObjects(Collector):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NestedObjects, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.edges = {} # {from_instance: [to_instances]} self.edges = {} # {from_instance: [to_instances]}
self.protected = set() self.protected = set()
self.model_objs = defaultdict(set) self.model_objs = defaultdict(set)
@ -138,13 +139,21 @@ class NestedObjects(Collector):
self.add_edge(None, obj) self.add_edge(None, obj)
self.model_objs[obj._meta.model].add(obj) self.model_objs[obj._meta.model].add(obj)
try: try:
return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs) return super().collect(objs, source_attr=source_attr, **kwargs)
except ProtectedError as e: except ProtectedError as e:
self.protected.update(e.protected_objects) self.protected.update(e.protected_objects)
def related_objects(self, related, objs): def related_objects(self, *args):
qs = super(NestedObjects, self).related_objects(related, objs) # Django >= 3.1
return qs.select_related(related.field.name) if len(args) == 3:
related_model, related_fields, objs = args
qs = super().related_objects(related_model, related_fields, objs)
return qs.select_related(*[related_field.name for related_field in related_fields])
# Django < 3.1
elif len(args) == 2:
related, objs = args
qs = super(NestedObjects, self).related_objects(related, objs)
return qs.select_related(related.field.name)
def _nested(self, obj, seen, format_callback): def _nested(self, obj, seen, format_callback):
if obj in seen: if obj in seen:
@ -191,7 +200,7 @@ def quote(s):
https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L66-L73 https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L66-L73
""" """
if not isinstance(s, six.string_types): if not isinstance(s, str):
return s return s
res = list(s) res = list(s)
for i in range(len(res)): for i in range(len(res)):
@ -202,7 +211,4 @@ def quote(s):
def type_str(text): def type_str(text):
if six.PY2: return force_str(text)
return force_bytes(text)
else:
return force_text(text)

View file

@ -1,16 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
import os import os
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse, reverse_lazy
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.utils.encoding import force_text from django.urls import reverse, reverse_lazy
from django.utils.encoding import force_str
from django.utils.text import get_text_list from django.utils.text import get_text_list
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
# braces 1.3 views exported AccessMixin # braces 1.3 views exported AccessMixin
# in braces 1.4 this was moved views._access and not exported in views # in braces 1.4 this was moved views._access and not exported in views
@ -34,7 +31,7 @@ class PermissionMixin(AccessMixin):
self.permissions = [ self.permissions = [
permission_class() permission_class()
for permission_class in self.permission_classes] for permission_class in self.permission_classes]
super(PermissionMixin, self).__init__(**kwargs) super().__init__(**kwargs)
def has_permission(self, obj=None): def has_permission(self, obj=None):
''' '''
@ -58,10 +55,10 @@ class PermissionMixin(AccessMixin):
request.get_full_path(), request.get_full_path(),
self.get_login_url(), self.get_login_url(),
self.get_redirect_field_name()) self.get_redirect_field_name())
return super(PermissionMixin, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PermissionMixin, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
permission_checker = permissions.TemplatePermissionChecker( permission_checker = permissions.TemplatePermissionChecker(
self.request, self.model_admin) self.request, self.model_admin)
context.update({ context.update({
@ -84,6 +81,9 @@ class Admin2Mixin(PermissionMixin):
return [os.path.join( return [os.path.join(
settings.ADMIN2_THEME_DIRECTORY, self.default_template_name)] settings.ADMIN2_THEME_DIRECTORY, self.default_template_name)]
def get_templates(self):
return os.path.join(settings.ADMIN2_THEME_DIRECTORY, self.default_template_name)
def get_model(self): def get_model(self):
return self.model return self.model
@ -111,14 +111,14 @@ class Admin2Mixin(PermissionMixin):
} }
return self.login_view().dispatch(request, extra_context=extra, *args, **kwargs) return self.login_view().dispatch(request, extra_context=extra, *args, **kwargs)
return super(Admin2Mixin, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
class Admin2ModelMixin(Admin2Mixin): class Admin2ModelMixin(Admin2Mixin):
model_admin = None model_admin = None
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(Admin2ModelMixin, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
model = self.get_model() model = self.get_model()
model_meta = model_options(model) model_meta = model_options(model)
app_verbose_names = self.model_admin.admin.app_verbose_names app_verbose_names = self.model_admin.admin.app_verbose_names
@ -142,7 +142,7 @@ class Admin2ModelMixin(Admin2Mixin):
return modelform_factory(self.get_model(), fields='__all__') return modelform_factory(self.get_model(), fields='__all__')
class Admin2ModelFormMixin(object): class Admin2ModelFormMixin:
def get_success_url(self): def get_success_url(self):
if '_continue' in self.request.POST: if '_continue' in self.request.POST:
view_name = admin2_urlname(self, 'update') view_name = admin2_urlname(self, 'update')
@ -167,19 +167,19 @@ class Admin2ModelFormMixin(object):
for added_object in formset.new_objects: for added_object in formset.new_objects:
change_message.append( change_message.append(
_('Added {0} "{1}".'.format( _('Added {0} "{1}".'.format(
force_text(added_object._meta.verbose_name), force_str(added_object._meta.verbose_name),
force_text(added_object)))) force_str(added_object))))
for changed_object, changed_fields in formset.changed_objects: for changed_object, changed_fields in formset.changed_objects:
change_message.append( change_message.append(
_('Changed {0} for {1} "{2}".'.format( _('Changed {0} for {1} "{2}".'.format(
get_text_list(changed_fields, _('and')), get_text_list(changed_fields, _('and')),
force_text(changed_object._meta.verbose_name), force_str(changed_object._meta.verbose_name),
force_text(changed_object)))) force_str(changed_object))))
for deleted_object in formset.deleted_objects: for deleted_object in formset.deleted_objects:
change_message.append( change_message.append(
_('Deleted {0} "{1}".'.format( _('Deleted {0} "{1}".'.format(
force_text(deleted_object._meta.verbose_name), force_str(deleted_object._meta.verbose_name),
force_text(deleted_object)))) force_str(deleted_object))))
change_message = ' '.join(change_message) change_message = ' '.join(change_message)
return change_message or _('No fields changed.') return change_message or _('No fields changed.')

View file

@ -1,26 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
import operator import operator
from datetime import datetime from datetime import datetime
from functools import reduce from functools import reduce
import extra_views import extra_views
from django.core.exceptions import FieldDoesNotExist
from django.conf import settings from django.conf import settings
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import (logout as auth_logout,
update_session_auth_hash)
from django.contrib.auth.forms import (PasswordChangeForm, from django.contrib.auth.forms import (PasswordChangeForm,
AdminPasswordChangeForm) AdminPasswordChangeForm)
from django.contrib.auth.views import (logout as auth_logout, from django.contrib.auth.views import LoginView as DjangoLoginView
login as auth_login)
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse, reverse_lazy
from django.db import models, router from django.db import models, router
from django.db.models.fields import FieldDoesNotExist
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.encoding import force_text from django.utils.encoding import force_str
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy from django.utils.translation import gettext_lazy
from django.urls import reverse, reverse_lazy
from django.views import generic from django.views import generic
from . import permissions, utils from . import permissions, utils
@ -30,7 +28,7 @@ from .models import LogEntry
from .viewmixins import Admin2Mixin, Admin2ModelMixin, Admin2ModelFormMixin from .viewmixins import Admin2Mixin, Admin2ModelMixin, Admin2ModelFormMixin
class AdminView(object): class AdminView:
def __init__(self, url, view, name=None): def __init__(self, url, view, name=None):
self.url = url self.url = url
@ -63,7 +61,7 @@ class IndexView(Admin2Mixin, generic.TemplateView):
app_verbose_names = None app_verbose_names = None
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super(IndexView, self).get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data.update({ data.update({
'apps': self.apps, 'apps': self.apps,
'app_verbose_names': self.app_verbose_names, 'app_verbose_names': self.app_verbose_names,
@ -90,7 +88,7 @@ class AppIndexView(Admin2Mixin, generic.TemplateView):
app_verbose_names = None app_verbose_names = None
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super(AppIndexView, self).get_context_data(**kwargs) data = super().get_context_data(**kwargs)
app_label = self.kwargs['app_label'] app_label = self.kwargs['app_label']
registry = self.apps[app_label] registry = self.apps[app_label]
data.update({ data.update({
@ -115,6 +113,7 @@ class ModelListView(Admin2ModelMixin, generic.ListView):
app name. app name.
""" """
default_template_name = "model_list.html" default_template_name = "model_list.html"
paginate_by = 10
permission_classes = ( permission_classes = (
permissions.IsStaffPermission, permissions.IsStaffPermission,
permissions.ModelViewPermission) permissions.ModelViewPermission)
@ -173,7 +172,7 @@ class ModelListView(Admin2ModelMixin, generic.ListView):
return queryset, use_distinct return queryset, use_distinct
def get_queryset(self): def get_queryset(self):
queryset = super(ModelListView, self).get_queryset() queryset = super().get_queryset()
search_term = self.request.GET.get('q', None) search_term = self.request.GET.get('q', None)
search_use_distinct = False search_use_distinct = False
if self.model_admin.search_fields and search_term: if self.model_admin.search_fields and search_term:
@ -247,7 +246,7 @@ class ModelListView(Admin2ModelMixin, generic.ListView):
return self._date_filter return self._date_filter
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ModelListView, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['model'] = self.get_model() context['model'] = self.get_model()
context['actions'] = self.get_actions().values() context['actions'] = self.get_actions().values()
context['search_fields'] = self.get_search_fields() context['search_fields'] = self.get_search_fields()
@ -283,7 +282,7 @@ class ModelListView(Admin2ModelMixin, generic.ListView):
elif year: elif year:
context["previous_date"] = { context["previous_date"] = {
"link": "?", "link": "?",
"text": ugettext_lazy(" All dates"), "text": gettext_lazy(" All dates"),
} }
context["dates"] = self._format_months(self.get_queryset()) context["dates"] = self._format_months(self.get_queryset())
@ -387,7 +386,7 @@ class ModelEditFormView(Admin2ModelMixin, Admin2ModelFormMixin,
context = super(ModelEditFormView, self).get_context_data(**kwargs) context = super(ModelEditFormView, self).get_context_data(**kwargs)
context['model'] = self.get_model() context['model'] = self.get_model()
context['action'] = "Change" context['action'] = "Change"
context['action_name'] = ugettext_lazy("Change") context['action_name'] = gettext_lazy("Change")
return context return context
def forms_valid(self, form, inlines): def forms_valid(self, form, inlines):
@ -420,14 +419,14 @@ class ModelAddFormView(Admin2ModelMixin, Admin2ModelFormMixin,
permissions.ModelAddPermission) permissions.ModelAddPermission)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ModelAddFormView, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['model'] = self.get_model() context['model'] = self.get_model()
context['action'] = "Add" context['action'] = "Add"
context['action_name'] = ugettext_lazy("Add") context['action_name'] = gettext_lazy("Add")
return context return context
def forms_valid(self, form, inlines): def forms_valid(self, form, inlines):
response = super(ModelAddFormView, self).forms_valid(form, inlines) response = super().forms_valid(form, inlines)
LogEntry.objects.log_action( LogEntry.objects.log_action(
self.request.user.id, self.request.user.id,
self.object, self.object,
@ -456,12 +455,12 @@ class ModelDeleteView(Admin2ModelMixin, generic.DeleteView):
permissions.ModelDeletePermission) permissions.ModelDeletePermission)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ModelDeleteView, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
def _format_callback(obj): def _format_callback(obj):
opts = utils.model_options(obj) opts = utils.model_options(obj)
return '%s: %s' % (force_text(capfirst(opts.verbose_name)), return '%s: %s' % (force_str(capfirst(opts.verbose_name)),
force_text(obj)) force_str(obj))
using = router.db_for_write(self.get_object()._meta.model) using = router.db_for_write(self.get_object()._meta.model)
collector = utils.NestedObjects(using=using) collector = utils.NestedObjects(using=using)
@ -477,7 +476,7 @@ class ModelDeleteView(Admin2ModelMixin, generic.DeleteView):
self.get_object(), self.get_object(),
LogEntry.DELETION, LogEntry.DELETION,
'Object deleted.') 'Object deleted.')
return super(ModelDeleteView, self).delete(request, *args, **kwargs) return super().delete(request, *args, **kwargs)
class ModelHistoryView(Admin2ModelMixin, generic.ListView): class ModelHistoryView(Admin2ModelMixin, generic.ListView):
@ -493,13 +492,14 @@ class ModelHistoryView(Admin2ModelMixin, generic.ListView):
app name. app name.
""" """
default_template_name = "model_history.html" default_template_name = "model_history.html"
paginate_by = 10
permission_classes = ( permission_classes = (
permissions.IsStaffPermission, permissions.IsStaffPermission,
permissions.ModelChangePermission permissions.ModelChangePermission
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ModelHistoryView, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['model'] = self.get_model() context['model'] = self.get_model()
context['object'] = self.get_object() context['object'] = self.get_object()
return context return context
@ -536,7 +536,7 @@ class PasswordChangeView(Admin2Mixin, generic.UpdateView):
def get_form_class(self): def get_form_class(self):
if self.request.user == self.get_object(): if self.request.user == self.get_object():
return self.admin_form_class return self.admin_form_class
return super(PasswordChangeView, self).get_form_class() return super().get_form_class()
def get_queryset(self): def get_queryset(self):
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -564,10 +564,7 @@ class LoginView(Admin2Mixin, generic.TemplateView):
authentication_form = AdminAuthenticationForm authentication_form = AdminAuthenticationForm
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
return auth_login(request, return DjangoLoginView.as_view(template_name=self.get_templates(), authentication_form=self.authentication_form, *args, **kwargs)(request)
authentication_form=self.authentication_form,
template_name=self.get_template_names(),
*args, **kwargs)
class LogoutView(Admin2Mixin, generic.TemplateView): class LogoutView(Admin2Mixin, generic.TemplateView):
@ -579,5 +576,6 @@ class LogoutView(Admin2Mixin, generic.TemplateView):
default_template_name = 'auth/logout.html' default_template_name = 'auth/logout.html'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return auth_logout(request, template_name=self.get_template_names(), auth_logout(request)
*args, **kwargs) context = self.get_context_data(**kwargs)
return self.render_to_response(context)

View file

@ -1,4 +1,4 @@
# Taken from https://github.com/django/django/blob/master/docs/_ext/djangodocs.py # Taken from https://github.com/django/django/blob/main/docs/_ext/djangodocs.py
import re import re
from sphinx import addnodes from sphinx import addnodes

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
#
# django-admin2 documentation build configuration file, created by # django-admin2 documentation build configuration file, created by
# sphinx-quickstart on Sat May 18 12:59:02 2013. # sphinx-quickstart on Sat May 18 12:59:02 2013.
# #

View file

@ -109,8 +109,7 @@ Now when you go to your fork on GitHub, you will see this branch listed under
the "Source" tab where it says "Switch Branches". Go ahead and select your the "Source" tab where it says "Switch Branches". Go ahead and select your
topic branch from this list, and then click the "Pull request" button. topic branch from this list, and then click the "Pull request" button.
Your pull request should be applied to the **develop** branch of django-admin2. Your pull request should be applied to the **main** branch of django-admin2.
Be sure to change from the default of ``master`` to ``develop``.
Next, you can add a comment about your branch. If this in response to Next, you can add a comment about your branch. If this in response to
a submitted issue, it is good to put a link to that issue in this initial a submitted issue, it is good to put a link to that issue in this initial
@ -132,8 +131,8 @@ Pull upstream changes into your fork regularly
To pull in upstream changes:: To pull in upstream changes::
git remote add upstream https://github.com/twoscoops/django-admin2.git git remote add upstream https://github.com/jazzband/django-admin2.git
git pull upstream develop git pull upstream main
For more info, see http://help.github.com/fork-a-repo/ For more info, see http://help.github.com/fork-a-repo/
@ -145,7 +144,7 @@ Advanced git users: Pull with rebase
This will pull and then reapply your work on top of the upcoming changes:: This will pull and then reapply your work on top of the upcoming changes::
git pull --rebase upstream develop git pull --rebase upstream main
It saves you from an extra merge, keeping the history cleaner, but it's potentially dangerous because you're rewriting history. For more info, see http://gitready.com/advanced/2009/02/11/pull-with-rebase.html It saves you from an extra merge, keeping the history cleaner, but it's potentially dangerous because you're rewriting history. For more info, see http://gitready.com/advanced/2009/02/11/pull-with-rebase.html
@ -262,8 +261,8 @@ How pull requests are checked, tested, and done
First we pull the code into a local branch:: First we pull the code into a local branch::
git checkout develop git checkout main
git checkout -b <submitter-github-name>-<submitter-branch> develop git checkout -b <submitter-github-name>-<submitter-branch> main
git pull git://github.com/<submitter-github-name>/django-admin2.git <submitter-branch> <branch-name> git pull git://github.com/<submitter-github-name>/django-admin2.git <submitter-branch> <branch-name>
Then we run the tests:: Then we run the tests::
@ -279,11 +278,11 @@ We do the following:
We finish with a merge and push to GitHub:: We finish with a merge and push to GitHub::
git checkout develop git checkout main
git merge <branch-name> git merge <branch-name>
git push origin develop git push origin main
.. _installation: install.html .. _installation: install.html
.. _GitHub project: https://github.com/twoscoops/django-admin2 .. _GitHub project: https://github.com/jazzband/django-admin2
.. _issue tracker: https://github.com/twoscoops/django-admin2/issues .. _issue tracker: https://github.com/jazzband/django-admin2/issues
.. _pydanny: http://pydanny.com .. _pydanny: http://pydanny.com

View file

@ -2,9 +2,9 @@
Welcome to django-admin2's documentation! Welcome to django-admin2's documentation!
========================================= =========================================
.. image:: https://travis-ci.org/jazzband/django-admin2.png .. image:: https://github.com/jazzband/django-admin2/workflows/Test/badge.svg
:alt: Build Status :target: https://github.com/jazzband/django-admin2/actions
:target: https://travis-ci.org/jazzband/django-admin2 :alt: GitHub Actions
**Warning:** This project is currently in an **alpha** state and currently not meant for real projects. **Warning:** This project is currently in an **alpha** state and currently not meant for real projects.

View file

@ -71,7 +71,7 @@ Marking strings for translation
**Python code** **Python code**
Make sure to use ugettext or ugettext_lazy on strings that will be shown to the users, Make sure to use gettext or gettext_lazy on strings that will be shown to the users,
with string interpolation ( "%(name_of_variable)s" instead of "%s" ) where needed. with string interpolation ( "%(name_of_variable)s" instead of "%s" ) where needed.
Remember that all languages do not use the same word order, so try to provide flexible strings to translate ! Remember that all languages do not use the same word order, so try to provide flexible strings to translate !

View file

@ -37,7 +37,6 @@ Then enter the following information (you will probably want to change the highl
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup from setuptools import setup
import re import re

View file

@ -1,25 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext_lazy, pgettext_lazy from django.utils.translation import gettext_lazy, pgettext_lazy
from djadmin2 import permissions from djadmin2 import permissions
from djadmin2.actions import BaseListAction from djadmin2.actions import BaseListAction
class CustomPublishAction(BaseListAction): class CustomPublishAction(BaseListAction):
permission_classes = BaseListAction.permission_classes + ( permission_classes = BaseListAction.permission_classes + (
permissions.ModelChangePermission, permissions.ModelChangePermission,
) )
description = ugettext_lazy('Publish selected items') description = gettext_lazy('Publish selected items')
success_message = pgettext_lazy('singular form', success_message = pgettext_lazy(
'Successfully published %(count)s %(items)s') 'singular form',
success_message_plural = pgettext_lazy('plural form', 'Successfully published %(count)s %(items)s')
'Successfully published %(count)s %(items)s') success_message_plural = pgettext_lazy(
'plural form',
'Successfully published %(count)s %(items)s')
default_template_name = "actions/publish_selected_items.html" default_template_name = "actions/publish_selected_items.html"
@ -32,7 +30,7 @@ class PublishAllItemsAction(BaseListAction):
permissions.ModelChangePermission, permissions.ModelChangePermission,
) )
description = ugettext_lazy('Publish all items') description = gettext_lazy('Publish all items')
success_message = pgettext_lazy( success_message = pgettext_lazy(
'singular form', 'singular form',
'Successfully published %(count)s %(items)s', 'Successfully published %(count)s %(items)s',
@ -52,10 +50,12 @@ class PublishAllItemsAction(BaseListAction):
def unpublish_items(request, queryset): def unpublish_items(request, queryset):
queryset.update(published=False) queryset.update(published=False)
messages.add_message(request, messages.INFO, ugettext_lazy(u'Items unpublished')) messages.add_message(request, messages.INFO,
gettext_lazy(u'Items unpublished'))
# Translators : action description # Translators : action description
unpublish_items.description = ugettext_lazy('Unpublish selected items') unpublish_items.description = gettext_lazy('Unpublish selected items')
def unpublish_all_items(request, queryset): def unpublish_all_items(request, queryset):
@ -63,8 +63,9 @@ def unpublish_all_items(request, queryset):
messages.add_message( messages.add_message(
request, request,
messages.INFO, messages.INFO,
ugettext_lazy('Items unpublished'), gettext_lazy('Items unpublished'),
) )
unpublish_all_items.description = ugettext_lazy('Unpublish all items')
unpublish_all_items.description = gettext_lazy('Unpublish all items')
unpublish_all_items.only_selected = False unpublish_all_items.only_selected = False

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import Post, Comment from .models import Post, Comment
@ -16,5 +13,6 @@ class PostAdmin(admin.ModelAdmin):
list_filter = ['published', 'title'] list_filter = ['published', 'title']
date_hierarchy = "published_date" date_hierarchy = "published_date"
admin.site.register(Post, PostAdmin) admin.site.register(Post, PostAdmin)
admin.site.register(Comment) admin.site.register(Comment)

View file

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy from django.utils.translation import gettext_lazy
from djadmin2 import renderers from djadmin2 import renderers
from djadmin2.actions import DeleteSelectedAction from djadmin2.actions import DeleteSelectedAction
@ -32,7 +30,7 @@ class PostAdmin(ModelAdmin2):
} }
save_on_top = True save_on_top = True
date_hierarchy = "published_date" date_hierarchy = "published_date"
ordering = ["-published_date", "title",] ordering = ["-published_date", "title", ]
class CommentAdmin(ModelAdmin2): class CommentAdmin(ModelAdmin2):
@ -42,13 +40,13 @@ class CommentAdmin(ModelAdmin2):
actions_on_bottom = True actions_on_bottom = True
actions_selection_counter = False actions_selection_counter = False
# Register the blog app with a verbose name # Register the blog app with a verbose name
djadmin2_site.register_app_verbose_name( djadmin2_site.register_app_verbose_name(
'blog', 'blog',
ugettext_lazy('My Blog') gettext_lazy('My Blog')
) )
# Register each model with the admin # Register each model with the admin
djadmin2_site.register(Post, PostAdmin) djadmin2_site.register(Post, PostAdmin)
djadmin2_site.register(Comment, CommentAdmin) djadmin2_site.register(Comment, CommentAdmin)

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -27,7 +24,8 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('num', models.PositiveSmallIntegerField()), ('num', models.PositiveSmallIntegerField()),
('parent', models.ForeignKey(to='blog.Count', null=True)), ('parent', models.ForeignKey(
to='blog.Count', null=True, on_delete=models.CASCADE)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
@ -49,7 +47,9 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('name', models.CharField(max_length=255)), ('name', models.CharField(max_length=255)),
('event', models.OneToOneField(to='blog.Event')), ('event', models.OneToOneField(
to='blog.Event',
on_delete=models.CASCADE)),
], ],
options={ options={
'verbose_name': 'awesome guest', 'verbose_name': 'awesome guest',
@ -59,7 +59,10 @@ class Migration(migrations.Migration):
name='Location', name='Location',
fields=[ fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('event', models.OneToOneField(verbose_name='awesome event', to='blog.Event')), ('event', models.OneToOneField(
verbose_name='awesome event',
to='blog.Event',
on_delete=models.CASCADE)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
@ -79,6 +82,10 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='comment', model_name='comment',
name='post', name='post',
field=models.ForeignKey(related_name='comments', verbose_name='post', to='blog.Post'), field=models.ForeignKey(
related_name='comments',
verbose_name='post',
to='blog.Post',
on_delete=models.CASCADE),
), ),
] ]

View file

@ -1,13 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.db import models from django.db import models
from django.utils import six from django.utils.translation import gettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
@python_2_unicode_compatible
class Post(models.Model): class Post(models.Model):
title = models.CharField(max_length=255, verbose_name=_('title')) title = models.CharField(max_length=255, verbose_name=_('title'))
body = models.TextField(verbose_name=_('body')) body = models.TextField(verbose_name=_('body'))
@ -22,10 +16,10 @@ class Post(models.Model):
verbose_name_plural = _('posts') verbose_name_plural = _('posts')
@python_2_unicode_compatible
class Comment(models.Model): class Comment(models.Model):
post = models.ForeignKey( post = models.ForeignKey(
Post, verbose_name=_('post'), related_name="comments") Post, verbose_name=_('post'), related_name="comments",
on_delete=models.CASCADE)
body = models.TextField(verbose_name=_('body')) body = models.TextField(verbose_name=_('body'))
def __str__(self): def __str__(self):
@ -38,13 +32,12 @@ class Comment(models.Model):
# Models needed for testing NestedObjects # Models needed for testing NestedObjects
@python_2_unicode_compatible
class Count(models.Model): class Count(models.Model):
num = models.PositiveSmallIntegerField() num = models.PositiveSmallIntegerField()
parent = models.ForeignKey('self', null=True) parent = models.ForeignKey('self', null=True, on_delete=models.CASCADE)
def __str__(self): def __str__(self):
return six.text_type(self.num) return str(self.num)
class Event(models.Model): class Event(models.Model):
@ -52,11 +45,14 @@ class Event(models.Model):
class Location(models.Model): class Location(models.Model):
event = models.OneToOneField(Event, verbose_name='awesome event') event = models.OneToOneField(
Event, verbose_name='awesome event',
on_delete=models.CASCADE
)
class Guest(models.Model): class Guest(models.Model):
event = models.OneToOneField(Event) event = models.OneToOneField(Event, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
class Meta: class Meta:

View file

@ -1,4 +1,4 @@
{% load i18n %}{% load staticfiles %}<!DOCTYPE html> {% load i18n %}{% load static %}<!DOCTYPE html>
<html lang="{{ request.LANGUAGE_CODE }}"> <html lang="{{ request.LANGUAGE_CODE }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">

View file

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load staticfiles %} {% load static %}
{% block content %} {% block content %}
<div class="row"> <div class="row">

View file

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n staticfiles %} {% load i18n static %}
{% block content %} {% block content %}
<div class="row"> <div class="row">

View file

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load staticfiles i18n %} {% load static i18n %}
{% block content %} {% block content %}
<div class="row"> <div class="row">

View file

@ -1,13 +1,11 @@
from __future__ import unicode_literals
import json import json
from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.models import AnonymousUser, User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils.encoding import force_text from django.urls import reverse
from django.utils.encoding import force_str
from djadmin2 import apiviews from djadmin2 import apiviews
from djadmin2.site import djadmin2_site from djadmin2.site import djadmin2_site
@ -32,14 +30,16 @@ class IndexAPIViewTest(APITestCase):
def test_response_ok(self): def test_response_ok(self):
request = self.factory.get(reverse('admin2:api_index')) request = self.factory.get(reverse('admin2:api_index'))
request.user = self.user request.user = self.user
view = apiviews.IndexAPIView.as_view(**djadmin2_site.get_api_index_kwargs()) view = apiviews.IndexAPIView.as_view(
**djadmin2_site.get_api_index_kwargs())
response = view(request) response = view(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_view_permission(self): def test_view_permission(self):
request = self.factory.get(reverse('admin2:api_index')) request = self.factory.get(reverse('admin2:api_index'))
request.user = AnonymousUser() request.user = AnonymousUser()
view = apiviews.IndexAPIView.as_view(**djadmin2_site.get_api_index_kwargs()) view = apiviews.IndexAPIView.as_view(
**djadmin2_site.get_api_index_kwargs())
self.assertRaises(PermissionDenied, view, request) self.assertRaises(PermissionDenied, view, request)
@ -72,7 +72,7 @@ class ListCreateAPIViewTest(APITestCase):
response.render() response.render()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn('"__unicode__":"Foo"', force_text(response.content)) self.assertIn('"__unicode__":"Foo"', force_str(response.content))
def test_pagination(self): def test_pagination(self):
request = self.factory.get(reverse('admin2:blog_post_api_list')) request = self.factory.get(reverse('admin2:blog_post_api_list'))
@ -84,7 +84,7 @@ class ListCreateAPIViewTest(APITestCase):
response.render() response.render()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = json.loads(force_text(response.content)) data = json.loads(force_str(response.content))
self.assertEqual(data['count'], 0) self.assertEqual(data['count'], 0)
# next and previous fields exist, but are null because we have no # next and previous fields exist, but are null because we have no
# content # content
@ -99,7 +99,7 @@ class RetrieveUpdateDestroyAPIViewTest(APITestCase):
post = Post.objects.create(title='Foo', body='Bar') post = Post.objects.create(title='Foo', body='Bar')
request = self.factory.get( request = self.factory.get(
reverse('admin2:blog_post_api_detail', reverse('admin2:blog_post_api_detail',
kwargs={'pk': post.pk})) kwargs={'pk': post.pk}))
request.user = self.user request.user = self.user
model_admin = self.get_model_admin(Post) model_admin = self.get_model_admin(Post)
view = apiviews.RetrieveUpdateDestroyAPIView.as_view( view = apiviews.RetrieveUpdateDestroyAPIView.as_view(
@ -111,7 +111,7 @@ class RetrieveUpdateDestroyAPIViewTest(APITestCase):
post = Post.objects.create(title='Foo', body='Bar') post = Post.objects.create(title='Foo', body='Bar')
request = self.factory.get( request = self.factory.get(
reverse('admin2:blog_post_api_detail', reverse('admin2:blog_post_api_detail',
kwargs={'pk': post.pk})) kwargs={'pk': post.pk}))
request.user = AnonymousUser() request.user = AnonymousUser()
model_admin = self.get_model_admin(Post) model_admin = self.get_model_admin(Post)
view = apiviews.RetrieveUpdateDestroyAPIView.as_view( view = apiviews.RetrieveUpdateDestroyAPIView.as_view(

View file

@ -1,5 +1,5 @@
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.core.urlresolvers import reverse from django.urls import reverse
from .test_apiviews import APITestCase from .test_apiviews import APITestCase

View file

@ -1,10 +1,7 @@
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
import django_filters import django_filters
from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls import reverse
from djadmin2 import filters as djadmin2_filters from djadmin2 import filters as djadmin2_filters
from djadmin2.types import ModelAdmin2 from djadmin2.types import ModelAdmin2
@ -20,10 +17,9 @@ class ListFilterBuilderTest(TestCase):
class PostAdminSimple(ModelAdmin2): class PostAdminSimple(ModelAdmin2):
list_filter = ['published', ] list_filter = ['published', ]
class PostAdminWithFilterInstances(ModelAdmin2): class PostAdminWithFilterInstances(ModelAdmin2):
list_filter = [ list_filter = [
django_filters.BooleanFilter(name='published'), django_filters.BooleanFilter(field_name='published'),
] ]
class FS(django_filters.FilterSet): class FS(django_filters.FilterSet):
@ -31,7 +27,6 @@ class ListFilterBuilderTest(TestCase):
model = Post model = Post
fields = ['published'] fields = ['published']
class PostAdminWithFilterSetInst(ModelAdmin2): class PostAdminWithFilterSetInst(ModelAdmin2):
list_filter = FS list_filter = FS
@ -46,19 +41,11 @@ class ListFilterBuilderTest(TestCase):
self.assertTrue( self.assertTrue(
issubclass(list_filter_inst.__class__, django_filters.FilterSet) issubclass(list_filter_inst.__class__, django_filters.FilterSet)
) )
self.assertEqual(
list_filter_inst.filters['published'].widget,
djadmin2_filters.NullBooleanLinksWidget,
)
list_filter_inst = djadmin2_filters.build_list_filter( list_filter_inst = djadmin2_filters.build_list_filter(
request, request,
PostAdminWithFilterInstances, PostAdminWithFilterInstances,
Post.objects.all(), Post.objects.all(),
) )
self.assertNotEqual(
list_filter_inst.filters['published'].widget,
djadmin2_filters.NullBooleanLinksWidget,
)
list_filter_inst = djadmin2_filters.build_list_filter( list_filter_inst = djadmin2_filters.build_list_filter(
request, request,
PostAdminWithFilterSetInst, PostAdminWithFilterSetInst,

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django import forms from django import forms
from django.forms import modelform_factory from django.forms import modelform_factory
from django.test import TestCase from django.test import TestCase

View file

@ -1,10 +1,10 @@
from blog.models import Post from blog.models import Post
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template import Template, Context from django.template import Template, Context
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls import reverse
from djadmin2.permissions import TemplatePermissionChecker from djadmin2.permissions import TemplatePermissionChecker
from djadmin2.site import djadmin2_site from djadmin2.site import djadmin2_site
@ -71,11 +71,14 @@ class TemplatePermissionTest(TestCase):
result = self.render('{{ permissions.has_add_permission }}', context) result = self.render('{{ permissions.has_add_permission }}', context)
self.assertEqual(result, 'True') self.assertEqual(result, 'True')
result = self.render('{{ permissions.blog_post.has_add_permission }}', context) result = self.render(
'{{ permissions.blog_post.has_add_permission }}', context)
self.assertEqual(result, 'True') self.assertEqual(result, 'True')
result = self.render('{{ permissions.blog_post.has_change_permission }}', context) result = self.render(
'{{ permissions.blog_post.has_change_permission }}', context)
self.assertEqual(result, 'False') self.assertEqual(result, 'False')
result = self.render('{{ permissions.auth_user.has_delete_permission }}', context) result = self.render(
'{{ permissions.auth_user.has_delete_permission }}', context)
self.assertEqual(result, 'False') self.assertEqual(result, 'False')
result = self.render( result = self.render(
@ -112,7 +115,7 @@ class TemplatePermissionTest(TestCase):
'{% load admin2_tags %}' '{% load admin2_tags %}'
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% with permissions|for_admin:post_admin as permissions %}' '{% with permissions|for_admin:post_admin as permissions %}'
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% endwith %}', '{% endwith %}',
context) context)
self.assertEqual(result, 'FalseFalse') self.assertEqual(result, 'FalseFalse')
@ -131,7 +134,7 @@ class TemplatePermissionTest(TestCase):
'{% load admin2_tags %}' '{% load admin2_tags %}'
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% with permissions|for_admin:post_admin as permissions %}' '{% with permissions|for_admin:post_admin as permissions %}'
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% endwith %}' '{% endwith %}'
'{{ permissions.blog_post.has_add_permission }}', '{{ permissions.blog_post.has_add_permission }}',
context) context)
@ -141,7 +144,7 @@ class TemplatePermissionTest(TestCase):
result = self.render( result = self.render(
'{% load admin2_tags %}' '{% load admin2_tags %}'
'{% with permissions|for_admin:"blog_post" as permissions %}' '{% with permissions|for_admin:"blog_post" as permissions %}'
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% endwith %}', '{% endwith %}',
context) context)
self.assertEqual(result, 'True') self.assertEqual(result, 'True')
@ -150,7 +153,7 @@ class TemplatePermissionTest(TestCase):
result = self.render( result = self.render(
'{% load admin2_tags %}' '{% load admin2_tags %}'
'{% with permissions|for_admin:"invalid_admin_name" as permissions %}' '{% with permissions|for_admin:"invalid_admin_name" as permissions %}'
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% endwith %}', '{% endwith %}',
context) context)
self.assertEqual(result, '') self.assertEqual(result, '')
@ -186,8 +189,8 @@ class TemplatePermissionTest(TestCase):
# user add permission # user add permission
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% with permissions|for_admin:"blog_post"|for_view:"add" as post_add_perm %}' '{% with permissions|for_admin:"blog_post"|for_view:"add" as post_add_perm %}'
# post add permission # post add permission
'{{ post_add_perm }}' '{{ post_add_perm }}'
'{% endwith %}', '{% endwith %}',
context) context)
self.assertEqual(result, 'FalseFalse') self.assertEqual(result, 'FalseFalse')
@ -212,8 +215,8 @@ class TemplatePermissionTest(TestCase):
# user add permission # user add permission
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% with permissions|for_admin:"blog_post"|for_view:"add" as post_add_perm %}' '{% with permissions|for_admin:"blog_post"|for_view:"add" as post_add_perm %}'
# post add permission # post add permission
'{{ post_add_perm }}' '{{ post_add_perm }}'
'{% endwith %}' '{% endwith %}'
# user change permission # user change permission
'{{ permissions|for_view:"change" }}', '{{ permissions|for_view:"change" }}',
@ -224,13 +227,13 @@ class TemplatePermissionTest(TestCase):
result = self.render( result = self.render(
'{% load admin2_tags %}' '{% load admin2_tags %}'
'{% with permissions|for_view:"change" as user_change_perm %}' '{% with permissions|for_view:"change" as user_change_perm %}'
'1{{ user_change_perm }}' '1{{ user_change_perm }}'
'2{{ user_change_perm|for_view:"add" }}' '2{{ user_change_perm|for_view:"add" }}'
# this shouldn't return True or False but '' since the # this shouldn't return True or False but '' since the
# previously bound change view doesn't belong to the newly # previously bound change view doesn't belong to the newly
# bound blog_post admin # bound blog_post admin
'3{{ user_change_perm|for_admin:"blog_post" }}' '3{{ user_change_perm|for_admin:"blog_post" }}'
'4{{ user_change_perm|for_admin:"blog_post"|for_view:"add" }}' '4{{ user_change_perm|for_admin:"blog_post"|for_view:"add" }}'
'{% endwith %}', '{% endwith %}',
context) context)
self.assertEqual(result, '1True2False34True') self.assertEqual(result, '1True2False34True')
@ -282,7 +285,7 @@ class TemplatePermissionTest(TestCase):
'{% load admin2_tags %}' '{% load admin2_tags %}'
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% with permissions|for_object:post as permissions %}' '{% with permissions|for_object:post as permissions %}'
'{{ permissions.has_add_permission }}' '{{ permissions.has_add_permission }}'
'{% endwith %}', '{% endwith %}',
context) context)
self.assertEqual(result, 'TrueFalse') self.assertEqual(result, 'TrueFalse')

View file

@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime from datetime import datetime
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from django.test import TestCase, Client from django.test import TestCase, Client
from django.utils.encoding import force_text from django.urls import reverse
from django.utils.encoding import force_str
from ..models import Post, Comment from ..models import Post, Comment
@ -140,7 +137,7 @@ class PostListTest(BaseIntegrationTest):
def test_actions_displayed(self): def test_actions_displayed(self):
response = self.client.get(reverse("admin2:blog_post_index")) response = self.client.get(reverse("admin2:blog_post_index"))
self.assertInHTML( self.assertInHTML(
'<a tabindex="-1" href="#" data-name="action" data-value="DeleteSelectedAction">Delete selected items</a>', force_text(response.content)) '<a tabindex="-1" href="#" data-name="action" data-value="DeleteSelectedAction">Delete selected items</a>', force_str(response.content))
def test_actions_displayed_twice(self): def test_actions_displayed_twice(self):
# If actions_on_top and actions_on_bottom are both set # If actions_on_top and actions_on_bottom are both set
@ -155,7 +152,7 @@ class PostListTest(BaseIntegrationTest):
response = self.client.post(reverse("admin2:blog_post_index"), params) response = self.client.post(reverse("admin2:blog_post_index"), params)
# caution : uses pluralization # caution : uses pluralization
self.assertInHTML( self.assertInHTML(
'<p>Are you sure you want to delete the selected post? The following item will be deleted:</p>', force_text(response.content)) '<p>Are you sure you want to delete the selected post? The following item will be deleted:</p>', force_str(response.content))
def test_delete_selected_post_confirmation(self): def test_delete_selected_post_confirmation(self):
post = Post.objects.create(title="A Post Title", body="body") post = Post.objects.create(title="A Post Title", body="body")
@ -331,7 +328,7 @@ class PostListTestCustomAction(BaseIntegrationTest):
def test_publish_action_displayed_in_list(self): def test_publish_action_displayed_in_list(self):
response = self.client.get(reverse("admin2:blog_post_index")) response = self.client.get(reverse("admin2:blog_post_index"))
self.assertInHTML( self.assertInHTML(
'<a tabindex="-1" href="#" data-name="action" data-value="CustomPublishAction">Publish selected items</a>', force_text(response.content)) '<a tabindex="-1" href="#" data-name="action" data-value="CustomPublishAction">Publish selected items</a>', force_str(response.content))
def test_publish_selected_items(self): def test_publish_selected_items(self):
post = Post.objects.create(title="A Post Title", post = Post.objects.create(title="A Post Title",
@ -350,7 +347,7 @@ class PostListTestCustomAction(BaseIntegrationTest):
def test_unpublish_action_displayed_in_list(self): def test_unpublish_action_displayed_in_list(self):
response = self.client.get(reverse("admin2:blog_post_index")) response = self.client.get(reverse("admin2:blog_post_index"))
self.assertInHTML( self.assertInHTML(
'<a tabindex="-1" href="#" data-name="action" data-value="unpublish_items">Unpublish selected items</a>', force_text(response.content)) '<a tabindex="-1" href="#" data-name="action" data-value="unpublish_items">Unpublish selected items</a>', force_str(response.content))
def test_unpublish_selected_items(self): def test_unpublish_selected_items(self):
post = Post.objects.create(title="A Post Title", post = Post.objects.create(title="A Post Title",
@ -380,7 +377,7 @@ class PostCreateViewTest(BaseIntegrationTest):
def test_view_ok(self): def test_view_ok(self):
response = self.client.get(reverse("admin2:blog_post_create")) response = self.client.get(reverse("admin2:blog_post_create"))
self.assertNotIn( self.assertNotIn(
'''enctype="multipart/form-data"''', force_text(response.content)) '''enctype="multipart/form-data"''', force_str(response.content))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_create_post(self): def test_create_post(self):

View file

@ -1,4 +1,3 @@
#from django.shortcuts import render
from django.views.generic import ListView, DetailView from django.views.generic import ListView, DetailView
from .models import Post from .models import Post
@ -7,6 +6,7 @@ from .models import Post
class BlogListView(ListView): class BlogListView(ListView):
model = Post model = Post
template_name = 'blog_list.html' template_name = 'blog_list.html'
paginate_by = 10
class BlogDetailView(DetailView): class BlogDetailView(DetailView):

Binary file not shown.

View file

@ -15,6 +15,8 @@ import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ['DJANGO_SETTINGS_MODULE'] = 'example.settings'
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
@ -40,6 +42,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# Uncomment the next line to enable admin documentation: # Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs', # 'django.contrib.admindocs',
'django_filters',
'rest_framework', 'rest_framework',
'djadmin2', 'djadmin2',
'djadmin2.tests', 'djadmin2.tests',
@ -49,14 +52,13 @@ INSTALLED_APPS = [
'polls' 'polls'
] ]
MIDDLEWARE_CLASSES = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
@ -66,7 +68,7 @@ ROOT_URLCONF = 'example.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -145,6 +147,13 @@ MEDIA_URL = "/media/"
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10 'PAGE_SIZE': 10,
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',)
} }
ADMIN2_THEME_DIRECTORY = "djadmin2theme_bootstrap3" ADMIN2_THEME_DIRECTORY = "djadmin2theme_bootstrap3"

View file

@ -1,20 +1,26 @@
from __future__ import unicode_literals
from blog.views import BlogListView, BlogDetailView from blog.views import BlogListView, BlogDetailView
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import re_path
from djadmin2.site import djadmin2_site from djadmin2.site import djadmin2_site
admin.autodiscover()
djadmin2_site.autodiscover() djadmin2_site.autodiscover()
urlpatterns = [ urlpatterns = [
url(r'^admin2/', include(djadmin2_site.urls)), re_path(r"^admin2/", djadmin2_site.urls),
url(r'^admin/', include(admin.site.urls)), re_path(r"^admin/", admin.site.urls),
url(r'^blog/', BlogListView.as_view(template_name="blog/blog_list.html"), name='blog_list'), re_path(
url(r'^blog/detail(?P<pk>\d+)/$', BlogDetailView.as_view(template_name="blog/blog_detail.html"), name='blog_detail'), r"^blog/",
url(r'^$', BlogListView.as_view(template_name="blog/home.html"), name='home'), BlogListView.as_view(template_name="blog/blog_list.html"),
name="blog_list",
),
re_path(
r"^blog/detail(?P<pk>\d+)/$",
BlogDetailView.as_view(template_name="blog/blog_detail.html"),
name="blog_detail",
),
re_path(r"^$", BlogListView.as_view(template_name="blog/home.html"), name="home"),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import CaptionedFile, UncaptionedFile from .models import CaptionedFile, UncaptionedFile

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from djadmin2.site import djadmin2_site from djadmin2.site import djadmin2_site
from .models import CaptionedFile, UncaptionedFile from .models import CaptionedFile, UncaptionedFile

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View file

@ -1,12 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
@python_2_unicode_compatible
class CaptionedFile(models.Model): class CaptionedFile(models.Model):
caption = models.CharField(max_length=200, verbose_name=_('caption')) caption = models.CharField(max_length=200, verbose_name=_('caption'))
publication = models.FileField(upload_to='captioned-files', verbose_name=_('Uploaded File')) publication = models.FileField(upload_to='captioned-files', verbose_name=_('Uploaded File'))
@ -19,7 +14,6 @@ class CaptionedFile(models.Model):
verbose_name_plural = _('Captioned Files') verbose_name_plural = _('Captioned Files')
@python_2_unicode_compatible
class UncaptionedFile(models.Model): class UncaptionedFile(models.Model):
publication = models.FileField(upload_to='uncaptioned-files', verbose_name=_('Uploaded File')) publication = models.FileField(upload_to='uncaptioned-files', verbose_name=_('Uploaded File'))

View file

@ -1,6 +1,8 @@
from os import path from os import path
from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
from files.models import CaptionedFile from files.models import CaptionedFile

View file

@ -1,9 +1,9 @@
from os import path from os import path
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse from django.urls import reverse
from django.test import TestCase, Client from django.test import TestCase, Client
from django.utils.encoding import force_text from django.utils.encoding import force_str
from ..models import CaptionedFile from ..models import CaptionedFile
@ -45,7 +45,7 @@ class CaptionedFileListTest(BaseIntegrationTest):
def test_actions_displayed(self): def test_actions_displayed(self):
response = self.client.get(reverse("admin2:files_captionedfile_index")) response = self.client.get(reverse("admin2:files_captionedfile_index"))
self.assertInHTML( self.assertInHTML(
'<a tabindex="-1" href="#" data-name="action" data-value="DeleteSelectedAction">Delete selected items</a>', force_text(response.content)) '<a tabindex="-1" href="#" data-name="action" data-value="DeleteSelectedAction">Delete selected items</a>', force_str(response.content))
def test_delete_selected_captioned_file(self): def test_delete_selected_captioned_file(self):
captioned_file = CaptionedFile.objects.create( captioned_file = CaptionedFile.objects.create(
@ -55,7 +55,7 @@ class CaptionedFileListTest(BaseIntegrationTest):
response = self.client.post( response = self.client.post(
reverse("admin2:files_captionedfile_index"), params) reverse("admin2:files_captionedfile_index"), params)
self.assertInHTML( self.assertInHTML(
'<p>Are you sure you want to delete the selected Captioned File? The following item will be deleted:</p>', force_text(response.content)) '<p>Are you sure you want to delete the selected Captioned File? The following item will be deleted:</p>', force_str(response.content))
def test_delete_selected_captioned_file_confirmation(self): def test_delete_selected_captioned_file_confirmation(self):
captioned_file = CaptionedFile.objects.create( captioned_file = CaptionedFile.objects.create(
@ -93,7 +93,7 @@ class CaptionedFileCreateViewTest(BaseIntegrationTest):
response = self.client.get( response = self.client.get(
reverse("admin2:files_captionedfile_create")) reverse("admin2:files_captionedfile_create"))
self.assertIn( self.assertIn(
'enctype="multipart/form-data"', force_text(response.content)) 'enctype="multipart/form-data"', force_str(response.content))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_create_captioned_file(self): def test_create_captioned_file(self):

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
import os import os
import sys import sys

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import Poll, Choice from .models import Poll, Choice

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from djadmin2.site import djadmin2_site from djadmin2.site import djadmin2_site
from djadmin2.types import Admin2TabularInline, ModelAdmin2 from djadmin2.types import Admin2TabularInline, ModelAdmin2
from .models import Poll, Choice from .models import Poll, Choice
@ -8,7 +5,7 @@ from .models import Poll, Choice
class ChoiceInline(Admin2TabularInline): class ChoiceInline(Admin2TabularInline):
model = Choice model = Choice
extra = 3 fields = '__all__'
class PollAdmin(ModelAdmin2): class PollAdmin(ModelAdmin2):

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -37,6 +34,9 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='choice', model_name='choice',
name='poll', name='poll',
field=models.ForeignKey(verbose_name='poll', to='polls.Poll'), field=models.ForeignKey(
verbose_name='poll',
to='polls.Poll',
on_delete=models.CASCADE),
), ),
] ]

View file

@ -1,15 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
import datetime import datetime
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
@python_2_unicode_compatible
class Poll(models.Model): class Poll(models.Model):
question = models.CharField(max_length=200, verbose_name=_('question')) question = models.CharField(max_length=200, verbose_name=_('question'))
pub_date = models.DateTimeField(verbose_name=_('date published')) pub_date = models.DateTimeField(verbose_name=_('date published'))
@ -28,9 +23,12 @@ class Poll(models.Model):
verbose_name_plural = _('polls') verbose_name_plural = _('polls')
@python_2_unicode_compatible
class Choice(models.Model): class Choice(models.Model):
poll = models.ForeignKey(Poll, verbose_name=_('poll')) poll = models.ForeignKey(
Poll,
verbose_name=_('poll'),
on_delete=models.CASCADE
)
choice_text = models.CharField( choice_text = models.CharField(
max_length=200, verbose_name=_('choice text')) max_length=200, verbose_name=_('choice text'))
votes = models.IntegerField(default=0, verbose_name=_('votes')) votes = models.IntegerField(default=0, verbose_name=_('votes'))

View file

@ -1,8 +1,8 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.test import TestCase, Client from django.test import TestCase, Client
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_str
from ..models import Poll from ..models import Poll
@ -40,7 +40,7 @@ class PollListTest(BaseIntegrationTest):
def test_actions_displayed(self): def test_actions_displayed(self):
response = self.client.get(reverse("admin2:polls_poll_index")) response = self.client.get(reverse("admin2:polls_poll_index"))
self.assertInHTML( self.assertInHTML(
'<a tabindex="-1" href="#" data-name="action" data-value="DeleteSelectedAction">Delete selected items</a>', force_text(response.content)) '<a tabindex="-1" href="#" data-name="action" data-value="DeleteSelectedAction">Delete selected items</a>', force_str(response.content))
def test_delete_selected_poll(self): def test_delete_selected_poll(self):
poll = Poll.objects.create( poll = Poll.objects.create(
@ -49,7 +49,7 @@ class PollListTest(BaseIntegrationTest):
'selected_model_pk': str(poll.pk)} 'selected_model_pk': str(poll.pk)}
response = self.client.post(reverse("admin2:polls_poll_index"), params) response = self.client.post(reverse("admin2:polls_poll_index"), params)
self.assertInHTML( self.assertInHTML(
'<p>Are you sure you want to delete the selected poll? The following item will be deleted:</p>', force_text(response.content)) '<p>Are you sure you want to delete the selected poll? The following item will be deleted:</p>', force_str(response.content))
def test_delete_selected_poll_confirmation(self): def test_delete_selected_poll_confirmation(self):
poll = Poll.objects.create( poll = Poll.objects.create(

3
fabfile.py vendored
View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, division, absolute_import, unicode_literals
from fabric.api import local, lcd from fabric.api import local, lcd
from fabric.contrib.console import confirm from fabric.contrib.console import confirm

View file

@ -1,7 +1,6 @@
django-extra-views<=0.7.1 django-extra-views
django-braces>=1.3.0 django-braces
djangorestframework<=3.3.3 djangorestframework
django-filter>=0.15.3 django-filter
django-debug-toolbar>=1.5 django-debug-toolbar
future>=0.15.2 pytz
pytz>=2016.4

View file

@ -1,4 +1,5 @@
-rrequirements.txt -rrequirements.txt
flake8==2.5.4 flake8>=2.5.4
pytest pytest
pytest-django pytest-django
pytest-cov

View file

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
from setuptools import setup from setuptools import setup
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
@ -81,6 +80,7 @@ if sys.argv[-1] == 'publish':
LONG_DESCRIPTION = remove_screenshots(open('README.rst').read()) LONG_DESCRIPTION = remove_screenshots(open('README.rst').read())
HISTORY = open('HISTORY.rst').read() HISTORY = open('HISTORY.rst').read()
class PyTest(TestCommand): class PyTest(TestCommand):
user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
@ -100,6 +100,7 @@ class PyTest(TestCommand):
errno = pytest.main(self.pytest_args) errno = pytest.main(self.pytest_args)
sys.exit(errno) sys.exit(errno)
setup( setup(
name='django-admin2', name='django-admin2',
version=version, version=version,
@ -112,8 +113,10 @@ setup(
"License :: OSI Approved :: BSD License", "License :: OSI Approved :: BSD License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: JavaScript", "Programming Language :: JavaScript",
"Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
@ -127,13 +130,12 @@ setup(
include_package_data=True, include_package_data=True,
#test_suite='runtests.runtests', #test_suite='runtests.runtests',
install_requires=[ install_requires=[
'django>=1.8.0', 'django>=2.2',
'django-extra-views<=0.7.1', 'django-extra-views>=0.12.0',
'django-braces>=1.3.0', 'django-braces>=1.3.0',
'djangorestframework<=3.3.3', 'djangorestframework>=3.11.1',
'django-filter>=0.15.3', 'django-filter>=2.4.0',
'pytz==2016.4', 'pytz>=2016.4',
'future>=0.15.2',
], ],
extras_require={ extras_require={
'testing': ['pytest', 'pytest-django', 'pytest-ipdb'], 'testing': ['pytest', 'pytest-django', 'pytest-ipdb'],

28
tox.ini
View file

@ -6,21 +6,29 @@ exclude = migrations/*,docs/*
[tox] [tox]
envlist = envlist =
py27-{1.8,1.9,1.10,master}, py35-{2.2},
py33-{1.8}, py36-{2.2,3.1},
py34-{1.8,1.9,1.10,master}, py37-{2.2,3.1,3.2},
py35-{1.8,1.9,1.10,master}, py38-{2.2,3.1,3.2,main},
[gh-actions]
python =
3.5: py35
3.6: py36
3.7: py37
3.8: py38
[testenv] [testenv]
commands = commands =
flake8 djadmin2 pytest --cov-append --cov djadmin2 --cov-report=xml []
py.test []
deps = deps =
-rrequirements_test.txt -rrequirements_test.txt
1.8: Django>=1.8,<1.9 3.1: Django>=3.1,<3.2
1.9: Django>=1.9,<1.10 3.2: Django>=3.2,<4.0
1.10: Django>=1.10,<1.11 2.2: Django>=2.2,<2.3
master: https://github.com/django/django/tarball/master main: https://github.com/django/django/tarball/main
setenv= setenv=
DJANGO_SETTINGS_MODULE = example.settings DJANGO_SETTINGS_MODULE = example.settings
PYTHONPATH = {toxinidir}/example:{toxinidir} PYTHONPATH = {toxinidir}/example:{toxinidir}
PYTHONDONTWRITEBYTECODE=1
PYTHONWARNINGS=once