Repository: django-extensions/django-extensions
Pull request #1676 (2021-08-05 20:35:29)

Added "managestate" command
Hi!

I use this command in my projects and have decided it may make use to others. I saw that a quite similar [pull request](https://github.com/django-extensions/django-extensions/pull/1362) is still not accepted, rethought the idea a little, and described the profit of this feature in the [documentation](https://github.com/django-extensions/django-extensions/commit/696f6fb553e41b4fd9bc834bfddfafea11467043). 

I also took into account comments under that pull request and added the necessary tests.

Hope this may be useful!


Patch:
diff --git a/.github/workflows/compile_catalog.yml b/.github/workflows/compile_catalog.yml
index 5e408784..bcdea4cb 100644
--- a/.github/workflows/compile_catalog.yml
+++ b/.github/workflows/compile_catalog.yml
@@ -16,7 +16,7 @@ jobs:
       - name: Set up Python 3.x
         uses: actions/setup-python@v2
         with:
-          python-version: 3.9
+          python-version: "3.10"
       - run: python -m pip install tox
       - name: Compile Catalog
         run: tox
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index c2006298..641b7191 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -16,12 +16,12 @@ jobs:
       - name: Set up Python 3.x
         uses: actions/setup-python@v2
         with:
-          python-version: 3.9
+          python-version: "3.10"
       - run: python -m pip install tox
-      - name: tox py39-flake8
+      - name: tox py310-flake8
         run: tox
         env:
-          TOXENV: py39-flake8
+          TOXENV: py310-flake8
 
   mypy:
     name: mypy
@@ -32,7 +32,7 @@ jobs:
       - name: Set up Python 3.x
         uses: actions/setup-python@v2
         with:
-          python-version: 3.9
+          python-version: "3.10"
       - run: python -m pip install tox
       - name: tox mypy
         run: tox
diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml
index 85caad92..dd8cdd41 100644
--- a/.github/workflows/precommit.yml
+++ b/.github/workflows/precommit.yml
@@ -16,7 +16,7 @@ jobs:
       - name: Set up Python 3.x
         uses: actions/setup-python@v2
         with:
-          python-version: 3.9
+          python-version: "3.10"
       - run: python -m pip install tox
       - name: tox precommit
         run: tox
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index de6fa94e..f4a3430f 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -16,7 +16,7 @@ jobs:
       - name: Set up Python 3.x
         uses: actions/setup-python@v2
         with:
-          python-version: 3.9
+          python-version: "3.10"
       - run: python -m pip install tox
       - name: safety
         run: tox
diff --git a/.gitignore b/.gitignore
index 12224611..2275dd50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,4 @@ htmlcov/
 django-sample-app*/
 *.swp
 *.swo
+*.sqlite3
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f5983c24..efc9ac73 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,4 +1,4 @@
--   repo: git://github.com/pre-commit/pre-commit-hooks
+-   repo: https://github.com/pre-commit/pre-commit-hooks
     sha: v0.9.1
     hooks:
     -   id: trailing-whitespace
@@ -20,11 +20,11 @@
         args:
         - --django
         exclude: ^tests/testapp|^tests/management/|^tests/collisions/|^tests/pythonrc.py|^tests/runner.py
--   repo: git://github.com/Lucas-C/pre-commit-hooks.git
+-   repo: https://github.com/Lucas-C/pre-commit-hooks.git
     sha: v1.0.1
     hooks:
     -   id: forbid-crlf
--   repo: git://github.com/trbs/pre-commit-hooks-trbs.git
+-   repo: https://github.com/trbs/pre-commit-hooks-trbs.git
     sha: e233916fb2b4b9019b4a3cc0497994c7926fe36b
     hooks:
     -   id: forbid-executables
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c941e49..0416263a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,145 +1,150 @@
 Changelog
 =========
 
+See https://github.com/django-extensions/django-extensions/releases
+
+
 3.1.4
 -----
 
 Changes:
- - Fix: set_default_site, improve django.contrib.sites application detection
- - Improvements: documentation, Fix name of mixin in docs
- - Improvements: mypy, type ignore backwards compatible imports
- - Improvement: graph_models, add --rankdir to change graph direction
 
+- Fix: set_default_site, improve django.contrib.sites application detection
+- Improvement: documentation, Fix name of mixin in docs
+- Improvement: mypy, type ignore backwards compatible imports
+- Improvement: graph_models, add --rankdir to change graph direction
+- Improvement: runserver_plus, Add --sql-truncate cli modifier
+- Improvement: shell_plus, Add --sql-truncate cli modifier
 
 3.1.3
 -----
 
 Changes:
- - Fix: Django 3.2, Run tests against Django 3.2
- - Fix: Django 3.2, Handle warnings for default_app_config (#1654)
- - Fix: sqldiff, Fix for missing field/index in model case
 
+- Fix: Django 3.2, Run tests against Django 3.2
+- Fix: Django 3.2, Handle warnings for default_app_config (#1654)
+- Fix: sqldiff, Fix for missing field/index in model case
 
 3.1.2
 -----
 
 Changes:
- - Improvement: shell_plus, not save ipython history when using Jupyter
- - Improvement: docs, fix spelling mistakes
- - Improvement: tests, move to Github Actions instead of Travis
- - Improvement: drop_test_database, delete all cloned test databases (#1637)
- - Improvement: setup.py, Added minimum Django>=2.2 version to PyPI package
- - Improvement: shell_plus, fix --command globals / locals error
 
+- Improvement: shell_plus, not save ipython history when using Jupyter
+- Improvement: docs, fix spelling mistakes
+- Improvement: tests, move to Github Actions instead of Travis
+- Improvement: drop_test_database, delete all cloned test databases (#1637)
+- Improvement: setup.py, Added minimum Django>=2.2 version to PyPI package
+- Improvement: shell_plus, fix --command globals / locals error
 
 3.1.1.post1
 -----------
 
 Changes:
- - Improvement: setup.py, Added minimum Django>=2.2 version to PyPI package
 
+- Improvement: setup.py, Added minimum Django>=2.2 version to PyPI package
 
 3.1.1
 -----
 
 Changes:
- - Improvement: graph_models, add option --app-labels
- - Improvement: shell_plus, update shell_plus for jupyterlab 3
- - Improvement: tests, add Python 3.9
 
+- Improvement: graph_models, add option --app-labels
+- Improvement: shell_plus, update shell_plus for jupyterlab 3
+- Improvement: tests, add Python 3.9
 
 3.1.0
 -----
 
 Changes:
- - Improvement: pipchecker, sleep 60s if pypi raises a fault
- - Improvement: add django_zero_downtime_migrations to list of supported postgresql engines
- - Improvement: use list of supported database engines from settings for all database commands
- - Improvement: reset_db, documentation
- - Fix: tests, Python 3.9 fixes for some tests
- - Fix: runserver_plus, parsing of RUNSERVER_PLUS_EXTRA_FILES
 
+- Improvement: pipchecker, sleep 60s if pypi raises a fault
+- Improvement: add django_zero_downtime_migrations to list of supported postgresql engines
+- Improvement: use list of supported database engines from settings for all database commands
+- Improvement: reset_db, documentation
+- Fix: tests, Python 3.9 fixes for some tests
+- Fix: runserver_plus, parsing of RUNSERVER_PLUS_EXTRA_FILES
 
 3.0.9
 -----
 
 Changes:
- - Improvement: runserver_plus, survive syntax and configuration errors part II
- - Improvement: tests, refactor test runner
- - Improvement: sqlcreate, support postgresql unix domain socket
 
+- Improvement: runserver_plus, survive syntax and configuration errors part II
+- Improvement: tests, refactor test runner
+- Improvement: sqlcreate, support postgresql unix domain socket
 
 3.0.8
 -----
 
 Changes:
- - Improvement: setup.cfg, remove universal flag from wheel, we only support Python 3 and up
- - Improvement: sqlcreate, fixed mentioned of old syncdb
- - Fix: runserver_plus, stop catching SyntaxError since reload for it was not working properly
 
+- Improvement: setup.cfg, remove universal flag from wheel, we only support Python 3 and up
+- Improvement: sqlcreate, fixed mentioned of old syncdb
+- Fix: runserver_plus, stop catching SyntaxError since reload for it was not working properly
 
 3.0.7
 -----
 
 Changes:
- - Improvement: runserver_plus, gh #1575 survive syntax and configuration errors
- - Improvement: runscript, use exit-code 1 if script is not found
 
+- Improvement: runserver_plus, gh #1575 survive syntax and configuration errors
+- Improvement: runscript, use exit-code 1 if script is not found
 
 3.0.6
 -----
 
 Changes:
- - Improvement: runscript, add --continue-on-error unless set runscript will exit on errors
- - Improvement: runscript, allow to return exit-code
- - Improvement: runscript, support raise CommandError(... returncode=...)
- - Improvement: runscript, run Django checks() and check_migrations() before executing scripts
- - Improvement: shell_plus, set application name on all postgresql backends
 
+- Improvement: runscript, add --continue-on-error unless set runscript will exit on errors
+- Improvement: runscript, allow to return exit-code
+- Improvement: runscript, support raise CommandError(... returncode=...)
+- Improvement: runscript, run Django checks() and check_migrations() before executing scripts
+- Improvement: shell_plus, set application name on all postgresql backends
 
 3.0.5
 -----
 
 Changes:
- - Fix: runserver_plus, exceptions must derive from BaseException error
 
+- Fix: runserver_plus, exceptions must derive from BaseException error
 
 3.0.4
 -----
 
 Changes:
- - Various cleanups
- - Deprecated using `--router` instead use `--database`
- - Locales: Indonesian and Polish, updated
- - Improvement: show_dsn, fix crash with django-postgres-extra
- - Improvement: print_settings, added wildcard support
- - Improvement: print_settings, added --fail option
- - Improvement: delete_squashed_migrations, add --database option
- - Improvement: runserver_plus, added RUNSERVER_PLUS_EXTRA_FILES setting
- - Improvement: runserver_plus, added runserver_plus_started signal
 
+- Various cleanups
+- Deprecated using `--router` instead use `--database`
+- Locales: Indonesian and Polish, updated
+- Improvement: show_dsn, fix crash with django-postgres-extra
+- Improvement: print_settings, added wildcard support
+- Improvement: print_settings, added --fail option
+- Improvement: delete_squashed_migrations, add --database option
+- Improvement: runserver_plus, added RUNSERVER_PLUS_EXTRA_FILES setting
+- Improvement: runserver_plus, added runserver_plus_started signal
 
 3.0.3
 -----
 
 Changes:
- - New: InternalIPS, allows to specify CIDRs for INTERNAL_IPS
- - Docs: restructure toctree
 
+- New: InternalIPS, allows to specify CIDRs for INTERNAL_IPS
+- Docs: restructure toctree
 
 3.0.2
 -----
 
 Changes:
- - Fix: shell_plus, fix honouring SHELL_PLUS in settings.py
 
+- Fix: shell_plus, fix honouring SHELL_PLUS in settings.py
 
 3.0.1
 -----
 
 Changes:
- - Fix: setup.py, add python_requires and remove legacy trove classifiers
 
+- Fix: setup.py, add python_requires and remove legacy trove classifiers
 
 3.0.0
 -----
@@ -147,222 +152,222 @@ Changes:
 This is the first Django Extensions release which only targets Django 2.2 and above.
 It drops official support for Python 2.7.
 
-
 Changes:
- - Removal of Python 2 support
- - Removal of deprecated keyczar encrypted fields EncryptedTextField and EncryptedCharField
- - Removal of deprecated passwd command
- - Removal of truncate_letters filter
- - Change: TimeStampedModel; Removed default ordering on abstract model
- - New: DjangoExtensionsConfig AppConfig
- - New: shell_plus, JupyterLab support
- - New: list_signals, List all signals by model and signal type
- - Improvement: shell_plus, use -- to directly pass additional arguments to Jupyter
- - Improvement: shell_plus, improvements to MySQL support
- - Improvement: jobs, use logging to record errors
- - Improvement: syncdata, added --remove-before flag
- - Improvement: graph_models, add field and model to template context
- - Fix: syncdata, fix non existent field in fixture data
- - Fix: pipchecker, compatibility with pip 20.1
 
+- Removal of Python 2 support
+- Removal of deprecated keyczar encrypted fields EncryptedTextField and EncryptedCharField
+- Removal of deprecated passwd command
+- Removal of truncate_letters filter
+- Change: TimeStampedModel; Removed default ordering on abstract model
+- New: DjangoExtensionsConfig AppConfig
+- New: shell_plus, JupyterLab support
+- New: list_signals, List all signals by model and signal type
+- Improvement: shell_plus, use -- to directly pass additional arguments to Jupyter
+- Improvement: shell_plus, improvements to MySQL support
+- Improvement: jobs, use logging to record errors
+- Improvement: syncdata, added --remove-before flag
+- Improvement: graph_models, add field and model to template context
+- Fix: syncdata, fix non existent field in fixture data
+- Fix: pipchecker, compatibility with pip 20.1
 
 2.2.9
 -----
 
 Changes:
- - Fix: shell_plus, move notebook down the list of preferred shells
- - Fix: sqldiff, fix KeyError when detecting missing (unique) indexes
- - Improvement: encrypted fields, make it harder to use deprecated keyczar fields
- - Locale: Removed empty localizations
 
+- Fix: shell_plus, move notebook down the list of preferred shells
+- Fix: sqldiff, fix KeyError when detecting missing (unique) indexes
+- Improvement: encrypted fields, make it harder to use deprecated keyczar fields
+- Locale: Removed empty localizations
 
 2.2.8
 -----
 
 Changes:
- - Locale: zh_Hans, removed as it generated UnicodeDecodeError errors (#1478)
 
+- Locale: zh_Hans, removed as it generated UnicodeDecodeError errors (#1478)
 
 2.2.7
 -----
 
 Changes:
- - Improvement: shell_plus, #865 always add manage.py basedir to path for notebook kernel
- - Improvement: docs, add zh-Hans locale
- - Improvement: runserver_plus, fix broken import for werkzeug v1.0.0
- - Improvement: runserver_plus, #1461 fix always trying to load StaticFilesHandler
- - Improvement: pipchecker, #1471 fix import of PipSession
 
+- Improvement: shell_plus, #865 always add manage.py basedir to path for notebook kernel
+- Improvement: docs, add zh-Hans locale
+- Improvement: runserver_plus, fix broken import for werkzeug v1.0.0
+- Improvement: runserver_plus, #1461 fix always trying to load StaticFilesHandler
+- Improvement: pipchecker, #1471 fix import of PipSession
 
 2.2.6
 -----
 
 Changes:
- - Improvement: travis, update pypy and pypy3 versions
- - Improvement: shell_plus, ability to print location/traceback besides sql
- - Improvement: runserver_plus, ability to print location/traceback besides sql
- - Improvement: UniqueFieldMixin, Support Django 2.2 UniqueConstraint.condition
- - Improvement: DEFAULT_MYSQL_ENGINES, add mysql.connector.django
- - Improvement: shell_plus, allow setting SHELL_PLUS="notebook"
- - Improvement: shell_plus, add -c/--command to shell_plus mirroring django's shell command
- - Fix: shell_plus, fix postgresql debug wrapper on django 3.0 or higher
- - Fix: runserver_plus, fix postgresql debug wrapper on django 3.0 or higher
 
+- Improvement: travis, update pypy and pypy3 versions
+- Improvement: shell_plus, ability to print location/traceback besides sql
+- Improvement: runserver_plus, ability to print location/traceback besides sql
+- Improvement: UniqueFieldMixin, Support Django 2.2 UniqueConstraint.condition
+- Improvement: DEFAULT_MYSQL_ENGINES, add mysql.connector.django
+- Improvement: shell_plus, allow setting SHELL_PLUS="notebook"
+- Improvement: shell_plus, add -c/--command to shell_plus mirroring django's shell command
+- Fix: shell_plus, fix postgresql debug wrapper on django 3.0 or higher
+- Fix: runserver_plus, fix postgresql debug wrapper on django 3.0 or higher
 
 2.2.5
 -----
 
 Changes:
- - Improvement: travis, add Python 3.8
- - Improvement: setup.py, update classifiers
 
+- Improvement: travis, add Python 3.8
+- Improvement: setup.py, update classifiers
 
 2.2.4
 -----
 
 Changes:
- - Improvement: RandomCharField, Support unique_together
- - Improvement: export_emails, add settings for overriding queryset fields, order_by and the full_name function
 
+- Improvement: RandomCharField, Support unique_together
+- Improvement: export_emails, add settings for overriding queryset fields, order_by and the full_name function
 
 2.2.3
 -----
 
 Changes:
-  - Fix: admin widgets, fix import of static template tag (part 2)
+
+- Fix: admin widgets, fix import of static template tag (part 2)
 
 2.2.2
 -----
 
 Changes:
-  - Fix: autoslugfield, find unique method overrideable
-  - Fix: notes, do not replace dot in template dirs
-  - Fix: admin widgets, fix import of static template tag
-  - Improvement: print_user_for_session, use session backend
-  - Improvement: sqlcreate, postgis support
-  - Improvement: graph_models, permit combination of includes and excludes
-  - Improvement: Adds missing GIS engine to DEFAULT_MYSQL_ENGINES
-  - Improvement: sqldiff, use lowercase field names in MySQL
-  - Improvement: sqldiff, mysql code could duplicate AUTO_INCREMENT and UNSIGNED statements
 
+- Fix: autoslugfield, find unique method overrideable
+- Fix: notes, do not replace dot in template dirs
+- Fix: admin widgets, fix import of static template tag
+- Improvement: print_user_for_session, use session backend
+- Improvement: sqlcreate, postgis support
+- Improvement: graph_models, permit combination of includes and excludes
+- Improvement: Adds missing GIS engine to DEFAULT_MYSQL_ENGINES
+- Improvement: sqldiff, use lowercase field names in MySQL
+- Improvement: sqldiff, mysql code could duplicate AUTO_INCREMENT and UNSIGNED statements
 
 2.2.1
 -----
 
 Changes:
-  - Fix: tests, support for newer versions of pytest
-  - Fix: tests, disable test with drf dependency for older python versions
 
+- Fix: tests, support for newer versions of pytest
+- Fix: tests, disable test with drf dependency for older python versions
 
 2.2.0
 -----
 
 Changes:
-  - Fix: removing wrongly released text_tags template
-  - Fix: graph_models, support for Python <3.6
-  - Improvement: ForeignKeySearchInput, wrap media files in static()
-  - Improvement: UniqField, added tests
-  - Improvement: dumpscript, fix orm_item_locator to use dateutil
-  - Improvement: graph_models, added argument to change arrow_shape
 
+- Fix: removing wrongly released text_tags template
+- Fix: graph_models, support for Python <3.6
+- Improvement: ForeignKeySearchInput, wrap media files in static()
+- Improvement: UniqField, added tests
+- Improvement: dumpscript, fix orm_item_locator to use dateutil
+- Improvement: graph_models, added argument to change arrow_shape
 
 2.1.9
 -----
 
 Changes:
-  - Fix: show_urls, fix for traceback on multi language sites
-  - Improvement: reset_db, fix typo's in help test
 
+- Fix: show_urls, fix for traceback on multi language sites
+- Improvement: reset_db, fix typo's in help test
 
 2.1.8
 -----
 
 Changes:
-  - New: HexValidator, validate hex strings
-  - Improvement: reset_db, move settings to `django_settings.settings` which makes it easier to override.
-  - Improvement: AutoSlugField, extend support for custom slugify function
-  - Fix: runprofileserver, fix autoreloader for newer Django versions
 
+- New: HexValidator, validate hex strings
+- Improvement: reset_db, move settings to `django_settings.settings` which makes it easier to override.
+- Improvement: AutoSlugField, extend support for custom slugify function
+- Fix: runprofileserver, fix autoreloader for newer Django versions
 
 2.1.7
 -----
 
 Changes:
-  - New: test, many many more tests :-) thanks everybody
-  - New: docs, many documentation updates
-  - New: graph_model, add simple theming support and django2018 theme
-  - Improvement: ModificationDateTimeField, make modificationfield name modifiable
-  - Improvement: graph_model, option to not showrelations labels in the graph
-  - Improvement: reset_db, allow to override list of backends for database engines
-  - Improvement: reset_db, add psqlextra backend
-  - Improvement: runserver_plus, idle support
-  - Improvement: generate_secret_key, removed get_random_string in favour of get_random_secret_key
-  - Improvement: update_permissions, add create-only and update-only flags
-  - Improvement: update_permissions, update changed names of permission to match correct permission name
-  - Improvement: syncdata, add --database option
-  - Improvement: runscript, allow to override RUNSCRIPT_SCRIPT_DIR
-  - Fix: create_command, fix mknod error on macos
-  - Fix: runserver_plus, fix in resolving ssl certificate path
-  - Fix: sqldiff, fix hstorefield
-  - Deprecate: truncate_letters, use Django's truncatechars
-  - Deprecate: passwd, use Django's changepassword
-  - Deprecate: Keyczar encrypted fields, Keyczar is abandoned / deprecated
 
+- New: test, many many more tests :-) thanks everybody
+- New: docs, many documentation updates
+- New: graph_model, add simple theming support and django2018 theme
+- Improvement: ModificationDateTimeField, make modificationfield name modifiable
+- Improvement: graph_model, option to not showrelations labels in the graph
+- Improvement: reset_db, allow to override list of backends for database engines
+- Improvement: reset_db, add psqlextra backend
+- Improvement: runserver_plus, idle support
+- Improvement: generate_secret_key, removed get_random_string in favour of get_random_secret_key
+- Improvement: update_permissions, add create-only and update-only flags
+- Improvement: update_permissions, update changed names of permission to match correct permission name
+- Improvement: syncdata, add --database option
+- Improvement: runscript, allow to override RUNSCRIPT_SCRIPT_DIR
+- Fix: create_command, fix mknod error on macos
+- Fix: runserver_plus, fix in resolving ssl certificate path
+- Fix: sqldiff, fix hstorefield
+- Deprecate: truncate_letters, use Django's truncatechars
+- Deprecate: passwd, use Django's changepassword
+- Deprecate: Keyczar encrypted fields, Keyczar is abandoned / deprecated
 
 2.1.6
 -----
 
 Changes:
-  - Fix: runserver_plus, auto_reloader fix for compatibility with Django 2.2
-  - New: test, many many more tests :-) thanks @kuter
 
+- Fix: runserver_plus, auto_reloader fix for compatibility with Django 2.2
+- New: test, many many more tests :-) thanks @kuter
 
 2.1.5
 -----
 
 Changes:
-  - New: ipdb, pdb and wdb filters
-  - Fix: ForeignKeySearchInput, error with widget render(...) parameters on Django 2.1
-  - Fix: pipchecker, unsupported format string passed to NoneType.format error
-  - Tests: bunch of new test cases
 
+- New: ipdb, pdb and wdb filters
+- Fix: ForeignKeySearchInput, error with widget render(...) parameters on Django 2.1
+- Fix: pipchecker, unsupported format string passed to NoneType.format error
+- Tests: bunch of new test cases
 
 2.1.4
 -----
 
 Changes:
-  - Fix: null_technical_500_response, handle function-based middleware
-  - Fix: shell_plus, fix #1261 check for --notebook-dir=... argument style
-  - Fix: graph_models, Excluded models displayed as an underscore
-  - Fix: set_fake_password, requires_model_validation has been replaced with requires_system_checks since 1.9
-  - Docs: admin_generator, new documentation and examples
-  - Improvement: JSONField, use new from_db_value syntax on Django 2 and up
-  - Improvement: EncryptedTextField, use new from_db_value syntax on Django 2 and up
-  - Improvement: graph_models, add --dot option
-  - Improvement: graph_models, allow to redirect (text) output to file
-  - Improvement: sqldiff, better support for indexes, index_together and unique_together
 
+- Fix: null_technical_500_response, handle function-based middleware
+- Fix: shell_plus, fix #1261 check for --notebook-dir=... argument style
+- Fix: graph_models, Excluded models displayed as an underscore
+- Fix: set_fake_password, requires_model_validation has been replaced with requires_system_checks since 1.9
+- Docs: admin_generator, new documentation and examples
+- Improvement: JSONField, use new from_db_value syntax on Django 2 and up
+- Improvement: EncryptedTextField, use new from_db_value syntax on Django 2 and up
+- Improvement: graph_models, add --dot option
+- Improvement: graph_models, allow to redirect (text) output to file
+- Improvement: sqldiff, better support for indexes, index_together and unique_together
 
 2.1.3
 -----
 
 Changes:
-  - Fix: Readme, add direct link to screencast video
-  - Fix: graph_models, regression under Python 2
-  - Fix: ForeignKeyAutocompleteAdmin, 2.0.8 breaks ForeignKeyAutocompleteAdmin
-  - Fix: AutoSlugField, fix regression when copying an autoslug model require the explicit clearing of the slug if it needs to be recalculated
-  - Fix: technical_response, check for AttributeError
-  - Improvement: graph_models, Add feature disable_abstract_fields
-  - Improvement: AutoSlugField, Add overwrite_on_add
-  - Improvement: runscript, Improve module existence test in runscript
 
+- Fix: Readme, add direct link to screencast video
+- Fix: graph_models, regression under Python 2
+- Fix: ForeignKeyAutocompleteAdmin, 2.0.8 breaks ForeignKeyAutocompleteAdmin
+- Fix: AutoSlugField, fix regression when copying an autoslug model require the explicit clearing of the slug if it needs to be recalculated
+- Fix: technical_response, check for AttributeError
+- Improvement: graph_models, Add feature disable_abstract_fields
+- Improvement: AutoSlugField, Add overwrite_on_add
+- Improvement: runscript, Improve module existence test in runscript
 
 2.1.2
 -----
 
 Changes:
-  - Fix: AutoSlugField, fix check on list or tuple type
 
+- Fix: AutoSlugField, fix check on list or tuple type
 
 2.1.1
 -----
@@ -370,29 +375,29 @@ Changes:
 Removed support for Django versions before 1.11
 
 Changes:
-  - Fix: foreignkey_searchinput, remove unnecessary img tag
-  - Fix: sqldiff, fix deprecated get_indexes call
-  - Fix: AutoSlugField, check that any non-callable value passed to populate_from is a string type
-  - Fix: tests, fix ChangingDirectoryTests: cd back in tearDown
-  - Fix: show_template_tags, should handle AppConfig class in INSTALLED applications
-  - Improvement: runserver_plus, reduce reraise pollution in traceback page
-  - Improvement: dumpscript, prevent many2many field with custom intermediate models to be added directly on the parent model
-  - Docs: fix typos
 
+- Fix: foreignkey_searchinput, remove unnecessary img tag
+- Fix: sqldiff, fix deprecated get_indexes call
+- Fix: AutoSlugField, check that any non-callable value passed to populate_from is a string type
+- Fix: tests, fix ChangingDirectoryTests: cd back in tearDown
+- Fix: show_template_tags, should handle AppConfig class in INSTALLED applications
+- Improvement: runserver_plus, reduce reraise pollution in traceback page
+- Improvement: dumpscript, prevent many2many field with custom intermediate models to be added directly on the parent model
+- Docs: fix typos
 
 2.1.0
 -----
 
 Changes:
-  - Fix: travis
 
+- Fix: travis
 
 2.0.9
 -----
 
 Changes:
-  - Improvement: use README as project description on PyPI
 
+- Improvement: use README as project description on PyPI
 
 2.0.8
 -----
@@ -400,118 +405,118 @@ Changes:
 Please stop using ForeignKeyAutocompleteAdmin edition :-)
 
 Changes:
-  - Fix: special markers in runserver_plus.rst
-  - Fix: shell_plus, refactor reading pythonrc file outside of exec(compile(...))
-  - Fix: reset_db, fix default utf8 support
-  - Fix: autoslugfield, Fix autoslug generation when existing model is copied
-  - Improvement: Cleanup management commands options after argparse migration #916
-  - Improvement: sqldiff, add more tests
-  - Improvement: sqldiff, add DurationField and SearchVectorField
-  - Improvement: shell_plus, add more tests
-  - Improvement: shell_plus, backport macos fix for tab completion
-  - Improvement: clear_cache, add --all option
-  - Improvement: pipchecker, treat dev versions as unstable
-  - Deprecation: ForeignKeyAutocompleteAdmin, Django 2.0 has similar capabilities, which are much better supported.
 
+- Fix: special markers in runserver_plus.rst
+- Fix: shell_plus, refactor reading pythonrc file outside of exec(compile(...))
+- Fix: reset_db, fix default utf8 support
+- Fix: autoslugfield, Fix autoslug generation when existing model is copied
+- Improvement: Cleanup management commands options after argparse migration #916
+- Improvement: sqldiff, add more tests
+- Improvement: sqldiff, add DurationField and SearchVectorField
+- Improvement: shell_plus, add more tests
+- Improvement: shell_plus, backport macos fix for tab completion
+- Improvement: clear_cache, add --all option
+- Improvement: pipchecker, treat dev versions as unstable
+- Deprecation: ForeignKeyAutocompleteAdmin, Django 2.0 has similar capabilities, which are much better supported.
 
 2.0.7
 -----
 
 Changes:
-  - Fix: pipchecker, pip 10.0.0 compatibility
-  - Fix: sqldiff, improve support of GIS fields by using Django introspection
-  - Fix: shell_plus, fix bug in windows when PYTHONPATH is defined
-  - Fix: shell_plus, Call execute on CursorWrapper instead of directly on cursor to ensure wrappers are run
-  - Fix: runserver_plus, Call execute on CursorWrapper instead of directly on cursor to ensure wrappers are run
-  - Improvement: sqldiff, drop old compatibility code
-  - Improvement: ForeignKeyAutocompleteAdminMixin, improvements for Django >1.9
 
+- Fix: pipchecker, pip 10.0.0 compatibility
+- Fix: sqldiff, improve support of GIS fields by using Django introspection
+- Fix: shell_plus, fix bug in windows when PYTHONPATH is defined
+- Fix: shell_plus, Call execute on CursorWrapper instead of directly on cursor to ensure wrappers are run
+- Fix: runserver_plus, Call execute on CursorWrapper instead of directly on cursor to ensure wrappers are run
+- Improvement: sqldiff, drop old compatibility code
+- Improvement: ForeignKeyAutocompleteAdminMixin, improvements for Django >1.9
 
 2.0.6
 -----
 
 Changes:
-  - Fix: shell_plus, Fix of deprecation warning in collision resolvers
 
+- Fix: shell_plus, Fix of deprecation warning in collision resolvers
 
 2.0.5
 -----
 
 Changes:
-  - Improvement: setup.py, Use PEP 508 when setuptools is version 36 or higher should fix issues with pipenv
-  - Fix: docs, Docs should show that django 2.0 is supported
 
+- Improvement: setup.py, Use PEP 508 when setuptools is version 36 or higher should fix issues with pipenv
+- Fix: docs, Docs should show that django 2.0 is supported
 
 2.0.4
 -----
 
 Changes:
-  - Fix: setup.py, fix installation of typing in python < 3.5
 
+- Fix: setup.py, fix installation of typing in python < 3.5
 
 2.0.3
 -----
 
 Changes:
-  - Fix: shell_plus, python 2.7 support broken due to use of Python3 super()
 
+- Fix: shell_plus, python 2.7 support broken due to use of Python3 super()
 
 2.0.2
 -----
 
 Changes:
-  - Improvement: sqldiff, add --include-defaults to include default value in missing field for sqldiff #1064
 
+- Improvement: sqldiff, add --include-defaults to include default value in missing field for sqldiff #1064
 
 2.0.1
 -----
 
 Changes:
-  - Fix: setup.py, do not include `typing` requirement in recent versions of Python
-  - Improvement: shell_plus, add support for using -- to pass cli argument directly to underlying python shell implementation
-  - New: generate_password, Generates a new password based on `BaseUserManager.make_random_password`
 
+- Fix: setup.py, do not include `typing` requirement in recent versions of Python
+- Improvement: shell_plus, add support for using -- to pass cli argument directly to underlying python shell implementation
+- New: generate_password, Generates a new password based on `BaseUserManager.make_random_password`
 
 2.0.0
 -----
 
 Changes:
-  - Fix: runserver_plus, for 1.11 still using MIDDLEWARE_CLASSES
-  - Fix: show_urls, Fix display in Django 2.0
-  - Fix: validate_templates, remove realpath in validate_templates
-  - Fix: sqldiff, bug with including proxy models in sqldiff output
-  - Improvement: shell_plus, allow configurating of sqlparse formatting and pygments formatting
-  - Improvement: shell_plus, add collision resolvers based on app label
-  - Improvement: shell_plus, automatic importing of subclasses defined in SHELL_PLUS_SUBCLASSES_IMPORT
-  - New: reset_schema, simple command to recreate public schema in PostgreSQL
-  - Docs: fix links to Werkzeug documentation
 
+- Fix: runserver_plus, for 1.11 still using MIDDLEWARE_CLASSES
+- Fix: show_urls, Fix display in Django 2.0
+- Fix: validate_templates, remove realpath in validate_templates
+- Fix: sqldiff, bug with including proxy models in sqldiff output
+- Improvement: shell_plus, allow configurating of sqlparse formatting and pygments formatting
+- Improvement: shell_plus, add collision resolvers based on app label
+- Improvement: shell_plus, automatic importing of subclasses defined in SHELL_PLUS_SUBCLASSES_IMPORT
+- New: reset_schema, simple command to recreate public schema in PostgreSQL
+- Docs: fix links to Werkzeug documentation
 
 1.9.9
 -----
 
 Changes:
-  - Fix: runserver_plus, fix for Django 2.0 middleware handling
-  - Fix: shell_plus, fixed app_name resolving
-  - Fix: AutoSlugField, deconstruct did not match construction values
-  - Fix: runjob, not compatible with apps that use AppConfig in INSTALLED_APPS
-  - Improvement: runserver_plus, added configuring paths to certificates
-  - Improvement: sample.py template, add newline to avoid linter warnings
-  - Improvement: jobs, add integration tests for runjob and runjobs management commands
-  - New: merge_model_instances, new management command for de-duplicating model instances
 
+- Fix: runserver_plus, fix for Django 2.0 middleware handling
+- Fix: shell_plus, fixed app_name resolving
+- Fix: AutoSlugField, deconstruct did not match construction values
+- Fix: runjob, not compatible with apps that use AppConfig in INSTALLED_APPS
+- Improvement: runserver_plus, added configuring paths to certificates
+- Improvement: sample.py template, add newline to avoid linter warnings
+- Improvement: jobs, add integration tests for runjob and runjobs management commands
+- New: merge_model_instances, new management command for de-duplicating model instances
 
 1.9.8
 -----
 
 Changes:
-  - Fix: show_urls, fix for Django 2.0 (Locale URL Resolvers are still broken)
-  - Fix: runserver_plus, fix rendering of ipv6 link
-  - Improvement: validate_templates, allow relative paths
-  - Improvement: validate_templates, automatically include app templates
-  - Improvement: pip_checker, could not find some packages
-  - Docs: shell_plus, `--print-sql` usage clearification
 
+- Fix: show_urls, fix for Django 2.0 (Locale URL Resolvers are still broken)
+- Fix: runserver_plus, fix rendering of ipv6 link
+- Improvement: validate_templates, allow relative paths
+- Improvement: validate_templates, automatically include app templates
+- Improvement: pip_checker, could not find some packages
+- Docs: shell_plus, `--print-sql` usage clearification
 
 1.9.7
 -----
@@ -520,52 +525,51 @@ This release add checking types with MyPy to the test suite. At this point
 only a few lines of code are explicitly typed.
 
 Changes:
-  - Improvement: shell_plus, Collision resolver implemented.
-  - Improvement: shell_plus, Skipping all models importing feature added.
-  - Improvement: runscript, Script execution directory policy feature added.
-  - django-extensions now requires the [typing](https://pypi.python.org/pypi/typing) package.
 
+- Improvement: shell_plus, Collision resolver implemented.
+- Improvement: shell_plus, Skipping all models importing feature added.
+- Improvement: runscript, Script execution directory policy feature added.
+- django-extensions now requires the [typing](https://pypi.python.org/pypi/typing) package.
 
 1.9.6
 -----
 
 Fix boo-boo with release version in django_extensions/__init__.py
 
-
 1.9.4
 -----
 
 Changes:
- - Fix missing test case
 
+- Fix missing test case
 
 1.9.3
 -----
 
 Changes:
- - Tests: shell_plus, simple test for get_imported_objects
 
+- Tests: shell_plus, simple test for get_imported_objects
 
 1.9.2
 -----
 
 Changes:
- - Fix: mail_debug, regression in mail_debug for older Pythons
- - Fix: shell_plus, SyntaxError on exec(), python compatibility
- - Fix: ForeignKeyAutocompleteAdminMixin, use text/plain
 
+- Fix: mail_debug, regression in mail_debug for older Pythons
+- Fix: shell_plus, SyntaxError on exec(), python compatibility
+- Fix: ForeignKeyAutocompleteAdminMixin, use text/plain
 
 1.9.1
 -----
 
 Changes:
- - Fix: graph_models, fix json option
- - Fix: runserver_plus, avoid duplicate messages logged to console
- - Fix: mail_debug, python3 fix
- - Improvement: sqldiff, basic support for array types in postgresql
- - Improvement: runscript, handle import errors better
- - Docs: updated documentation for model extensions
 
+- Fix: graph_models, fix json option
+- Fix: runserver_plus, avoid duplicate messages logged to console
+- Fix: mail_debug, python3 fix
+- Improvement: sqldiff, basic support for array types in postgresql
+- Improvement: runscript, handle import errors better
+- Docs: updated documentation for model extensions
 
 1.9.0
 -----
@@ -575,29 +579,28 @@ default behaviour to automatically load PYTHONSTARTUP and ~/.pythonrc.py
 unless --no-startup is set.
 
 Changes:
- - Fix: pipchecker, fix up-to-date check for Github sha commits
- - Fix: JSONField, fix handling to_python() for strings with tests
- - Fix: print_settings, fix print_settings to receive positional args
- - Improvement: shell_plus, update PYTHONSTARTUP / pythonrc handling to match Django
- - Improvement: shell_plus, added new 1.11 features from django.db.models to shell_plus import list
- - Improvement: runserver_plus, startup message now accounts for https
- - Docs: jobs, improve documentation about jobs scheduling
- - Docs: admin, add documentation for ForeignKeyAutocompleteStackedInline and ForeignKeyAutocompleteTabularInline
- - Docs: fix typos
 
+- Fix: pipchecker, fix up-to-date check for Github sha commits
+- Fix: JSONField, fix handling to_python() for strings with tests
+- Fix: print_settings, fix print_settings to receive positional args
+- Improvement: shell_plus, update PYTHONSTARTUP / pythonrc handling to match Django
+- Improvement: shell_plus, added new 1.11 features from django.db.models to shell_plus import list
+- Improvement: runserver_plus, startup message now accounts for https
+- Docs: jobs, improve documentation about jobs scheduling
+- Docs: admin, add documentation for ForeignKeyAutocompleteStackedInline and ForeignKeyAutocompleteTabularInline
+- Docs: fix typos
 
 1.8.1
 -----
 
 Changes:
- - Build: use tox's 'TOXENV' environment variable
- - Fix: resetdb, fix problem that 'utf8_support' option is ignored
- - Improvement: export_emails, moved custom csv UnicodeWriter (for py2) into compat.py
- - Translations: pt, removed since it was causing issues with the builds
-                     if anybody wants to update and fix it that would be
-                     much appreciated !
-
 
+- Build: use tox's 'TOXENV' environment variable
+- Fix: resetdb, fix problem that 'utf8_support' option is ignored
+- Improvement: export_emails, moved custom csv UnicodeWriter (for py2) into compat.py
+- Translations: pt, removed since it was causing issues with the builds
+                    if anybody wants to update and fix it that would be
+                    much appreciated !
 
 1.8.0
 -----
@@ -608,120 +611,119 @@ Deprecation schedule for JSONField has been removed after requests from the
 community.
 
 Changes:
- - Fix: runserver_plus, fixed Python 3 print syntax
- - Fix: sqldiff, Use 'display_size', not 'precision' to identify MySQL bool field
- - Fix: export_emails, fix and refactor the command and all its output options
- - Improvement: tests, added Python 3.6 and PyPy3.5-5.8.0
- - Improvement: clear_cache, add --cache option to support multiple caches
- - Improvement: runserver_plus, limit printing SQL queries to avoid flooding the terminal
- - Improvement: shell_plus, limit printing SQL queries to avoid flooding the terminal
- - Docs: graph_models, document including/excluding specific models
- - Docs: shell_plus, added PTPython
 
+- Fix: runserver_plus, fixed Python 3 print syntax
+- Fix: sqldiff, Use 'display_size', not 'precision' to identify MySQL bool field
+- Fix: export_emails, fix and refactor the command and all its output options
+- Improvement: tests, added Python 3.6 and PyPy3.5-5.8.0
+- Improvement: clear_cache, add --cache option to support multiple caches
+- Improvement: runserver_plus, limit printing SQL queries to avoid flooding the terminal
+- Improvement: shell_plus, limit printing SQL queries to avoid flooding the terminal
+- Docs: graph_models, document including/excluding specific models
+- Docs: shell_plus, added PTPython
 
 1.7.9
 -----
 
 Changes:
- - Fix: AutoSlugField, foreignkey relationships
- - Fix: shell_plus, supported backends 'postgresql' for set_application_name
- - Improvement: various commands, Add syntax highlighting when printing SQL
- - Improvement: pipchecker, treat rc versions as unstable
- - Improvement: shell_plus, allow to subclass and overwrite import_objects
- - Improvement: shell_plus, fix SHELL_PLUS_PRE_IMPORTS example
- - Improvement: setup.py, fix and unify running tests
- - Improvement: runserver_plus, add RUNSERVERPLUS_POLLER_RELOADER_TYPE setting
- - Improvement: generate_secret_key, use algorithm from django
- - Docs: fix grammar and spelling mistakes
-
 
+- Fix: AutoSlugField, foreignkey relationships
+- Fix: shell_plus, supported backends 'postgresql' for set_application_name
+- Improvement: various commands, Add syntax highlighting when printing SQL
+- Improvement: pipchecker, treat rc versions as unstable
+- Improvement: shell_plus, allow to subclass and overwrite import_objects
+- Improvement: shell_plus, fix SHELL_PLUS_PRE_IMPORTS example
+- Improvement: setup.py, fix and unify running tests
+- Improvement: runserver_plus, add RUNSERVERPLUS_POLLER_RELOADER_TYPE setting
+- Improvement: generate_secret_key, use algorithm from django
+- Docs: fix grammar and spelling mistakes
 
 1.7.8
 -----
 
 Changes:
- - Improvement: django 1.11, add testing for Django 1.11
- - Improvement: pipchecker, make it possible to parse https github urls
- - Improvement: unreferenced_files, make command much faster by using set()
- - Docs: add undocumented commands
- - Docs: shell_plus, additional documentation for referencing nested modules
- - Fix: sync_s3, fix exclusion of directories
- - Fix: runprofileserver, fix ip:port specification
- - Fix: runprofileserver, support --nothreading
 
+- Improvement: django 1.11, add testing for Django 1.11
+- Improvement: pipchecker, make it possible to parse https github urls
+- Improvement: unreferenced_files, make command much faster by using set()
+- Docs: add undocumented commands
+- Docs: shell_plus, additional documentation for referencing nested modules
+- Fix: sync_s3, fix exclusion of directories
+- Fix: runprofileserver, fix ip:port specification
+- Fix: runprofileserver, support --nothreading
 
 1.7.7
 -----
 
 Changes:
- - Improvement: admin_generator, use decorator style for registering ModelAdmins.
- - Improvement: sqldiff, quote tablename for PRAGMA in sqlite
- - Fix: graph_models, Fix `attributes` referenced before assignment
- - Fix: pipchecker, Fix AttributeError caused by missing method
 
+- Improvement: admin_generator, use decorator style for registering ModelAdmins.
+- Improvement: sqldiff, quote tablename for PRAGMA in sqlite
+- Fix: graph_models, Fix `attributes` referenced before assignment
+- Fix: pipchecker, Fix AttributeError caused by missing method
 
 1.7.6
 -----
 
 Changes:
- - Improvement: sqldiff, ignore proxy models in diff (with cli option to include them if wanted)
 
+- Improvement: sqldiff, ignore proxy models in diff (with cli option to include them if wanted)
 
 1.7.5
 -----
 
 Changes:
- - New: ForeignKeyAutocompleteAdmin, Add autocomplete for inline model admins
- - Improvement: graph_models, Rewrite modelviz module from method to class based processor
- - Improvement: db fields, make MAX_UNIQUE_QUERY_ATTEMPTS configurable per field and via settings
- - Improvement: runserver_plus, Added nopin option to disable pin
- - Fix: graph_models, Support PyDot 1.2.0 and higher
- - Fix: shell_plus, Fix that aliases from SHELL_PLUS_MODEL_ALIASES were not applied
- - Removed: validate_templatetags, remove support for pre django-1.5 style {% url %} tags
- - Cleanup: removing support for end-of-life Python 3.2
- - Docs: simplify installation instructions
- - Docs: fix example for NOTEBOOK_ARGUMENTS
- - Docs: Remove extraneous '}' characters from shell_plus docs
 
+- New: ForeignKeyAutocompleteAdmin, Add autocomplete for inline model admins
+- Improvement: graph_models, Rewrite modelviz module from method to class based processor
+- Improvement: db fields, make MAX_UNIQUE_QUERY_ATTEMPTS configurable per field and via settings
+- Improvement: runserver_plus, Added nopin option to disable pin
+- Fix: graph_models, Support PyDot 1.2.0 and higher
+- Fix: shell_plus, Fix that aliases from SHELL_PLUS_MODEL_ALIASES were not applied
+- Removed: validate_templatetags, remove support for pre django-1.5 style {% url %} tags
+- Cleanup: removing support for end-of-life Python 3.2
+- Docs: simplify installation instructions
+- Docs: fix example for NOTEBOOK_ARGUMENTS
+- Docs: Remove extraneous '}' characters from shell_plus docs
 
 1.7.4
 -----
 
 Changes:
- - Improvement: show_urls, support --no-color option
- - Fix: notes, Fix reading templates setting after django 1.8
- - Fix: create_app, Fixed typo in deprecation warning
- - Fix: shell_plus, Use new location for url reverse import
- - Docs: some commands where missing from the docs
- - Docs: runscript, added documentation for --traceback
 
+- Improvement: show_urls, support --no-color option
+- Fix: notes, Fix reading templates setting after django 1.8
+- Fix: create_app, Fixed typo in deprecation warning
+- Fix: shell_plus, Use new location for url reverse import
+- Docs: some commands where missing from the docs
+- Docs: runscript, added documentation for --traceback
 
 1.7.3
 -----
 
 Changes:
- - Fix: ForeignKeySearchInput, fix bug with constructing search_path urls
- - Docs: runscript, fix runscript example
- - Deprecation: JSONField, Django now includes JSONField our field is now deprecated
 
+- Fix: ForeignKeySearchInput, fix bug with constructing search_path urls
+- Docs: runscript, fix runscript example
+- Deprecation: JSONField, Django now includes JSONField our field is now deprecated
 
 1.7.2
 -----
 
 Changes:
- - Fix: passwd, Update passwd command up to Django>=1.8
- - Improvement: shell_plus, add settings.SHELL_PLUS_PRINT_SQL config option
- - Improvement: shell_plus, allow to specifies the connection file to use if using the --kernel option
 
+- Fix: passwd, Update passwd command up to Django>=1.8
+- Improvement: shell_plus, add settings.SHELL_PLUS_PRINT_SQL config option
+- Improvement: shell_plus, allow to specifies the connection file to use if using the --kernel option
 
 1.7.1
 -----
 
 Changes:
- - Fix: sqldiff, fix optional app_label arguments
- - Fix: runscript, remove args from command class
- - Doc: runscript, remove = from --script-args example
 
+- Fix: sqldiff, fix optional app_label arguments
+- Fix: runscript, remove args from command class
+- Doc: runscript, remove = from --script-args example
 
 1.7.0
 -----
@@ -734,70 +736,70 @@ make sure that Django Extensions uses the current Django API's. This should
 result in better and easier to maintain code (and hopefully less bugs :).
 
 This release touches a lot of code if you have any issues please report them
-at https://github.com/django-extensions/django-extensions/issues
+at [https://github.com/django-extensions/django-extensions/issues]
 
 We still need more tests to make sure we don't break people's projects when
 refactoring. If you have some spare time please contribute tests !
 
 Changes:
- - Cleanup: removing backwards compatibility hacks for (now) unsupported versions of Django
- - Cleanup: use six instead of home grown functions
- - Fix: AutoSlugField, allow_duplicates didn't set slug value to model instance
- - Fix: MongoDB fields, verbose_name on mongoengine fields does not seem to be supported
- - Fix: MongoDB fields, fix relative import problem with json.py
- - Improvement: Start using pre-commit
- - Improvement: syncdata, Replace custom transaction logic with transaction.atomic
- - Improvement: Django 1.10, use from_db_value instead of models.SubfieldBase
- - Improvement: print_user_session, support for non standard user model
- - Improvement: widont, tests to work with py2 and py3
- - Improvement: runserver_plus, prevent 2nd reload of debugger on runserver_plus
- - Improvement: runserver_plus, prevent killing the server when request.META values are evaluated
- - Improvement: reset_db, add argument to make closing sessions optional
- - Improvement: print_settings, Fix positional arguments
- - Improvement: runscript, migrate to argparse and add_arguments
- - Improvement: graph_models, do not rely on .models_module for inclusion in output
- - Improvement: jsonfield, fix issues with mutable default
- - Docs: Convert readthedocs links for their .org -> .io migration
 
+- Cleanup: removing backwards compatibility hacks for (now) unsupported versions of Django
+- Cleanup: use six instead of home grown functions
+- Fix: AutoSlugField, allow_duplicates didn't set slug value to model instance
+- Fix: MongoDB fields, verbose_name on mongoengine fields does not seem to be supported
+- Fix: MongoDB fields, fix relative import problem with json.py
+- Improvement: Start using pre-commit
+- Improvement: syncdata, Replace custom transaction logic with transaction.atomic
+- Improvement: Django 1.10, use from_db_value instead of models.SubfieldBase
+- Improvement: print_user_session, support for non standard user model
+- Improvement: widont, tests to work with py2 and py3
+- Improvement: runserver_plus, prevent 2nd reload of debugger on runserver_plus
+- Improvement: runserver_plus, prevent killing the server when request.META values are evaluated
+- Improvement: reset_db, add argument to make closing sessions optional
+- Improvement: print_settings, Fix positional arguments
+- Improvement: runscript, migrate to argparse and add_arguments
+- Improvement: graph_models, do not rely on .models_module for inclusion in output
+- Improvement: jsonfield, fix issues with mutable default
+- Docs: Convert readthedocs links for their .org -> .io migration
 
 1.6.7
 -----
 
 Changes:
- - Fix: describe_form, fix No module named 'django.db.models.loading' error
- - Improvement: shell_plus,  Add a setting to prefix all models in an application #887
- - Improvement: pipchecker, check for requirements-{dev,prod}.txt as well
- - Docs: pipchecker, update documentation
+
+- Fix: describe_form, fix No module named 'django.db.models.loading' error
+- Improvement: shell_plus,  Add a setting to prefix all models in an application #887
+- Improvement: pipchecker, check for requirements-{dev,prod}.txt as well
+- Docs: pipchecker, update documentation
 
 1.6.6
 -----
 
 Changes:
- - Fix: admin_generator, fix for using all apps in Django <1.7
- - Fix: dump_script, fix for using all apps in Django <1.7
- - Fix: UniqueFieldMixin, resolve get_fields_with_model deprecation Django 1.10
- - Fix: runprofileserver, Fix call grind format to enable source code navigation in qcachegrind.
- - Docs: runserver_plus, add a little note about the debugger PIN.
 
+- Fix: admin_generator, fix for using all apps in Django <1.7
+- Fix: dump_script, fix for using all apps in Django <1.7
+- Fix: UniqueFieldMixin, resolve get_fields_with_model deprecation Django 1.10
+- Fix: runprofileserver, Fix call grind format to enable source code navigation in qcachegrind.
+- Docs: runserver_plus, add a little note about the debugger PIN.
 
 1.6.5
 -----
 
 Bumped version number since PyPi returns 500 errors while uploading packages :(
 
-
 1.6.4
 -----
 
 Changes:
- - Fix: jobs cache_cleanup, use `caches` instead of deprecated `get_cache`
- - Fix: ModificationDateTimeField, missing default value for `update_modified`
- - Fix: modelviz, use get_model_compat and look up missing app_label
- - Fix: modelviz, use get_models_for_app instead of get_models_compat
- - Fix: dumpscript, use `list_app_labels` instead of `get_apps` when no app_labels are given
- - Improvement: compat.py, move code from try to else block for Django 1.7+
- - Docstring: get_models_for_app, clearify argument
 
+- Fix: jobs cache_cleanup, use `caches` instead of deprecated `get_cache`
+- Fix: ModificationDateTimeField, missing default value for `update_modified`
+- Fix: modelviz, use get_model_compat and look up missing app_label
+- Fix: modelviz, use get_models_for_app instead of get_models_compat
+- Fix: dumpscript, use `list_app_labels` instead of `get_apps` when no app_labels are given
+- Improvement: compat.py, move code from try to else block for Django 1.7+
+- Docstring: get_models_for_app, clearify argument
 
 1.6.3
 -----
@@ -810,439 +812,441 @@ Bumped version number for incomplete PyPi upload
 The long over due release :-)
 
 Changes:
- - Fix: JsonFields, do not parse floats as decimals. This fixes bugs that causes
-        them to be returned as strings after multiple saves. Note that this can
-        be backwards incompatible !
- - Fix: use add_arguments() instead of option_list (Django 1.10)
- - Fix: create_command, Django 1.9 fixes
- - Fix: create_jobs, Django 1.9 fixes
- - Fix: RandomCharField, when not unique get the first value from the generator
- - Fix: graph_models, render() must be called with a dict
- - Fix: graph_models, use force_bytes fixes command for Python 3
- - Fix: graph_models, fix django 1.6 compatibility for strings defined relation
- - Fix: graph_models, fix settings.GRAPH_MODELS breaking the command
- - Fix: graph_models, add support for lazy relationships
- - Fix: ForeignKeyAutocompleteAdmin, url_patterns is just a list (Django 1.9+)
- - Fix: ForeignKeySearchInput, use url reversing instead of hardcoded paths
- - Fix: find_template, Fix for Django 1.8+
- - Fix: admin_generator, incompatible "default" identifier raising TypeError
- - Improvement: show_urls, add json and pretty-json formatting
- - Improvement: runserver_plus, add support for whitenoise
- - Improvement: ModificationDateTimeField, add parameter to preserve timestamps on save
- - Improvement: runprofileserver, raise command error when hotspot is not available
- - Improvement: reset_db, better parsing of mysql cnf file
- - Improvement: restored coverage for Python 3.2
- - Improvement: pep8 fixes, remove unused shims & imports & commented code
- - Improvement: graph_models, JSON output
- - Improvement: graph_models, add wildcard filters
- - Docs: removed text on donations, the hope was that we could generate some
-         funds to have more consistent development and outreach.
- - Docs: runserver_plus, added some documentation about LOGGING
- - Docs: runscript, update documentation to match Django tutorial for Django 1.8+
- - Docs: runprofileserver, add documentation on profiler choices
- - Docs: update_permissions, add basic documentation for command
 
+- Fix: JsonFields, do not parse floats as decimals. This fixes bugs that causes
+      them to be returned as strings after multiple saves. Note that this can
+      be backwards incompatible !
+- Fix: use add_arguments() instead of option_list (Django 1.10)
+- Fix: create_command, Django 1.9 fixes
+- Fix: create_jobs, Django 1.9 fixes
+- Fix: RandomCharField, when not unique get the first value from the generator
+- Fix: graph_models, render() must be called with a dict
+- Fix: graph_models, use force_bytes fixes command for Python 3
+- Fix: graph_models, fix django 1.6 compatibility for strings defined relation
+- Fix: graph_models, fix settings.GRAPH_MODELS breaking the command
+- Fix: graph_models, add support for lazy relationships
+- Fix: ForeignKeyAutocompleteAdmin, url_patterns is just a list (Django 1.9+)
+- Fix: ForeignKeySearchInput, use url reversing instead of hardcoded paths
+- Fix: find_template, Fix for Django 1.8+
+- Fix: admin_generator, incompatible "default" identifier raising TypeError
+- Improvement: show_urls, add json and pretty-json formatting
+- Improvement: runserver_plus, add support for whitenoise
+- Improvement: ModificationDateTimeField, add parameter to preserve timestamps on save
+- Improvement: runprofileserver, raise command error when hotspot is not available
+- Improvement: reset_db, better parsing of mysql cnf file
+- Improvement: restored coverage for Python 3.2
+- Improvement: pep8 fixes, remove unused shims & imports & commented code
+- Improvement: graph_models, JSON output
+- Improvement: graph_models, add wildcard filters
+- Docs: removed text on donations, the hope was that we could generate some
+        funds to have more consistent development and outreach.
+- Docs: runserver_plus, added some documentation about LOGGING
+- Docs: runscript, update documentation to match Django tutorial for Django 1.8+
+- Docs: runprofileserver, add documentation on profiler choices
+- Docs: update_permissions, add basic documentation for command
 
 1.6.1
 -----
 
 Changes:
- - Revert: JSONField, revert Django 1.9 fix as it breaks the field (ticket #781)
 
+- Revert: JSONField, revert Django 1.9 fix as it breaks the field (ticket #781)
 
 1.6.0
 -----
 
 Changes:
- - Fix: Django 1.9 compatibility
- - New: runserver_plus, add --startup-messages to control when to show them
- - New: added support for Python 3.5
- - Improvement: show_template_tags, renamed from show_templatetags for consistency
- - Removed: jquery library (after dropping support for Django 1.5)
 
+- Fix: Django 1.9 compatibility
+- New: runserver_plus, add --startup-messages to control when to show them
+- New: added support for Python 3.5
+- Improvement: show_template_tags, renamed from show_templatetags for consistency
+- Removed: jquery library (after dropping support for Django 1.5)
 
 1.5.9
 -----
 
 Changes:
- - Fix: wheel still had the old migrations directory in the package
 
+- Fix: wheel still had the old migrations directory in the package
 
 1.5.8
 -----
 
 Changes:
- - Fix: migrations, fix BadMigrationError with Django 1.8+
- - Fix: reset_db, Django 1.8+ compatibility fix
- - Fix: runserver_plus, fix signature of null_technical_500_response for Django 1.8+
- - Fix: graph_models, use force_bytes instead of .decode('utf8')
- - Improvement: print_settings, add format option to only print values
- - Improvement: print_settings, add format option for simple key = value text output
- - Improvement: email_export, documentation updates
- - Improvement: shell_plus, auto load conditional db expressions Case and When
 
+- Fix: migrations, fix BadMigrationError with Django 1.8+
+- Fix: reset_db, Django 1.8+ compatibility fix
+- Fix: runserver_plus, fix signature of null_technical_500_response for Django 1.8+
+- Fix: graph_models, use force_bytes instead of .decode('utf8')
+- Improvement: print_settings, add format option to only print values
+- Improvement: print_settings, add format option for simple key = value text output
+- Improvement: email_export, documentation updates
+- Improvement: shell_plus, auto load conditional db expressions Case and When
 
 1.5.7
 -----
 
 Changes:
- - Fix: CreationDateTimeField, migration error
- - Fix: ModificationDateTimeField, migration error
- - Fix: shell_plus, options is not always in db config dictionary
- - Fix: admin filters, contrib.admin.util fallback code
- - Fix: graph_models, correctly support parsing lists for cli options
- - Improvement: sqldsn, support postfix
- - Improvement: utils, remove get_project_root function
 
+- Fix: CreationDateTimeField, migration error
+- Fix: ModificationDateTimeField, migration error
+- Fix: shell_plus, options is not always in db config dictionary
+- Fix: admin filters, contrib.admin.util fallback code
+- Fix: graph_models, correctly support parsing lists for cli options
+- Improvement: sqldsn, support postfix
+- Improvement: utils, remove get_project_root function
 
 1.5.6
 -----
 
 Changes:
- - New: RandomCharField, prepopulates random character string
- - New: (Not)NullFieldListFilter, filters for admin
- - New: runserver_plus, integrate with django-pdb
- - New: runserver_plus, add check_migrations from Django
- - Improvement: show_urls, nested namespace support
- - Improvement: show_urls, allow to specify alternative urlconf
- - Improvement: show_urls, support i18n_patterns
- - Improvement: show_urls, use --language to filter on a particular language
- - Improvement: admin_generator, added docstrings to module
- - Improvement: shell_plus, allow cli arguments to be passed to ipython
- - Improvement: shell_plus, fixed PYTHONPATH bug when using django-admin shell_plus --notebook
- - Improvement: shell_plus, set application_name on PostgreSQL databases
- - Improvement: shell_plus, load user pypython config file
- - Improvement: CreationDateTimeField, use auto_now_add instead of default ModificationDateTimeField
- - Improvement: ModificationDateTimeField, use auto_now instead of pre_save method
- - Improvement: ForeignKeyAutocompleteAdmin, added ability to filter autocomplete query
- - Fix: shell_plus, support for pypython>=0.27
- - Fix: shell_plus, load apps and models directly through the apps interface when available
- - Fix: shell_plus, use ipython start_ipython instead of embed
- - Fix: shell_plus, fix swalling ImportErrors with IPython 3 and higher
- - Fix: dumpscript, fix missing imports in dumped script
- - Fix: admin_generator, fix issues with Django 1.9
- - Fix: template tags, move exception for import failure to inside of the template tags
- - Fix: reset_db, fix for Django 1.9
- - Fix: runserver_plus, fix for Django 1.9
 
+- New: RandomCharField, prepopulates random character string
+- New: (Not)NullFieldListFilter, filters for admin
+- New: runserver_plus, integrate with django-pdb
+- New: runserver_plus, add check_migrations from Django
+- Improvement: show_urls, nested namespace support
+- Improvement: show_urls, allow to specify alternative urlconf
+- Improvement: show_urls, support i18n_patterns
+- Improvement: show_urls, use --language to filter on a particular language
+- Improvement: admin_generator, added docstrings to module
+- Improvement: shell_plus, allow cli arguments to be passed to ipython
+- Improvement: shell_plus, fixed PYTHONPATH bug when using django-admin shell_plus --notebook
+- Improvement: shell_plus, set application_name on PostgreSQL databases
+- Improvement: shell_plus, load user pypython config file
+- Improvement: CreationDateTimeField, use auto_now_add instead of default ModificationDateTimeField
+- Improvement: ModificationDateTimeField, use auto_now instead of pre_save method
+- Improvement: ForeignKeyAutocompleteAdmin, added ability to filter autocomplete query
+- Fix: shell_plus, support for pypython>=0.27
+- Fix: shell_plus, load apps and models directly through the apps interface when available
+- Fix: shell_plus, use ipython start_ipython instead of embed
+- Fix: shell_plus, fix swalling ImportErrors with IPython 3 and higher
+- Fix: dumpscript, fix missing imports in dumped script
+- Fix: admin_generator, fix issues with Django 1.9
+- Fix: template tags, move exception for import failure to inside of the template tags
+- Fix: reset_db, fix for Django 1.9
+- Fix: runserver_plus, fix for Django 1.9
 
 1.5.5
 -----
 
 Changes:
- - Fix: sqldiff, previous Django 1.8 fix was slightly broken
 
+- Fix: sqldiff, previous Django 1.8 fix was slightly broken
 
 1.5.4
 -----
 
 Changes:
- - Improvement: syncdata, add skip-remove option
- - Improvement: logging, report how often mail was ratelimited
- - Fix: admin, Django 1.8 compatibility module_name is now called model_name
- - Fix: notes, Python 3.x fix force output of filter into list
- - Fix: sqldiff, fix for Django 1.8
 
+- Improvement: syncdata, add skip-remove option
+- Improvement: logging, report how often mail was ratelimited
+- Fix: admin, Django 1.8 compatibility module_name is now called model_name
+- Fix: notes, Python 3.x fix force output of filter into list
+- Fix: sqldiff, fix for Django 1.8
 
 1.5.3
 -----
 
 Changes:
- - New: ratelimiter, a simple ratelimiter filter for Python logging
- - Fix: various improvements for Django 1.8
- - Fix: sync_s3, use os.walk instead of os.path.walk (py3 fix)
- - Improvement: pipchecker, use name instead of url_name to fix casing mismatches
- - Improvement: pipchecker, use https
- - Improvement: pipchecker, fix issues with new(er) pip versions
- - Docs: fixed a few typos
- - Docs: added documentation about NOTEBOOK_ARGUMENTS settings
 
+- New: ratelimiter, a simple ratelimiter filter for Python logging
+- Fix: various improvements for Django 1.8
+- Fix: sync_s3, use os.walk instead of os.path.walk (py3 fix)
+- Improvement: pipchecker, use name instead of url_name to fix casing mismatches
+- Improvement: pipchecker, use https
+- Improvement: pipchecker, fix issues with new(er) pip versions
+- Docs: fixed a few typos
+- Docs: added documentation about NOTEBOOK_ARGUMENTS settings
 
 1.5.2
 -----
 
 Changes:
- - New: sqldsn, prints Data Source Name for defined database(s)
- - Fix: graph_models, Django 1.8 support
- - Fix: highlighting tag, fix usage of is_safe
- - Fix: runscript, fix for runscript with AppConfig apps
- - Fix: sqldiff, KeyError when index is missing in database
- - Fix: sqldiff, multi column indexes was also counted as a single colomn index
- - Improvements: JSONField, Added try/catch for importing json/simplejson for Django 1.7
 
+- New: sqldsn, prints Data Source Name for defined database(s)
+- Fix: graph_models, Django 1.8 support
+- Fix: highlighting tag, fix usage of is_safe
+- Fix: runscript, fix for runscript with AppConfig apps
+- Fix: sqldiff, KeyError when index is missing in database
+- Fix: sqldiff, multi column indexes was also counted as a single colomn index
+- Improvements: JSONField, Added try/catch for importing json/simplejson for Django 1.7
 
 1.5.1
 -----
 
 Changes:
- - New: runserver_plus, add support for --extra-files parameter
- - Fix: Django 1.7 defined MIDDLEWARE_CLASSES for tests
- - Fix: shell_plus, problem when auto-loading modules with empty '__module__' property
- - Improvement: shell_plus, IPython 3.x support for notebooks
- - Improvement: tests, move to py.test and lots of other improvements
- - Improvement: create_app, add migrations folder
- - Improvement: tox.ini, refactored to be more DRY
- - Improvement: runserver_plus, also reload on changes to translation files
- - Improvement: runserver_plus, add reloader_interval support
- - Improvement: create_template_tags, removed unused command line option
- - Docs: print_user_for_session, add note about SESSION_ENGINE
- - Docs: runserver_plus, added section about IO calls and CPU usage
 
+- New: runserver_plus, add support for --extra-files parameter
+- Fix: Django 1.7 defined MIDDLEWARE_CLASSES for tests
+- Fix: shell_plus, problem when auto-loading modules with empty '__module__' property
+- Improvement: shell_plus, IPython 3.x support for notebooks
+- Improvement: tests, move to py.test and lots of other improvements
+- Improvement: create_app, add migrations folder
+- Improvement: tox.ini, refactored to be more DRY
+- Improvement: runserver_plus, also reload on changes to translation files
+- Improvement: runserver_plus, add reloader_interval support
+- Improvement: create_template_tags, removed unused command line option
+- Docs: print_user_for_session, add note about SESSION_ENGINE
+- Docs: runserver_plus, added section about IO calls and CPU usage
 
 1.5.0
 -----
 
 Changes:
- - Fix: various fixes for Django 1.8
- - Improvement: shell_plus, autodetect vi mode by looking at $EDITOR shell env setting
- - Improvement: shell_plus, print which shell is being used at verbosity > 1
- - Improvement: shell_plus, added --no-browser option for IPython notebooks
- - Improvement: tox.ini, updated to latest Django versions
- - Docs: add reference to JSONField in documentation
- - Docs: fixed various typo's and links in docs and changelog
- - Docs: added some basic use cases to README
- - Docs: added information for companies or people wanting to donate towards the project
- - Fix: graphmodels, fix for python3
- - Fix: dumpscript, fix check for missing import_helper module in Python 3
- - Fix: runprofileserver, explicitly close file to avoid error on windows
- - Fix: json field, migration issues when adding new JSONField to existing model
- - Fix: runjobs, fix python3 issues
 
+- Fix: various fixes for Django 1.8
+- Improvement: shell_plus, autodetect vi mode by looking at $EDITOR shell env setting
+- Improvement: shell_plus, print which shell is being used at verbosity > 1
+- Improvement: shell_plus, added --no-browser option for IPython notebooks
+- Improvement: tox.ini, updated to latest Django versions
+- Docs: add reference to JSONField in documentation
+- Docs: fixed various typo's and links in docs and changelog
+- Docs: added some basic use cases to README
+- Docs: added information for companies or people wanting to donate towards the project
+- Fix: graphmodels, fix for python3
+- Fix: dumpscript, fix check for missing import_helper module in Python 3
+- Fix: runprofileserver, explicitly close file to avoid error on windows
+- Fix: json field, migration issues when adding new JSONField to existing model
+- Fix: runjobs, fix python3 issues
 
 1.4.9
 -----
 
 Changes:
- - New: drop_test_database, drops the test database
- - New: command_signals, git commit -a -m 'bumped version number' (see docs)
- - Bugfix: runserver_plus, removed empty lines when logging on Python 3
 
+- New: drop_test_database, drops the test database
+- New: command_signals, git commit -a -m 'bumped version number' (see docs)
+- Bugfix: runserver_plus, removed empty lines when logging on Python 3
 
 1.4.8
 -----
 
 Changes:
- - Bugfix: validators, fix NoWhitespaceValidator __eq__ check
 
+- Bugfix: validators, fix NoWhitespaceValidator __eq__ check
 
 1.4.7
 -----
 
 Changes:
- - New: validators.py, NoControlCharactersValidator and NoWhitespaceValidator
- - New: EmailNotificationCommand class, email exceptions from Django Commands
- - Improvement: runserver_plus, enable threading by default and added --nothreading
- - Improvement: runscript, better detection when import error occured in script
- - Improvement: runscript, use EmailNotificationCommand class
- - Deprecation: deprecated UUIDField since Django 1.8 will have a native version.
- - Removed: completely remove support for automatically finding project root.
 
+- New: validators.py, NoControlCharactersValidator and NoWhitespaceValidator
+- New: EmailNotificationCommand class, email exceptions from Django Commands
+- Improvement: runserver_plus, enable threading by default and added --nothreading
+- Improvement: runscript, better detection when import error occured in script
+- Improvement: runscript, use EmailNotificationCommand class
+- Deprecation: deprecated UUIDField since Django 1.8 will have a native version.
+- Removed: completely remove support for automatically finding project root.
 
 1.4.6
 -----
 
 Changes:
- - Improvement: sqldiff, fix for dbcolumn not used in few places when generating the sqldiff
- - Fix: sqldiff, backwards compatibility fix for Django 1.4
- - Fix: ForeignKey Field, handling of __str__ instead of __unicode__ in python3
 
+- Improvement: sqldiff, fix for dbcolumn not used in few places when generating the sqldiff
+- Fix: sqldiff, backwards compatibility fix for Django 1.4
+- Fix: ForeignKey Field, handling of __str__ instead of __unicode__ in python3
 
 1.4.5
 -----
 
 Changes:
- - New: clear_cache, Clear django cache, useful when testing or deploying
- - Improvement: AutoSlugField, add the possibility to define a custom slugify function
- - Improvement: shell_plus --notebook, add a big warning when the notebook extension is not going to be loaded
- - Improvement: setup.py, add pypy classifier
- - Improvement: readme, add pypy badges
- - Fix: admin_generator, Fixed Python 3 __unicode__/__str__ compatibility
 
+- New: clear_cache, Clear django cache, useful when testing or deploying
+- Improvement: AutoSlugField, add the possibility to define a custom slugify function
+- Improvement: shell_plus --notebook, add a big warning when the notebook extension is not going to be loaded
+- Improvement: setup.py, add pypy classifier
+- Improvement: readme, add pypy badges
+- Fix: admin_generator, Fixed Python 3 __unicode__/__str__ compatibility
 
 1.4.4
 -----
 
 Changes:
- - Fix: admin_generator, fix ImproperlyConfigured exception on Django 1.7
- - Improvement: Remove "requires_model_validation" and "requires_system_checks" in commands which set the default value
 
+- Fix: admin_generator, fix ImproperlyConfigured exception on Django 1.7
+- Improvement: Remove "requires_model_validation" and "requires_system_checks" in commands which set the default value
 
 1.4.1
 -----
 
 Changes:
- - New: shell_plus, Added python-prompt-toolkit integration for shell_plus
- - New: shell_plus, Added --ptipython (PYPython + IPython)
- - Improvement: reset_db, output traceback to easy debugging in case of error
- - Improvement: dumpscript, add --autofield to dumpscript to include autofields in export
- - Improvement: show_urls, Include namespace in URL name
- - Improvement: show_urls, Allow multiple decorators on the show_urls command
- - Improvement: runscript, show script errors with verbosity > 1
- - Fix: jobs, daily_cleanup job use clearsessions for Django 1.5 and later
- - Fix: shell_plus, refactored importing and selecting shells to avoid polluted exception
- - Fix: shell_plus, Fix model loading for sentry
 
+- New: shell_plus, Added python-prompt-toolkit integration for shell_plus
+- New: shell_plus, Added --ptipython (PYPython + IPython)
+- Improvement: reset_db, output traceback to easy debugging in case of error
+- Improvement: dumpscript, add --autofield to dumpscript to include autofields in export
+- Improvement: show_urls, Include namespace in URL name
+- Improvement: show_urls, Allow multiple decorators on the show_urls command
+- Improvement: runscript, show script errors with verbosity > 1
+- Fix: jobs, daily_cleanup job use clearsessions for Django 1.5 and later
+- Fix: shell_plus, refactored importing and selecting shells to avoid polluted exception
+- Fix: shell_plus, Fix model loading for sentry
 
 1.4.0
 -----
 
 Changes:
- - New admin_generator, can generate a admin.py file from models
- - Improvement: sqldiff, use the same exit codes as diff uses
- - Improvement: sqldiff, add support for unsigned numeric fields
- - Improvement: sqldiff, add NOT NULL support for MySQL
- - Improvement: sqldiff, add proper AUTO_INCREMENT support for MySQL
- - Improvement: sqldiff, detect tables for which no model exists
- - Improvement: travis.yml, add pypy to tests
- - Fix: sqldiff, fix for mysql misreported field lengths
- - Fix: sqldiff, in PG custom int primary keys would be mistaking for serial
- - Fix: sqldiff, use Django 1.7 db_parameters() for detect check constraints
- - Fix: update_permissions, Django 1.7 support
- - Fix: encrypted fields, fix for Django 1.7 migrations
 
+- New admin_generator, can generate a admin.py file from models
+- Improvement: sqldiff, use the same exit codes as diff uses
+- Improvement: sqldiff, add support for unsigned numeric fields
+- Improvement: sqldiff, add NOT NULL support for MySQL
+- Improvement: sqldiff, add proper AUTO_INCREMENT support for MySQL
+- Improvement: sqldiff, detect tables for which no model exists
+- Improvement: travis.yml, add pypy to tests
+- Fix: sqldiff, fix for mysql misreported field lengths
+- Fix: sqldiff, in PG custom int primary keys would be mistaking for serial
+- Fix: sqldiff, use Django 1.7 db_parameters() for detect check constraints
+- Fix: update_permissions, Django 1.7 support
+- Fix: encrypted fields, fix for Django 1.7 migrations
 
 1.3.11
 ------
 
 Changes:
- - Improvement: sqldiff, show differences for not managed tables
- - Improvement: show_urls -f aligned, 3 spaces between columns
- - Improvement: reset_db, support mysql options files in reset_db
- - Fix: sqldiff, Fixed bug with --output_text option and notnull-differ text
- - Fix: reset_db, Fix for PostgreSQL databases with dashes, dots, etc in the name
- - Fix: dumpscript, AttributeError for datefields that are None
- - Docs: Adding RUNSERVERPLUS_SERVER_ADDRESS_PORT to docs
 
+- Improvement: sqldiff, show differences for not managed tables
+- Improvement: show_urls -f aligned, 3 spaces between columns
+- Improvement: reset_db, support mysql options files in reset_db
+- Fix: sqldiff, Fixed bug with --output_text option and notnull-differ text
+- Fix: reset_db, Fix for PostgreSQL databases with dashes, dots, etc in the name
+- Fix: dumpscript, AttributeError for datefields that are None
+- Docs: Adding RUNSERVERPLUS_SERVER_ADDRESS_PORT to docs
 
 1.3.10
 ------
 
 Changes:
- - Fix: show_urls, fix bug in new formatter when column is empty
 
+- Fix: show_urls, fix bug in new formatter when column is empty
 
 1.3.9
 -----
 
 Changes:
- - Feature: shell_plus, add --kernel option to start as standalone IPython kernel
- - Feature: reset_db, Programmatically determine PostGIS template
- - Feature: sqldiff, add support for PointField and MultiPolygonField
- - Test: renamed test app
- - Fix: runserver_plus, --print-sql for Django 1.7
- - Fix: shell_plus, --print-sql for Django 1.7
- - Fix: show_urls, add support for functions that use functools.partial
- - Fix: show_urls, add formatter for aligned output (will most likely become future default)
- - Fix: shell_plus / notebook, support for Django 1.7
- - Docs: various fixes and improvements
- - Cleanup: Remove work arounds for Django 0.96 and earlier
 
+- Feature: shell_plus, add --kernel option to start as standalone IPython kernel
+- Feature: reset_db, Programmatically determine PostGIS template
+- Feature: sqldiff, add support for PointField and MultiPolygonField
+- Test: renamed test app
+- Fix: runserver_plus, --print-sql for Django 1.7
+- Fix: shell_plus, --print-sql for Django 1.7
+- Fix: show_urls, add support for functions that use functools.partial
+- Fix: show_urls, add formatter for aligned output (will most likely become future default)
+- Fix: shell_plus / notebook, support for Django 1.7
+- Docs: various fixes and improvements
+- Cleanup: Remove work arounds for Django 0.96 and earlier
 
 1.3.8
 -----
 
 Changes:
- - Feature: show_urls, add option to specify dense or verbose output format
- - Improvement: better support for django 1.7 migrations
- - Improvement: better support for django's admin docs
- - BugFix: runjob, job_name and app_name was swapped in error message
- - Docs: Update link to chinese docs
- - Python3: unreferenced_files, fix python3 compatibility
- - Python3: pipchecker, fix python3 compatibility
+
+- Feature: show_urls, add option to specify dense or verbose output format
+- Improvement: better support for django 1.7 migrations
+- Improvement: better support for django's admin docs
+- BugFix: runjob, job_name and app_name was swapped in error message
+- Docs: Update link to chinese docs
+- Python3: unreferenced_files, fix python3 compatibility
+- Python3: pipchecker, fix python3 compatibility
 
 1.3.7
 -----
 
 Changes:
- - Reinstated: clean_pyc and compile_pyc commands, these now depends on BASE_DIR
-               in settings.py as per Django 1.6. We urge everybody to include a
-               BASE_DIR settings in their project file! auto-detecting the
-               project-root is now deprecated and will be removed in 1.4.0.
- - I18N: Added russian locale
- - Docs: runscript, Add section about passing arguments to scripts
- - Python3: show_url, Fixed to AttributeError 'func_globals'
- - Deprecated: clean_pyc, compile_pyc, Auto-detecting project root
 
+- Reinstated: clean_pyc and compile_pyc commands, these now depends on BASE_DIR
+              in settings.py as per Django 1.6. We urge everybody to include a
+              BASE_DIR settings in their project file! auto-detecting the
+              project-root is now deprecated and will be removed in 1.4.0.
+- I18N: Added russian locale
+- Docs: runscript, Add section about passing arguments to scripts
+- Python3: show_url, Fixed to AttributeError 'func_globals'
+- Deprecated: clean_pyc, compile_pyc, Auto-detecting project root
 
 1.3.6
 -----
 
 Changes:
- - Additional version bump because we mistakenly already uploaded
-   version 1.3.5 of the wheel package with the code of 1.3.4
 
+- Additional version bump because we mistakenly already uploaded
+  version 1.3.5 of the wheel package with the code of 1.3.4
 
 1.3.5
 -----
 
 Changes:
- - Feature: Django-Extensions is now also distributed as a Wheel package
- - Improvement: dumpscript, improved the readability of comments in generated script
- - Improvement: sqldiff, backported get_constraints() for PostgreSQL
- - Improvement: shell_plus, consistent colorization
- - BugFix: encrypted fields, there is no decoding to unicode in Python 3
- - BugFix: shell_plus, importing modules failed in some edge cases
- - Django 1.7: included Django 1.7 in test suite
- - Python 3.4: included Python 3.4 in test suite
 
+- Feature: Django-Extensions is now also distributed as a Wheel package
+- Improvement: dumpscript, improved the readability of comments in generated script
+- Improvement: sqldiff, backported get_constraints() for PostgreSQL
+- Improvement: shell_plus, consistent colorization
+- BugFix: encrypted fields, there is no decoding to unicode in Python 3
+- BugFix: shell_plus, importing modules failed in some edge cases
+- Django 1.7: included Django 1.7 in test suite
+- Python 3.4: included Python 3.4 in test suite
 
 1.3.4
 -----
 
 Changes:
- - Feature: Start maintaining a CHANGELOG file in the repository
- - Feature: ActivatorModelManager now has an ActivatorQuerySet
- - Feature: Add a deconstruct() method for future Django 1.7 migration compatibility
- - Feature: show_urls, now support --language for i18n_patterns
- - Feature: show_urls, now shows the decoraters set on a view function
- - Feature: graph_models, now support --include-models to restrict the graph to specified models
- - Feature: print_settings, allow to specify the settings you want to see
- - Improvement: graph_models, use '//' instead of '#' as comment character in dot files
- - Improvement: graph_models, added error message for abstract models without explicit relations
- - Improvement: JSONField, use python's built-in json support by default with fallback on django.utils.simplejson
- - Improvement: PostgreSQLUUIDField, parse value into UUID type before sending it to the database
- - Improvement: Use django.JQuery in autocomplete.js if available
- - Improvement: use "a not in b" instead of "not a in b" in the entire codebase
- - Removed: clean_pyc command since it does not work correctly in many cases
- - Removed: sync_media_s3 command in favor of sync_s3
- - BugFix: syncdata, use pk instead of id for identifying primary key of objects
- - BugFix: sync_s3, use safer content type per default
- - BugFix: export_emails, filtering on groups
- - BugFix: print_user_for_session, use USERNAME_FIELD if defined
- - BugFix: update_permission, fixed TypeError issue
- - BugFix: JSONField, do not coerse a json string into a python list
- - BugFix: import json issue by using absolute imports
- - BugFix: add minimal version number to six (>=1.2)
- - Docs: graph_models, Added some documentation about using dot templates
- - Docs: reset_db, short description on SQL DDL used
- - Docs: Added specific list of supported Python and Django versions
- - Docs: Add link to GoDjango screencast
- - Docs: Add ShortUUIDField to docs
- - Python3: fixes to graph_models and export_emails for Python3 compatibility
 
+- Feature: Start maintaining a CHANGELOG file in the repository
+- Feature: ActivatorModelManager now has an ActivatorQuerySet
+- Feature: Add a deconstruct() method for future Django 1.7 migration compatibility
+- Feature: show_urls, now support --language for i18n_patterns
+- Feature: show_urls, now shows the decoraters set on a view function
+- Feature: graph_models, now support --include-models to restrict the graph to specified models
+- Feature: print_settings, allow to specify the settings you want to see
+- Improvement: graph_models, use '//' instead of '#' as comment character in dot files
+- Improvement: graph_models, added error message for abstract models without explicit relations
+- Improvement: JSONField, use python's built-in json support by default with fallback on django.utils.simplejson
+- Improvement: PostgreSQLUUIDField, parse value into UUID type before sending it to the database
+- Improvement: Use django.JQuery in autocomplete.js if available
+- Improvement: use "a not in b" instead of "not a in b" in the entire codebase
+- Removed: clean_pyc command since it does not work correctly in many cases
+- Removed: sync_media_s3 command in favor of sync_s3
+- BugFix: syncdata, use pk instead of id for identifying primary key of objects
+- BugFix: sync_s3, use safer content type per default
+- BugFix: export_emails, filtering on groups
+- BugFix: print_user_for_session, use USERNAME_FIELD if defined
+- BugFix: update_permission, fixed TypeError issue
+- BugFix: JSONField, do not coerse a json string into a python list
+- BugFix: import json issue by using absolute imports
+- BugFix: add minimal version number to six (>=1.2)
+- Docs: graph_models, Added some documentation about using dot templates
+- Docs: reset_db, short description on SQL DDL used
+- Docs: Added specific list of supported Python and Django versions
+- Docs: Add link to GoDjango screencast
+- Docs: Add ShortUUIDField to docs
+- Python3: fixes to graph_models and export_emails for Python3 compatibility
 
 1.3.3
 -----
 
 Changes:
- - Docs: Made it clearer that Django Extensions requires Django 1.4 or higher
- - Translations: FR Updated
- - Python3: Fix for shell_plus
 
+- Docs: Made it clearer that Django Extensions requires Django 1.4 or higher
+- Translations: FR Updated
+- Python3: Fix for shell_plus
 
 1.3.0
 -----
 
 Changes:
- - Feature: SQLDiff much better notnull detection
- - Feature: reset_db add option to explicit set the PostGreSQL owner of the newly created DB
- - Feature: shell_plus added support for MongoEngine
- - Feature: sync_s3 enable syncing to other cloud providers compatible with s3
- - Improvement: ForeignKeyAutocompleteAdmin add option to limit queryset
- - BugFix: graph_models fix issue with models without primary key
- - BugFix: graph_models fix UnicodeDecodeError using --verbose-names
- - BugFix: dumpscript fix problems with date/datetimes by saving them now as ISO8601
- - Docs: many improvements
- - Docs: Chinese translation !!!
- - Python3: various improvements
- - Tests: add Django 1.6
+
+- Feature: SQLDiff much better notnull detection
+- Feature: reset_db add option to explicit set the PostGreSQL owner of the newly created DB
+- Feature: shell_plus added support for MongoEngine
+- Feature: sync_s3 enable syncing to other cloud providers compatible with s3
+- Improvement: ForeignKeyAutocompleteAdmin add option to limit queryset
+- BugFix: graph_models fix issue with models without primary key
+- BugFix: graph_models fix UnicodeDecodeError using --verbose-names
+- BugFix: dumpscript fix problems with date/datetimes by saving them now as ISO8601
+- Docs: many improvements
+- Docs: Chinese translation !!!
+- Python3: various improvements
+- Tests: add Django 1.6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f488dbe8..3c04c570 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,10 +18,13 @@ $ source venv/bin/activate
 # install django-extensions in development mode
 (venv) $ pip install -e .
 # install dependencies
+# might need to replace DJango with 'Django<4' until issue #1695 is resolved.
 (venv) $ pip install Django -r requirements-dev.txt
 
 # for accessing the GUI portion of the test application
 (venv) $ export DJANGO_EXTENSIONS_DATABASE_NAME="db.sqlite3"    # you may change if you want to use any other database
+# run migrations
+(venv) $ python manage.py migrate
 # start the development server
 (venv) $ python manage.py runserver
 ```
diff --git a/README.rst b/README.rst
index 37f8bc4d..d54fc37d 100644
--- a/README.rst
+++ b/README.rst
@@ -68,7 +68,9 @@ Installing It
 =============
 
 To enable `django_extensions` in your project you need to add it to `INSTALLED_APPS` in your projects
-`settings.py` file::
+`settings.py` file:
+
+.. code-block:: python
 
     INSTALLED_APPS = (
         ...
diff --git a/django_extensions/__init__.py b/django_extensions/__init__.py
index 88026466..2c7e8b7e 100644
--- a/django_extensions/__init__.py
+++ b/django_extensions/__init__.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-VERSION = (3, 1, 3)
+VERSION = (3, 1, 6, 'DEV')
 
 
 def get_version(version):
diff --git a/django_extensions/locale/ar/LC_MESSAGES/django.po b/django_extensions/locale/ar/LC_MESSAGES/django.po
new file mode 100644
index 00000000..0963b4f8
--- /dev/null
+++ b/django_extensions/locale/ar/LC_MESSAGES/django.po
@@ -0,0 +1,109 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-06-06 11:44+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: admin/__init__.py:139
+msgid "and"
+msgstr "و"
+
+#: admin/__init__.py:141
+#, python-format
+msgid "Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
+msgstr "إستعمل الحقل الأيسر من (model_name)% لبحث ضمن الأحقال التالية (field_list)% "
+
+#: admin/filter.py:24 admin/filter.py:53
+msgid "Yes"
+msgstr "نعم"
+
+#: admin/filter.py:25 admin/filter.py:54
+msgid "No"
+msgstr "لا"
+
+#: admin/filter.py:32
+msgid "All"
+msgstr "كل"
+
+#: db/models.py:18
+msgid "created"
+msgstr "تم تكونه"
+
+#: db/models.py:19
+msgid "modified"
+msgstr "تم تعديله"
+
+#: db/models.py:37
+msgid "title"
+msgstr "عنوان"
+
+#: db/models.py:38
+msgid "description"
+msgstr "وصف"
+
+#: db/models.py:59
+msgid "slug"
+msgstr "رابط "
+
+#: db/models.py:120 mongodb/models.py:76
+msgid "Inactive"
+msgstr "غير نشط"
+
+#: db/models.py:121 mongodb/models.py:77
+msgid "Active"
+msgstr "نشط"
+
+#: db/models.py:123
+msgid "status"
+msgstr "الحالة"
+
+#: db/models.py:124 mongodb/models.py:80
+msgid "keep empty for an immediate activation"
+msgstr "أترك الحقل فارغ ليتم التنشيط مباشرة"
+
+#: db/models.py:125 mongodb/models.py:81
+msgid "keep empty for indefinite activation"
+msgstr "أترك الحقل فارغ لتنشيط لمدة غير محددة"
+
+#: mongodb/fields/__init__.py:22
+#, python-format
+msgid "String (up to %(max_length)s)"
+msgstr "سلسلة الإحرف (طولها يصل إلى %(max_length))"
+
+#: validators.py:14
+msgid "Control Characters like new lines or tabs are not allowed."
+msgstr "لا يسمح  إستعمال أحرف تحكم مثل حرف العودة إلى السطر أو علامات التبويب"
+
+#: validators.py:48
+msgid "Leading and Trailing whitespaces are not allowed."
+msgstr "المسافات البيضاء الزائدة عند البداية أو نهاية غير مسموح بها"
+
+#: validators.py:74
+msgid "Only a hex string is allowed."
+msgstr "مسموح إستعمال سلسلة أحرف hex فقط"
+
+#: validators.py:75
+#, python-format
+msgid "Invalid length. Must be %(length)d characters."
+msgstr "الطول غير مقبول, يجب أن لا يكون أطول من %(length)d"
+
+#: validators.py:76
+#, python-format
+msgid "Ensure that there are more than %(min)s characters."
+msgstr "تأكد أن طول سلسلة الإحرف أطول من (min)% "
+
+#: validators.py:77
+#, python-format
+msgid "Ensure that there are no more than %(max)s characters."
+msgstr "تأكد أن طول سلسلة الأحرف لا تتجوز (max)% "
diff --git a/django_extensions/management/commands/dumpscript.py b/django_extensions/management/commands/dumpscript.py
index 086a8c81..98392f4c 100644
--- a/django_extensions/management/commands/dumpscript.py
+++ b/django_extensions/management/commands/dumpscript.py
@@ -41,7 +41,7 @@ from django.db.models import (
 )
 from django.db.models.deletion import Collector
 from django.utils import timezone
-from django.utils.encoding import force_str, smart_text
+from django.utils.encoding import force_str, smart_str
 
 from django_extensions.management.utils import signalcommand
 
@@ -205,7 +205,7 @@ class ModelCode(Code):
         Return a dictionary of import statements, with the variable being
         defined as the key.
         """
-        return {self.model.__name__: smart_text(self.model.__module__)}
+        return {self.model.__name__: smart_str(self.model.__module__)}
     imports = property(get_imports)
 
     def get_lines(self):
diff --git a/django_extensions/management/commands/graph_models.py b/django_extensions/management/commands/graph_models.py
index c70e1696..3e0ef77d 100644
--- a/django_extensions/management/commands/graph_models.py
+++ b/django_extensions/management/commands/graph_models.py
@@ -153,6 +153,12 @@ class Command(BaseCommand):
                 'dest': 'relations_as_fields',
                 'help': 'Do not show relations as fields in the graph.',
             },
+            '--relation-fields-only': {
+                'action': 'store',
+                'default': False,
+                'dest': 'relation_fields_only',
+                'help': 'Only display fields that are relevant for relations',
+            },
             '--disable-sort-fields -S': {
                 'action': 'store_false',
                 'default': True,
@@ -163,7 +169,7 @@ class Command(BaseCommand):
                 'action': 'store_true',
                 'default': False,
                 'dest': 'hide_edge_labels',
-                'help': 'Do not showrelations labels in the graph.',
+                'help': 'Do not show relations labels in the graph.',
             },
             '--arrow-shape': {
                 'action': 'store',
@@ -172,6 +178,12 @@ class Command(BaseCommand):
                 'choices': ['box', 'crow', 'curve', 'icurve', 'diamond', 'dot', 'inv', 'none', 'normal', 'tee', 'vee'],
                 'help': 'Arrow shape to use for relations. Default is dot. Available shapes: box, crow, curve, icurve, diamond, dot, inv, none, normal, tee, vee.',
             },
+            '--color-code-deletions': {
+                'action': 'store_true',
+                'default': False,
+                'dest': 'color_code_deletions',
+                'help': 'Color the relations according to their on_delete setting, where it it applicable. The colors are: red (CASCADE), orange (SET_NULL), green (SET_DEFAULT), yellow (SET), blue (PROTECT), grey (DO_NOTHING) and purple (RESTRICT).',
+            },
             '--rankdir': {
                 'action': 'store',
                 'default': 'TB',
diff --git a/django_extensions/management/commands/managestate.py b/django_extensions/management/commands/managestate.py
new file mode 100644
index 00000000..071256be
--- /dev/null
+++ b/django_extensions/management/commands/managestate.py
@@ -0,0 +1,174 @@
+import json
+from operator import itemgetter
+from pathlib import Path
+
+from django import get_version
+from django.core.management import call_command
+from django.core.management.base import BaseCommand, CommandError
+from django.db import DEFAULT_DB_ALIAS, connections
+from django.db.migrations.loader import MigrationLoader
+from django.db.migrations.recorder import MigrationRecorder
+from django.utils import timezone
+from django.utils.version import get_version_tuple
+
+DEFAULT_FILENAME = 'managestate.json'
+DEFAULT_STATE = 'default'
+VERSION = get_version_tuple(get_version())
+
+
+class Command(BaseCommand):
+    help = 'Manage database state in the convenient way.'
+    conn = database = filename = verbosity = None
+    migrate_args = migrate_options = None
+    _applied_migrations = None
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            'action', choices=('dump', 'load'),
+            help='An action to do. '
+                 'Dump action saves applied migrations to a file. '
+                 'Load action applies migrations specified in a file.',
+        )
+        parser.add_argument(
+            'state', nargs='?', default=DEFAULT_STATE,
+            help=f'A name of a state. Usually a name of a git branch. Defaults to "{DEFAULT_STATE}"',
+        )
+        parser.add_argument(
+            '-d', '--database', default=DEFAULT_DB_ALIAS,
+            help=f'Nominates a database to synchronize. Defaults to the "{DEFAULT_DB_ALIAS}" database.',
+        )
+        parser.add_argument(
+            '-f', '--filename', default=DEFAULT_FILENAME,
+            help=f'A file to write to. Defaults to "{DEFAULT_FILENAME}"',
+        )
+
+        # migrate command arguments
+        parser.add_argument(
+            '--noinput', '--no-input', action='store_false', dest='interactive',
+            help='The argument for "migrate" command. '
+                 'Tells Django to NOT prompt the user for input of any kind.',
+        )
+        parser.add_argument(
+            '--fake', action='store_true',
+            help='The argument for "migrate" command. '
+                 'Mark migrations as run without actually running them.',
+        )
+        parser.add_argument(
+            '--fake-initial', action='store_true',
+            help='The argument for "migrate" command. '
+                 'Detect if tables already exist and fake-apply initial migrations if so. Make sure '
+                 'that the current database schema matches your initial migration before using this '
+                 'flag. Django will only check for an existing table name.',
+        )
+        parser.add_argument(
+            '--plan', action='store_true',
+            help='The argument for "migrate" command. '
+                 'Shows a list of the migration actions that will be performed.',
+        )
+        parser.add_argument(
+            '--run-syncdb', action='store_true',
+            help='The argument for "migrate" command. '
+                 'Creates tables for apps without migrations.',
+        )
+        parser.add_argument(
+            '--check', action='store_true', dest='check_unapplied',
+            help='The argument for "migrate" command. '
+                 'Exits with a non-zero status if unapplied migrations exist.',
+        )
+
+    def handle(self, action, database, filename, state, *args, **options):
+        self.migrate_args = args
+        self.migrate_options = options
+        self.verbosity = options['verbosity']
+        self.conn = connections[database]
+        self.database = database
+        self.filename = filename
+        getattr(self, action)(state)
+
+    def dump(self, state: str):
+        """Save applied migrations to a file."""
+        migrated_apps = self.get_migrated_apps()
+        migrated_apps.update(self.get_applied_migrations())
+        self.write({state: migrated_apps})
+        self.stdout.write(self.style.SUCCESS(
+            f'Migrations for state "{state}" have been successfully saved to {self.filename}.'
+        ))
+
+    def load(self, state: str):
+        """Apply migrations from a file."""
+        migrations = self.read().get(state)
+        if migrations is None:
+            raise CommandError(f'No such state saved: {state}')
+
+        if VERSION < (3, 0):
+            self.migrate_options.pop('check_unapplied', None)
+
+        kwargs = {
+            **self.migrate_options,
+            'database': self.database,
+            'verbosity': self.verbosity - 1 if self.verbosity > 1 else 0
+        }
+
+        for app, migration in migrations.items():
+            if self.is_applied(app, migration):
+                continue
+
+            if self.verbosity > 1:
+                self.stdout.write(self.style.WARNING(f'Applying migrations for "{app}"'))
+            args = (app, migration, *self.migrate_args)
+            call_command('migrate', *args, **kwargs)
+
+        self.stdout.write(self.style.SUCCESS(
+            f'Migrations for "{state}" have been successfully applied.'
+        ))
+
+    def get_migrated_apps(self) -> dict:
+        """Installed apps having migrations."""
+        apps = MigrationLoader(self.conn).migrated_apps
+        migrated_apps = dict.fromkeys(apps, 'zero')
+        if self.verbosity > 1:
+            self.stdout.write('Apps having migrations: ' + ', '.join(sorted(migrated_apps)))
+        return migrated_apps
+
+    def get_applied_migrations(self) -> dict:
+        """Installed apps with last applied migrations."""
+        if self._applied_migrations:
+            return self._applied_migrations
+
+        migrations = MigrationRecorder(self.conn).applied_migrations()
+        migrations = migrations if VERSION < (3, 0) else migrations.keys()
+        last_applied = sorted(migrations, key=itemgetter(1))
+
+        self._applied_migrations = dict(last_applied)
+        return self._applied_migrations
+
+    def is_applied(self, app: str, migration: str) -> bool:
+        """Check whether a migration for an app is applied or not."""
+        applied = self.get_applied_migrations().get(app)
+        if applied == migration:
+            if self.verbosity > 1:
+                self.stdout.write(self.style.WARNING(
+                    f'Migrations for "{app}" are already applied.'
+                ))
+            return True
+        return False
+
+    def read(self) -> dict:
+        """Get saved state from the file."""
+        path = Path(self.filename)
+        if not path.exists() or not path.is_file():
+            raise CommandError(f'No such file: {self.filename}')
+
+        with open(self.filename) as file:
+            return json.load(file)
+
+    def write(self, data: dict):
+        """Write new data to the file using existent one."""
+        try:
+            saved = self.read()
+        except CommandError:
+            saved = {}
+
+        saved.update(data, updated_at=str(timezone.now()))
+        with open(self.filename, 'w') as file:
+            json.dump(saved, file, indent=2, sort_keys=True)
diff --git a/django_extensions/management/commands/pipchecker.py b/django_extensions/management/commands/pipchecker.py
index 5615d509..e3563ee2 100644
--- a/django_extensions/management/commands/pipchecker.py
+++ b/django_extensions/management/commands/pipchecker.py
@@ -24,7 +24,35 @@ try:
     except ImportError:
         from pip._internal.download import PipSession  # type:ignore
     from pip._internal.req.req_file import parse_requirements
-    from pip._internal.utils.misc import get_installed_distributions
+    try:
+        from pip._internal.utils.misc import get_installed_distributions  # type:ignore
+    except ImportError:
+        from typing import cast
+
+        def get_installed_distributions(
+            local_only=True,
+            include_editables=True,
+            editables_only=False,
+            user_only=False,
+            paths=None,
+        ):
+            """Return a list of installed Distribution objects.
+            Left for compatibility until direct pkg_resources uses are refactored out.
+            """
+            from pip._internal.metadata import get_default_environment, get_environment
+            from pip._internal.metadata.pkg_resources import Distribution as _Dist
+
+            if paths is None:
+                env = get_default_environment()
+            else:
+                env = get_environment(paths)
+            dists = env.iter_installed_distributions(
+                local_only=local_only,
+                include_editables=include_editables,
+                editables_only=editables_only,
+                user_only=user_only,
+            )
+            return [cast(_Dist, dist)._dist for dist in dists]
 except ImportError:
     # pip < 10
     try:
@@ -152,6 +180,7 @@ class Command(BaseCommand):
                     try:
                         available = pypi.package_releases(req["pip_req"].name, True) or pypi.package_releases(req["pip_req"].name.replace('-', '_'), True)
                         retry = False
+                        sleep(1)  # crude way slow down to avoid HTTPTooManyRequests
                     except Fault as err:
                         self.stdout.write(err.faultString)
                         self.stdout.write("Retrying in 60 seconds!")
diff --git a/django_extensions/management/commands/reset_db.py b/django_extensions/management/commands/reset_db.py
index bca8247f..09bc1d43 100644
--- a/django_extensions/management/commands/reset_db.py
+++ b/django_extensions/management/commands/reset_db.py
@@ -24,7 +24,7 @@ class Command(BaseCommand):
     def add_arguments(self, parser):
         super().add_arguments(parser)
         parser.add_argument(
-            '--noinput', action='store_false',
+            '--noinput', '--no-input', action='store_false',
             dest='interactive', default=True,
             help='Tells Django to NOT prompt the user for input of any kind.'
         )
diff --git a/django_extensions/management/commands/reset_schema.py b/django_extensions/management/commands/reset_schema.py
index 95b56732..8ef0c0f3 100644
--- a/django_extensions/management/commands/reset_schema.py
+++ b/django_extensions/management/commands/reset_schema.py
@@ -24,7 +24,7 @@ class Command(BaseCommand):
     def add_arguments(self, parser):
         super().add_arguments(parser)
         parser.add_argument(
-            '--noinput', action='store_false',
+            '--noinput', '--no-input', action='store_false',
             dest='interactive', default=True,
             help='Tells Django to NOT prompt the user for input of any kind.'
         )
diff --git a/django_extensions/management/commands/runscript.py b/django_extensions/management/commands/runscript.py
index a6739029..921c6e39 100644
--- a/django_extensions/management/commands/runscript.py
+++ b/django_extensions/management/commands/runscript.py
@@ -89,8 +89,6 @@ class Command(EmailNotificationCommand):
 
     @signalcommand
     def handle(self, *args, **options):
-        self.check()
-        self.check_migrations()
 
         NOTICE = self.style.SQL_TABLE
         NOTICE2 = self.style.SQL_FIELD
diff --git a/django_extensions/management/commands/runserver_plus.py b/django_extensions/management/commands/runserver_plus.py
index 3f69485b..867e85fb 100644
--- a/django_extensions/management/commands/runserver_plus.py
+++ b/django_extensions/management/commands/runserver_plus.py
@@ -150,6 +150,8 @@ class Command(BaseCommand):
                             help='Specifies an output file to send a copy of all messages (not flushed immediately).')
         parser.add_argument('--print-sql', action='store_true', default=False,
                             help="Print SQL queries as they're executed")
+        parser.add_argument('--truncate-sql', action='store', type=int,
+                            help="Truncate SQL queries to a number of characters.")
         parser.add_argument('--print-sql-location', action='store_true', default=False,
                             help="Show location in code where SQL query generated from")
         cert_group = parser.add_mutually_exclusive_group()
@@ -286,7 +288,9 @@ class Command(BaseCommand):
             self.addr = '::1' if self.use_ipv6 else '127.0.0.1'
             self._raw_ipv6 = True
 
-        with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"], print_sql_location=options["print_sql_location"], logger=logger.info, confprefix="RUNSERVER_PLUS"):
+        truncate = None if options["truncate_sql"] == 0 else options["truncate_sql"]
+
+        with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"], print_sql_location=options["print_sql_location"], truncate=truncate, logger=logger.info, confprefix="RUNSERVER_PLUS"):
             self.inner_run(options)
 
     def get_handler(self, *args, **options):
@@ -318,7 +322,7 @@ class Command(BaseCommand):
         class WSGIRequestHandler(_WSGIRequestHandler):
             def make_environ(self):
                 environ = super().make_environ()
-                if not options['keep_meta_shutdown_func']:
+                if not options['keep_meta_shutdown_func'] and 'werkzeug.server.shutdown' in environ:
                     del environ['werkzeug.server.shutdown']
                 return environ
 
diff --git a/django_extensions/management/commands/shell_plus.py b/django_extensions/management/commands/shell_plus.py
index c5a97282..8229918f 100644
--- a/django_extensions/management/commands/shell_plus.py
+++ b/django_extensions/management/commands/shell_plus.py
@@ -84,6 +84,10 @@ class Command(BaseCommand):
             default=False,
             help="Print SQL queries as they're executed"
         )
+        parser.add_argument(
+            '--truncate-sql', action='store', type=int,
+            help="Truncate SQL queries to a number of characters."
+        )
         parser.add_argument(
             '--print-sql-location', action='store_true',
             default=False,
@@ -251,23 +255,19 @@ class Command(BaseCommand):
             ksm = app.kernel_spec_manager
             for kid, ks in self.generate_kernel_specs(app, ipython_arguments).items():
                 roots = [os.path.dirname(ks.resource_dir), ksm.user_kernel_dir]
-                success = False
+
                 for root in roots:
                     kernel_dir = os.path.join(root, kid)
                     try:
                         if not os.path.exists(kernel_dir):
                             os.makedirs(kernel_dir)
-
                         with open(os.path.join(kernel_dir, 'kernel.json'), 'w') as f:
                             f.write(ks.to_json())
-
-                        success = True
                         break
                     except OSError:
                         continue
-
-                if not success:
-                    raise CommandError("Could not write kernel %r in directories %r" % (kid, roots))
+                else:
+                    raise CommandError('Could not write kernel %r in directories %r' % (kid, roots))
 
         app.start()
 
@@ -508,8 +508,9 @@ for k, m in shells.import_objects({}, no_style()).items():
         print_sql = getattr(settings, 'SHELL_PLUS_PRINT_SQL', False)
         runner = None
         runner_name = None
+        truncate = None if options["truncate_sql"] == 0 else options["truncate_sql"]
 
-        with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"] or print_sql, print_sql_location=options["print_sql_location"], confprefix="SHELL_PLUS"):
+        with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"] or print_sql, truncate=truncate, print_sql_location=options["print_sql_location"], confprefix="SHELL_PLUS"):
             SETTINGS_SHELL_PLUS = getattr(settings, 'SHELL_PLUS', None)
 
             def get_runner_by_flag(flag):
diff --git a/django_extensions/management/commands/show_template_tags.py b/django_extensions/management/commands/show_template_tags.py
index 79561890..399dd262 100644
--- a/django_extensions/management/commands/show_template_tags.py
+++ b/django_extensions/management/commands/show_template_tags.py
@@ -8,7 +8,7 @@ from django.apps import apps
 from django.core.management import color
 from django.core.management import BaseCommand
 from django.utils import termcolors
-from django.utils.encoding import smart_text
+from django.utils.encoding import smart_str
 
 from django_extensions.compat import load_tag_library
 from django_extensions.management.color import _dummy_style_func
@@ -41,7 +41,7 @@ def format_block(block, nlspaces=0):
     http://code.activestate.com/recipes/145672/
     """
     # separate block into lines
-    lines = smart_text(block).split('\n')
+    lines = smart_str(block).split('\n')
 
     # remove leading/trailing empty lines
     while lines and not lines[0]:
diff --git a/django_extensions/management/commands/sqldsn.py b/django_extensions/management/commands/sqldsn.py
index dab37723..32d732fb 100644
--- a/django_extensions/management/commands/sqldsn.py
+++ b/django_extensions/management/commands/sqldsn.py
@@ -16,6 +16,72 @@ from django_extensions.settings import SQLITE_ENGINES, POSTGRESQL_ENGINES, MYSQL
 from django_extensions.utils.deprecation import RemovedInNextVersionWarning
 
 
+def _sqlite_name(dbhost, dbport, dbname, dbuser, dbpass):
+    return dbname
+
+
+def _mysql_keyvalue(dbhost, dbport, dbname, dbuser, dbpass):
+    dsnstr = f'host="{dbhost}", db="{dbname}", user="{dbuser}", passwd="{dbpass}"'
+    if dbport is not None:
+        dsnstr += f', port="{dbport}"'
+    return dsnstr
+
+
+def _mysql_args(dbhost, dbport, dbname, dbuser, dbpass):
+    dsnstr = f'-h "{dbhost}" -D "{dbname}" -u "{dbuser}" -p "{dbpass}"'
+    if dbport is not None:
+        dsnstr += f' -P {dbport}'
+    return dsnstr
+
+
+def _postgresql_keyvalue(dbhost, dbport, dbname, dbuser, dbpass):
+    dsnstr = f"host='{dbhost}' dbname='{dbname}' user='{dbuser}' password='{dbpass}'"
+    if dbport is not None:
+        dsnstr += f" port='{dbport}'"
+    return dsnstr
+
+
+def _postgresql_kwargs(dbhost, dbport, dbname, dbuser, dbpass):
+    dsnstr = f"host={dbhost!r}, database={dbname!r}, user={dbuser!r}, password={dbpass!r}"
+    if dbport is not None:
+        dsnstr += f", port={dbport!r}"
+    return dsnstr
+
+
+def _postgresql_pgpass(dbhost, dbport, dbname, dbuser, dbpass):
+    return ':'.join(str(s) for s in [dbhost, dbport, dbname, dbuser, dbpass])
+
+
+def _uri(engine):
+    def inner(dbhost, dbport, dbname, dbuser, dbpass):
+        host = dbhost or ''
+        if dbport is not None and dbport != '':
+            host += f':{dbport}'
+        if dbuser is not None and dbuser != '':
+            user = dbuser
+            if dbpass is not None and dbpass != '':
+                user += f':{dbpass}'
+            host = f'{user}@{host}'
+        return f'{engine}://{host}/{dbname}'
+    return inner
+
+
+_FORMATTERS = [
+    (SQLITE_ENGINES, None, _sqlite_name),
+    (SQLITE_ENGINES, 'filename', _sqlite_name),
+    (SQLITE_ENGINES, 'uri', _uri('sqlite')),
+    (MYSQL_ENGINES, None, _mysql_keyvalue),
+    (MYSQL_ENGINES, 'keyvalue', _mysql_keyvalue),
+    (MYSQL_ENGINES, 'args', _mysql_args),
+    (MYSQL_ENGINES, 'uri', _uri('mysql')),
+    (POSTGRESQL_ENGINES, None, _postgresql_keyvalue),
+    (POSTGRESQL_ENGINES, 'keyvalue', _postgresql_keyvalue),
+    (POSTGRESQL_ENGINES, 'kwargs', _postgresql_kwargs),
+    (POSTGRESQL_ENGINES, 'uri', _uri('postgresql')),
+    (POSTGRESQL_ENGINES, 'pgpass', _postgresql_pgpass),
+]
+
+
 class Command(BaseCommand):
     help = "Prints DSN on stdout, as specified in settings.py"
     requires_system_checks = False
@@ -23,21 +89,23 @@ class Command(BaseCommand):
 
     def add_arguments(self, parser):
         super().add_arguments(parser)
-        parser.add_argument(
+        dbspec = parser.add_mutually_exclusive_group()
+        dbspec.add_argument(
             '-R', '--router', action='store',
             dest='router', default=DEFAULT_DB_ALIAS,
             help='Use this router-database other then default (deprecated: use --database instead)'
         )
-        parser.add_argument(
+        dbspec.add_argument(
             '--database', default=DEFAULT_DB_ALIAS,
             help='Nominates a database to run command for. Defaults to the "%s" database.' % DEFAULT_DB_ALIAS,
         )
+        styles = sorted(set([style for _, style, _ in _FORMATTERS if style is not None]))
         parser.add_argument(
             '-s', '--style', action='store',
-            dest='style', default=None,
-            help='DSN format style: keyvalue, uri, pgpass, all'
+            dest='style', default=None, choices=styles + ['all'],
+            help='DSN format style.'
         )
-        parser.add_argument(
+        dbspec.add_argument(
             '-a', '--all', action='store_true',
             dest='all', default=False,
             help='Show DSN for all database routes'
@@ -79,78 +147,27 @@ class Command(BaseCommand):
         dbname = dbinfo.get('NAME')
         dbhost = dbinfo.get('HOST')
         dbport = dbinfo.get('PORT')
-
-        dsn = []
-
-        if engine in SQLITE_ENGINES:
-            dsn.append('{}'.format(dbname))
-        elif engine in MYSQL_ENGINES:
-            dsn.append(self._mysql(dbhost, dbport, dbname, dbuser, dbpass))
-        elif engine in POSTGRESQL_ENGINES:
-            dsn.extend(self._postgresql(
-                dbhost, dbport, dbname, dbuser, dbpass, dsn_style=dsn_style))
-        else:
-            dsn.append(self.style.ERROR('Unknown database, can''t generate DSN'))
+        if dbport == '':
+            dbport = None
+
+        dsn = [
+            formatter(dbhost, dbport, dbname, dbuser, dbpass)
+            for engines, style, formatter in _FORMATTERS
+            if engine in engines and (
+                dsn_style == style or dsn_style == 'all' and style is not None)
+        ]
+
+        if not dsn:
+            available = ', '.join(
+                style for engines, style, _ in _FORMATTERS
+                if engine in engines and style is not None)
+            dsn = [self.style.ERROR(
+                f"Invalid style {dsn_style} for {engine} (available: {available})"
+                if available else "Unknown database, can't generate DSN"
+            )]
 
         if not quiet:
-            sys.stdout.write(self.style.SQL_TABLE("DSN for database '%s' with engine '%s':\n" % (database, engine)))
+            sys.stdout.write(self.style.SQL_TABLE(f'DSN for database {database!r} with engine {engine!r}:\n'))
 
         for output in dsn:
-            sys.stdout.write("{}\n".format(output))
-
-    def _mysql(self, dbhost, dbport, dbname, dbuser, dbpass):
-        dsnstr = 'host="{0}", db="{2}", user="{3}", passwd="{4}"'
-
-        if dbport is not None:
-            dsnstr += ', port="{1}"'
-
-        return dsnstr.format(dbhost, dbport, dbname, dbuser, dbpass)
-
-    def _postgresql(self, dbhost, dbport, dbname, dbuser, dbpass, dsn_style=None):  # noqa
-        """PostgreSQL psycopg2 driver accepts two syntaxes
-
-        Plus a string for .pgpass file
-        """
-        dsn = []
-
-        if dsn_style is None or dsn_style == 'all' or dsn_style == 'keyvalue':
-            dsnstr = "host='{0}' dbname='{2}' user='{3}' password='{4}'"
-
-            if dbport is not None:
-                dsnstr += " port='{1}'"
-
-            dsn.append(dsnstr.format(
-                dbhost,
-                dbport,
-                dbname,
-                dbuser,
-                dbpass,
-            ))
-
-        if dsn_style in ('all', 'kwargs'):
-            dsnstr = "host='{0}', database='{2}', user='{3}', password='{4}'"
-            if dbport is not None:
-                dsnstr += ", port='{1}'"
-
-            dsn.append(dsnstr.format(
-                dbhost,
-                dbport,
-                dbname,
-                dbuser,
-                dbpass,
-            ))
-
-        if dsn_style in ('all', 'uri'):
-            dsnstr = "postgresql://{user}:{password}@{host}/{name}"
-
-            dsn.append(dsnstr.format(
-                host="{host}:{port}".format(host=dbhost, port=dbport) if dbport else dbhost,  # noqa
-                name=dbname,
-                user=dbuser,
-                password=dbpass,
-            ))
-
-        if dsn_style in ('all', 'pgpass'):
-            dsn.append(':'.join(map(str, filter(None, [dbhost, dbport, dbname, dbuser, dbpass]))))
-
-        return dsn
+            sys.stdout.write(f'{output}\n')
diff --git a/django_extensions/management/debug_cursor.py b/django_extensions/management/debug_cursor.py
index 0ca584b9..ad68cfd6 100644
--- a/django_extensions/management/debug_cursor.py
+++ b/django_extensions/management/debug_cursor.py
@@ -8,13 +8,16 @@ from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.db.backends import utils
 
+from django_extensions.settings import DEFAULT_PRINT_SQL_TRUNCATE_CHARS
+
 
 @contextmanager
 def monkey_patch_cursordebugwrapper(print_sql=None, print_sql_location=False, truncate=None, logger=print, confprefix="DJANGO_EXTENSIONS"):
     if not print_sql:
         yield
     else:
-        truncate = getattr(settings, '%s_PRINT_SQL_TRUNCATE' % confprefix, 1000)
+        if truncate is None:
+            truncate = getattr(settings, '%s_PRINT_SQL_TRUNCATE' % confprefix, DEFAULT_PRINT_SQL_TRUNCATE_CHARS)
 
         # Code orginally from http://gist.github.com/118990
         sqlparse = None
diff --git a/django_extensions/management/jobs.py b/django_extensions/management/jobs.py
index 1c6cdb48..e13a4f2e 100644
--- a/django_extensions/management/jobs.py
+++ b/django_extensions/management/jobs.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 import os
 import sys
-from imp import find_module
+import importlib
 from typing import Optional  # NOQA
 from django.apps import apps
 
@@ -67,22 +67,20 @@ def my_import(name):
 
 def find_jobs(jobs_dir):
     try:
-        return [f[:-3] for f in os.listdir(jobs_dir) if not f.startswith('_') and f.endswith(".py")]
+        return sorted([f[:-3] for f in os.listdir(jobs_dir) if not f.startswith('_') and f.endswith(".py")])
     except OSError:
         return []
 
 
-def find_job_module(app_name, when=None):
+def find_job_module(app_name: str, when: Optional[str] = None) -> str:
+    """Find the directory path to a job module."""
     parts = app_name.split('.')
     parts.append('jobs')
     if when:
         parts.append(when)
-    parts.reverse()
-    path = None
-    while parts:
-        part = parts.pop()
-        f, path, descr = find_module(part, path and [path] or None)
-    return path
+    module_name = ".".join(parts)
+    module = importlib.import_module(module_name)
+    return module.__path__[0]
 
 
 def import_job(app_name, name, when=None):
diff --git a/django_extensions/management/modelviz.py b/django_extensions/management/modelviz.py
index 15552d07..81804a3a 100644
--- a/django_extensions/management/modelviz.py
+++ b/django_extensions/management/modelviz.py
@@ -13,9 +13,13 @@ import os
 import re
 
 from django.apps import apps
+from django.db.models import deletion
 from django.db.models.fields.related import (
     ForeignKey, ManyToManyField, OneToOneField, RelatedField,
 )
+from django.db.models.fields.reverse_related import (
+    OneToOneRel, ManyToOneRel,
+)
 from django.contrib.contenttypes.fields import GenericRelation
 from django.template import Context, Template, loader
 from django.utils.encoding import force_str
@@ -41,9 +45,21 @@ __contributors__ = [
     "Mikkel Munch Mortensen <https://www.detfalskested.dk/>",
     "Andrzej Bistram <andrzej.bistram@gmail.com>",
     "Daniel Lipsitt <danlipsitt@gmail.com>",
+    "Tobias Mitterdorfer <tobias.mitterdorfer97@gmail.com>"
 ]
 
 
+ON_DELETE_COLORS = {
+    deletion.CASCADE: 'red',
+    deletion.PROTECT: 'blue',
+    deletion.SET_NULL: 'orange',
+    deletion.SET_DEFAULT: 'green',
+    deletion.SET: 'yellow',
+    deletion.DO_NOTHING: 'grey',
+    deletion.RESTRICT: 'purple',
+}
+
+
 def parse_file_or_list(arg):
     if not arg:
         return []
@@ -68,6 +84,7 @@ class ModelGraph:
         self.verbose_names = kwargs.get('verbose_names', False)
         self.inheritance = kwargs.get('inheritance', True)
         self.relations_as_fields = kwargs.get("relations_as_fields", True)
+        self.relation_fields_only = kwargs.get("relation_fields_only", False)
         self.sort_fields = kwargs.get("sort_fields", True)
         self.language = kwargs.get('language', None)
         if self.language is not None:
@@ -80,6 +97,7 @@ class ModelGraph:
         )
         self.hide_edge_labels = kwargs.get('hide_edge_labels', False)
         self.arrow_shape = kwargs.get("arrow_shape")
+        self.color_code_deletions = kwargs.get("color_code_deletions", False)
         if self.all_applications:
             self.app_labels = [app.label for app in apps.get_app_configs()]
         else:
@@ -152,7 +170,7 @@ class ModelGraph:
             'primary_key': field.primary_key,
         }
 
-    def add_relation(self, field, model, extras=""):
+    def add_relation(self, field, model, extras="", color=None):
         if self.verbose_names and field.verbose_name:
             label = force_str(field.verbose_name)
             if label.islower():
@@ -183,6 +201,9 @@ class ModelGraph:
         else:
             target_model = field.remote_field.model
 
+        if color:
+            extras = '[{}, color={}]'.format(extras[1:-1], color)
+
         _rel = self.get_relation_context(target_model, field, label, extras)
 
         if _rel not in model['relations'] and self.use_model(_rel['target']):
@@ -337,9 +358,15 @@ class ModelGraph:
             # excluding field redundant with inheritance relation
             # excluding fields inherited from abstract classes. they too show as local_fields
             return newmodel
+
+        color = None
+        if self.color_code_deletions and isinstance(field, (OneToOneField, ForeignKey)):
+            field_on_delete = getattr(field.remote_field, 'on_delete', None)
+            color = ON_DELETE_COLORS.get(field_on_delete)
+
         if isinstance(field, OneToOneField):
             relation = self.add_relation(
-                field, newmodel, '[arrowhead=none, arrowtail=none, dir=both]'
+                field, newmodel, '[arrowhead=none, arrowtail=none, dir=both]', color
             )
         elif isinstance(field, ForeignKey):
             relation = self.add_relation(
@@ -348,6 +375,7 @@ class ModelGraph:
                 '[arrowhead=none, arrowtail={}, dir=both]'.format(
                     self.arrow_shape
                 ),
+                color
             )
         else:
             relation = None
@@ -416,6 +444,12 @@ class ModelGraph:
                     return True
             if field.name in self.exclude_columns:
                 return True
+        if self.relation_fields_only:
+            if not isinstance(
+                field,
+                (ForeignKey, ManyToManyField, OneToOneField, RelatedField, OneToOneRel, ManyToOneRel)
+            ):
+                return True
         return False
 
 
diff --git a/django_extensions/settings.py b/django_extensions/settings.py
index ec1b0cca..4b71b677 100644
--- a/django_extensions/settings.py
+++ b/django_extensions/settings.py
@@ -28,3 +28,5 @@ DEFAULT_POSTGRESQL_ENGINES = (
 SQLITE_ENGINES = getattr(settings, 'DJANGO_EXTENSIONS_RESET_DB_SQLITE_ENGINES', DEFAULT_SQLITE_ENGINES)
 MYSQL_ENGINES = getattr(settings, 'DJANGO_EXTENSIONS_RESET_DB_MYSQL_ENGINES', DEFAULT_MYSQL_ENGINES)
 POSTGRESQL_ENGINES = getattr(settings, 'DJANGO_EXTENSIONS_RESET_DB_POSTGRESQL_ENGINES', DEFAULT_POSTGRESQL_ENGINES)
+
+DEFAULT_PRINT_SQL_TRUNCATE_CHARS = 1000
diff --git a/django_extensions/validators.py b/django_extensions/validators.py
index 5bddff67..41c1dcc1 100644
--- a/django_extensions/validators.py
+++ b/django_extensions/validators.py
@@ -83,6 +83,8 @@ class HexValidator:
         self.max_length = max_length
         if message:
             self.message = message
+        else:
+            self.message = self.messages['invalid']
         if code:
             self.code = code
 
@@ -92,7 +94,7 @@ class HexValidator:
             raise ValidationError(self.messages['length'], code='hex_only_length', params={'length': self.length})
         if self.min_length and len(value) < self.min_length:
             raise ValidationError(self.messages['min_length'], code='hex_only_min_length', params={'min': self.min_length})
-        if self.max_length and len(value) < self.max_length:
+        if self.max_length and len(value) > self.max_length:
             raise ValidationError(self.messages['max_length'], code='hex_only_max_length', params={'max': self.max_length})
 
         try:
diff --git a/docs/command_extensions.rst b/docs/command_extensions.rst
index 070f5ce6..ea8299de 100644
--- a/docs/command_extensions.rst
+++ b/docs/command_extensions.rst
@@ -107,6 +107,8 @@ Command Extensions
   test via some automated system (BuildBot, Jenkins, etc) and making sure that
   the test database is always dropped at the end.
 
+* *raise_test_exception* - Raises a test exception via command. Useful for debugging error reporters such as Sentry.
+
 * :doc:`reset_db` - Resets a database (currently sqlite3, mysql, postgres). Uses "DROP DATABASE" and "CREATE DATABASE".
 
 * *runjob* - Run a single maintenance job.  Part of the jobs system.
diff --git a/docs/conf.py b/docs/conf.py
index fdc049be..5a3b2b6d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -47,7 +47,7 @@ copyright = 'Copyright (C) 2008-2015 Michael Trier, Bas van Oostveen and contrib
 # The short X.Y version.
 version = '3.1'
 # The full version, including alpha/beta/rc tags.
-release = '3.1.3'
+release = '3.1.5'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/docs/graph_models.rst b/docs/graph_models.rst
index b1443f5b..3315d2e3 100644
--- a/docs/graph_models.rst
+++ b/docs/graph_models.rst
@@ -133,6 +133,11 @@ image by using the *graph_models* command::
   # Create a graph with 'normal' arrow shape for relations
   $ ./manage.py graph_models -a --arrow-shape normal -o my_project_sans_foo_bar.png
 
+::
+
+  # Create a graph with colored edges for relations with on_delete settings
+  $ ./manage.py graph_models -a --color-code-deletions -o my_project_colored.png
+
 ::
 
   # Create a graph with different layout direction,
diff --git a/docs/managestate.rst b/docs/managestate.rst
new file mode 100644
index 00000000..7ef2bee3
--- /dev/null
+++ b/docs/managestate.rst
@@ -0,0 +1,70 @@
+managestate
+==========
+
+:synopsis: Saves current applied migrations to a file or applies migrations from this file.
+
+The `managestate` command fetches last applied migrations from a specified database
+and saves them to a specified file. After that, you may easily apply saved migrations.
+The advantage of this approach is that you may have at hand several database states
+and quickly switch between them.
+
+Why?
+----
+
+While you develop several features or fix some bugs at the same time you often meet
+the situation when you need to apply or unapply database migrations before you check out
+to another feature/bug branch. You always need to view current migrations by `showmigrations`,
+then apply or unapply it manually using `migrate` and there is no problem if you work with
+one Django app. But when there is more than one, it starts to annoy. To forget about the problem
+and quickly switch between branches use the `managestate` command.
+
+How?
+----
+
+To dump current migrations use::
+
+    $ ./manage.py managestate dump
+
+A state will be saved to `managestate.json` just about the following::
+
+    {
+      "default": {
+        "admin": "0003_logentry_add_action_flag_choices",
+        "auth": "0012_alter_user_first_name_max_length",
+        "contenttypes": "0002_remove_content_type_name",
+        "sessions": "0001_initial",
+        "sites": "0002_alter_domain_unique",
+        "myapp": "zero"
+      },
+      "updated_at": "2021-06-27 10:42:50.364070"
+    }
+
+As you see, migrations have been saved as the state called "default".
+You can specify it using the positional argument::
+
+    $ ./manage.py managestate dump my_feature_branch
+
+Then migrations will be added to `managestate.json` under the key "my_feature_branch".
+To change the filename use `-f` or `--filename` flag.
+
+When you load a state from a file, you may also use all arguments defined for the `migrate` command.
+
+Examples
+----
+
+Save an initial database state of the branch "master/main" before developing features::
+
+    $ ./manage.py managestate dump master
+
+Check out to your branch, develop your feature, and dump its state when you are going to get reviewed::
+
+    $ ./manage.py managestate dump super-feature
+
+Check out to the "master" branch back and rollback a database state with just one command::
+
+    $ ./manage.py managestate load master
+
+If you need to add some improvements to your feature, just use::
+
+    $ ./manage.py managestate load super-feature
+
diff --git a/docs/shell_plus.rst b/docs/shell_plus.rst
index e2545157..d11d43e5 100644
--- a/docs/shell_plus.rst
+++ b/docs/shell_plus.rst
@@ -28,7 +28,7 @@ Python::
 
   $ ./manage.py shell_plus --plain
 
-It is possible to directly add command line arguments to the underlying Python shell using `--`::
+It is possible to directly add command line arguments to the underlying Python shell using ``--``::
 
   $ ./manage.py shell_plus --ipython -- --profile=foo
 
@@ -436,3 +436,16 @@ You can also set the configuration option SHELL_PLUS_PRINT_SQL to omit the above
 
   # print SQL queries in shell_plus
   SHELL_PLUS_PRINT_SQL = True
+
+Printing SQL queries also comes with the possibility of specifying the maximum amount of characters to display:
+
+  $ ./manage.py shell_plus --print-sql --truncate-sql
+
+`--truncate-sql` accepts an int value starting from 0 (which disables truncation). Defaults to 1000.
+
+You can also set the configuration option SHELL_PLUS_PRINT_SQL_TRUNCATE to omit the above command line option.
+
+::
+
+  # print SQL queries in shell_plus
+  SHELL_PLUS_PRINT_SQL_TRUNCATE = None
diff --git a/docs/sqldsn.rst b/docs/sqldsn.rst
index 9233a1ac..b02fa0fc 100644
--- a/docs/sqldsn.rst
+++ b/docs/sqldsn.rst
@@ -16,10 +16,33 @@ Currently the following databases are supported:
 
 Patches to support other databases are welcome! :-)
 
+
+Supported Styles
+----------------
+
+Currently the following databases are supported:
+
++----------+------------+-------+---------+---------------------------+
+| Style    | PostgreSQL | MySQL | Sqlite3 | Description               |
++==========+============+=======+=========+===========================+
+| args     |            |   Y   |         | command-line arguments    |
++----------+------------+-------+---------+---------------------------+
+| filename |            |       |    Y    | filename                  |
++----------+------------+-------+---------+---------------------------+
+| keyvalue |      Y     |   Y   |         | key-value pairs (legacy)  |
++----------+------------+-------+---------+---------------------------+
+| kwargs   |      Y     |       |         | Python keyword arguments  |
++----------+------------+-------+---------+---------------------------+
+| pgpass   |      Y     |       |         | ``.pgpass`` format        |
++----------+------------+-------+---------+---------------------------+
+| uri      |      Y     |   Y   |    Y    | (See ``dj-database-url``) |
++----------+------------+-------+---------+---------------------------+
+
+
 Exit Codes
 ----------
 
-Exit status is 0
+Exit status is 0 unless invalid options were given.
 
 
 Example Usage
@@ -45,6 +68,11 @@ Example Usage
   # Print all DSN styles available for the default database
   $ ./manage.py sqldsn --style=all
 
+::
+
+  # Print the URI for the default database
+  $ ./manage.py sqldsn -q --style=uri
+
 ::
 
   # Create .pgpass file for default database by using the quiet option
diff --git a/setup.py b/setup.py
index f15a33ad..60a2d2e3 100644
--- a/setup.py
+++ b/setup.py
@@ -127,8 +127,15 @@ setup(
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: Implementation :: CPython',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Topic :: Utilities',
     ],
+    project_urls={
+        "Documentation": "https://django-extensions.readthedocs.io/",
+        "Changelog": "https://github.com/django-extensions/django-extensions/blob/main/CHANGELOG.md",
+        "Source": "https://github.com/django-extensions/django-extensions",
+        "Tracker": "https://github.com/django-extensions/django-extensions/issues",
+    },
 )
diff --git a/tox.ini b/tox.ini
index 71052dbc..57ede169 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,19 +8,19 @@ envlist =
     precommit
     safety
     mypy
-    {py37,py38,py39}-flake8
-    {py36,py37,py38,py39,pypy}-dj22
-    {py36,py37,py38,py39,pypy}-dj30
-    {py36,py37,py38,py39,pypy}-dj31
-    {py36,py37,py38,py39,pypy}-dj32
-    {py38,py39,pypy}-djmaster
-    py39-dj32-postgres
-    py39-dj32-mysql
-    py39-djmaster-postgres
+    {py37,py38,py39,py310}-flake8
+    {py36,py37,py38,py39,py310,pypy}-dj32
+    {py38,py39,py310,pypy}-dj40
+    {py38,py39,py310,pypy}-djmaster
+    py310-dj32-postgres
+    py310-dj40-postgres
+    py310-dj32-mysql
+    py310-dj40-mysql
+    py310-djmaster-postgres
 
 [testenv]
 commands = make test
-whitelist_externals = make
+allowlist_externals = make
 passenv =
     DJANGO_EXTENSIONS_DATABASE_ENGINE
     DJANGO_EXTENSIONS_DATABASE_NAME
@@ -38,10 +38,8 @@ setenv =
 deps =
     pip >= 21.1
     -rrequirements-dev.txt
-    dj22: Django==2.2
-    dj30: Django>=3.0,<3.1
-    dj31: Django>=3.1,<3.2
     dj32: Django>=3.2,<4.0
+    dj40: Django>=4.0,<4.1
     djmaster: https://github.com/django/django/archive/refs/heads/main.zip
     postgres: psycopg2-binary
     mysql: mysqlclient
@@ -59,14 +57,20 @@ deps =
 commands = safety check --full-report
 
 [testenv:mypy]
+allowlist_externals=
+    echo
 deps =
     pip >= 21.1
     mypy
+    types-requests
+    types-boto
+    types-PyYAML
+    types-Werkzeug
     -rrequirements-dev.txt
 commands = mypy --ignore-missing-imports django_extensions
 
 [testenv:compile-catalog]
-whitelist_externals =
+allowlist_externals =
     make
 deps =
     pip >= 21.1
@@ -91,3 +95,9 @@ deps =
     pip >= 21.1
     flake8
 commands = flake8 django_extensions tests
+
+[testenv:py310-flake8]
+deps =
+    pip >= 21.1
+    flake8
+commands = flake8 django_extensions tests


Test patch:
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index ff338210..25fe24d2 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -18,14 +18,20 @@ jobs:
           - 3.7
           - 3.8
           - 3.9
+          - "3.10"
           - pypy3
         tox-django-version:
-          - "22"
-          - "30"
-          - "31"
           - "32"
+          - "40"
           # GH Actions don't support something like allow-failure ?
           # - "master"
+        exclude:
+          - python-version: "3.6"
+            tox-django-version: "40"
+          - python-version: "3.7"
+            tox-django-version: "40"
+          - python-version: "pypy3"
+            tox-django-version: "40"
     steps:
       - name: Checkout
         uses: actions/checkout@v2
@@ -58,7 +64,7 @@ jobs:
       max-parallel: 4
       matrix:
         python-version:
-          - 3.9
+          - "3.10"
         tox-django-version:
           - "32"
     steps:
diff --git a/django_extensions/management/commands/drop_test_database.py b/django_extensions/management/commands/drop_test_database.py
index be2089dc..5021ab5b 100644
--- a/django_extensions/management/commands/drop_test_database.py
+++ b/django_extensions/management/commands/drop_test_database.py
@@ -21,7 +21,7 @@ class Command(BaseCommand):
     def add_arguments(self, parser):
         super().add_arguments(parser)
         parser.add_argument(
-            '--noinput', action='store_false', dest='interactive',
+            '--noinput', '--no-input', action='store_false', dest='interactive',
             default=True, help='Tells Django to NOT prompt the user for input of any kind.'
         )
         parser.add_argument(
diff --git a/django_extensions/management/commands/raise_test_exception.py b/django_extensions/management/commands/raise_test_exception.py
new file mode 100644
index 00000000..9cc3b759
--- /dev/null
+++ b/django_extensions/management/commands/raise_test_exception.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+from django.core.management.base import BaseCommand
+
+from django_extensions.management.utils import signalcommand
+
+
+class DjangoExtensionsTestException(Exception):
+    pass
+
+
+class Command(BaseCommand):
+    help = (
+        "Raises a test Exception named DjangoExtensionsTestException. "
+        "Useful for debugging integration with error reporters like Sentry."
+    )
+
+    @signalcommand
+    def handle(self, *args, **options):
+        message = (
+            "This is a test exception via the "
+            "django-extensions raise_test_exception management command."
+        )
+        raise DjangoExtensionsTestException(message)
diff --git a/tests/management/commands/shell_plus_tests/test_shell_plus.py b/tests/management/commands/shell_plus_tests/test_shell_plus.py
index be59e900..bd90ccd2 100644
--- a/tests/management/commands/shell_plus_tests/test_shell_plus.py
+++ b/tests/management/commands/shell_plus_tests/test_shell_plus.py
@@ -41,6 +41,39 @@ def test_shell_plus_print_sql(capsys):
     assert re.search(r"SELECT\s+.+\s+FROM\s+.auth_user.\s+LIMIT\s+1", out)
 
 
+@pytest.mark.django_db()
+@override_settings(SHELL_PLUS_SQLPARSE_ENABLED=False, SHELL_PLUS_PYGMENTS_ENABLED=False)
+def test_shell_plus_print_sql_truncate(capsys):
+    try:
+        from django.db import connection
+        from django.db.backends import utils
+        CursorDebugWrapper = utils.CursorDebugWrapper
+        force_debug_cursor = True if connection.force_debug_cursor else False
+        call_command("shell_plus", "--plain", "--print-sql", "--truncate-sql=0", "--command=User.objects.all().exists()")
+    finally:
+        utils.CursorDebugWrapper = CursorDebugWrapper
+        connection.force_debug_cursor = force_debug_cursor
+
+    out, err = capsys.readouterr()
+
+    assert re.search(r"SELECT\s+.+\s+FROM\s+.auth_user.\s+LIMIT\s+1", out)
+
+    try:
+        from django.db import connection
+        from django.db.backends import utils
+        CursorDebugWrapper = utils.CursorDebugWrapper
+        force_debug_cursor = True if connection.force_debug_cursor else False
+        call_command("shell_plus", "--plain", "--print-sql", "--truncate-sql=4", "--command=User.objects.all().exists()")
+    finally:
+        utils.CursorDebugWrapper = CursorDebugWrapper
+        connection.force_debug_cursor = force_debug_cursor
+
+    out, err = capsys.readouterr()
+
+    assert re.search(r"SELE", out)
+    assert not re.search(r"SELEC", out)
+
+
 def test_shell_plus_plain_startup():
     command = shell_plus.Command()
     command.tests_mode = True
diff --git a/tests/management/commands/test_graph_models.py b/tests/management/commands/test_graph_models.py
index f502c834..206a70b2 100644
--- a/tests/management/commands/test_graph_models.py
+++ b/tests/management/commands/test_graph_models.py
@@ -106,6 +106,26 @@ class GraphModelsOutputTests(TestCase):
         with self.assertRaises(CommandError):
             call_command('graph_models', all_applications=True, pygraphviz=True)
 
+    def test_graph_models_relation_fields_only(self):
+        # use of --relation-fields-only ignores all non-relation-fields
+        stdout = StringIO()
+        call_command('graph_models', all_applications=True, stdout=stdout, json=True)
+        with_no_flag = json.loads(stdout.getvalue())
+
+        stdout = StringIO()
+        call_command('graph_models', all_applications=True, relation_fields_only=True, stdout=stdout, json=True)
+        with_flag = json.loads(stdout.getvalue())
+
+        # delete all entries for fields that do not display relations
+        for graph_idx, graph in reversed(list(enumerate(with_no_flag["graphs"]))):
+            for model_idx, model in reversed(list(enumerate(graph["models"]))):
+                for field_idx, field in reversed(list(enumerate(model["fields"]))):
+                    if field["type"] not in ["ForeignKey (id)", "AutoField", "OneToOneField (id)"] and not field["primary_key"]:
+                        del with_no_flag["graphs"][graph_idx]["models"][model_idx]["fields"][field_idx]
+
+        # assert that manually deleting all non-relation-fields is same as using the flag
+        self.assertEqual(with_no_flag, with_flag)
+
 
 def test_disable_abstract_fields_not_active():
     out = StringIO()
diff --git a/tests/management/commands/test_managestate.py b/tests/management/commands/test_managestate.py
new file mode 100644
index 00000000..0ab40231
--- /dev/null
+++ b/tests/management/commands/test_managestate.py
@@ -0,0 +1,107 @@
+import json
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+import pytest
+from django.core.management import CommandError, call_command
+from django.db import connection
+from django.db.migrations.recorder import MigrationRecorder
+
+from django_extensions.management.commands.managestate import DEFAULT_FILENAME, DEFAULT_STATE, Command
+
+pytestmark = [pytest.mark.django_db]
+
+COMMAND = 'managestate'
+DEFAULT_FILE = Path(DEFAULT_FILENAME)
+
+
+@pytest.fixture
+def make_dump(request):
+    request.addfinalizer(DEFAULT_FILE.unlink)
+    return call_command(COMMAND, 'dump', '-v', 0)
+
+
+@pytest.fixture
+def cmd_data():
+    cmd = Command()
+    cmd.verbosity = 0
+    cmd.conn = connection
+    data = cmd.get_migrated_apps()
+    data.update(cmd.get_applied_migrations())
+    return data
+
+
+class TestManageStateExceptions:
+    """Tests for managestate management command exceptions."""
+
+    def test_bad_action(self):
+        with pytest.raises(CommandError):
+            call_command(COMMAND, 'bad_action')
+
+    def test_no_such_file(self):
+        with pytest.raises(CommandError):
+            call_command(COMMAND, 'load', '-f', 'non_existent_file')
+
+    def test_not_a_file(self):
+        with pytest.raises(CommandError), TemporaryDirectory() as tmp_dir:
+            call_command(COMMAND, 'load', '-f', tmp_dir)
+
+    @pytest.mark.usefixtures('make_dump')
+    def test_no_such_state(self):
+        with pytest.raises(CommandError):
+            call_command(COMMAND, 'load', 'non_existent_state')
+
+
+class TestManageState:
+    """Tests managestate management command"""
+
+    def test_dump(self, request, capsys):
+        request.addfinalizer(DEFAULT_FILE.unlink)
+        call_command(COMMAND, 'dump', '-v', 0)
+        stdout, _ = capsys.readouterr()
+        assert DEFAULT_FILE.exists()
+        assert 'successfully saved' in stdout
+
+    @pytest.mark.parametrize('filename', [DEFAULT_FILENAME, 'custom_f1l3n4m3.json'])
+    def test_dump_files(self, request, filename):
+        path = Path(filename)
+        request.addfinalizer(path.unlink)
+        call_command(COMMAND, 'dump', '-f', filename)
+        assert path.exists()
+
+    @pytest.mark.parametrize('state', [DEFAULT_STATE, 'custom_state'])
+    def test_dump_states(self, request, state):
+        request.addfinalizer(DEFAULT_FILE.unlink)
+        call_command(COMMAND, 'dump', state)
+        with open(DEFAULT_FILE) as file:
+            data = json.load(file)
+        assert isinstance(data, dict)
+        assert data.get(state) is not None
+
+    @pytest.mark.usefixtures('make_dump')
+    def test_load(self, capsys):
+        call_command(COMMAND, 'load', '-v', 0)
+        stdout, _ = capsys.readouterr()
+        assert 'successfully applied' in stdout
+
+    @pytest.mark.parametrize('filename', [DEFAULT_FILENAME, 'custom_f1l3n4m3.json'])
+    def test_load_files(self, request, capsys, filename):
+        request.addfinalizer(Path(filename).unlink)
+        call_command(COMMAND, 'dump', '-f', filename)
+        call_command(COMMAND, 'load', '-f', filename)
+        stdout, _ = capsys.readouterr()
+        assert 'successfully applied' in stdout
+
+    @pytest.mark.parametrize('state', [DEFAULT_STATE, 'custom_state'])
+    def test_load_states(self, request, capsys, state):
+        request.addfinalizer(DEFAULT_FILE.unlink)
+        call_command(COMMAND, 'dump', state)
+        call_command(COMMAND, 'load', state)
+        stdout, _ = capsys.readouterr()
+        assert 'successfully applied' in stdout
+
+    def test_migration_is_last_applied(self, cmd_data):
+        migrations = MigrationRecorder(connection).applied_migrations()
+        for app, migration in cmd_data.items():
+            last_migration = sorted(filter(lambda x: x[0] == app, migrations))[-1][1]
+            assert migration == last_migration
diff --git a/tests/management/commands/test_raise_test_exception.py b/tests/management/commands/test_raise_test_exception.py
new file mode 100644
index 00000000..3381d39d
--- /dev/null
+++ b/tests/management/commands/test_raise_test_exception.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+import pytest
+
+from django.core.management import call_command
+
+from django_extensions.management.commands.raise_test_exception import DjangoExtensionsTestException
+
+
+def test_raise_test_exception():
+    with pytest.raises(DjangoExtensionsTestException):
+        call_command('raise_test_exception')
diff --git a/tests/management/commands/test_show_urls.py b/tests/management/commands/test_show_urls.py
index 0d647ba8..aed4287b 100644
--- a/tests/management/commands/test_show_urls.py
+++ b/tests/management/commands/test_show_urls.py
@@ -1,12 +1,13 @@
 # -*- coding: utf-8 -*-
 from io import StringIO
 
-from django.conf.urls import url
+from django.urls import path
 from django.core.management import CommandError, call_command
 from django.http import HttpResponse
 from django.test import TestCase
 from django.test.utils import override_settings
 from django.views.generic.base import View
+from django import VERSION
 
 from unittest.mock import Mock, patch
 
@@ -20,9 +21,9 @@ class ClassView(View):
 
 
 urlpatterns = [
-    url(r'lambda/view', lambda request: HttpResponse('OK')),
-    url(r'function/based/', function_based_view, name='function-based-view'),
-    url(r'class/based/', ClassView.as_view(), name='class-based-view'),
+    path('lambda/view', lambda request: HttpResponse('OK')),
+    path('function/based/', function_based_view, name='function-based-view'),
+    path('class/based/', ClassView.as_view(), name='class-based-view'),
 ]
 
 
@@ -57,14 +58,23 @@ class ShowUrlsTests(TestCase):
         lines = m_stdout.getvalue().splitlines()
         self.assertIn('/lambda/view\ttests.management.commands.test_show_urls.<lambda>', lines[0])
         self.assertIn('/function/based/\ttests.management.commands.test_show_urls.function_based_view\tfunction-based-view', lines[1])
-        self.assertIn('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[2])
+
+        if VERSION >= (4, 0):
+            self.assertIn('/class/based/\ttests.management.commands.test_show_urls.view\tclass-based-view', lines[2])
+        else:
+            self.assertIn('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[2])
 
     @patch('sys.stdout', new_callable=StringIO)
     def test_should_show_urls_sorted_alphabetically(self, m_stdout):
         call_command('show_urls', verbosity=3)
 
         lines = m_stdout.getvalue().splitlines()
-        self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[0])
+
+        if VERSION >= (4, 0):
+            self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.view\tclass-based-view', lines[0])
+        else:
+            self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[0])
+
         self.assertEqual('/function/based/\ttests.management.commands.test_show_urls.function_based_view\tfunction-based-view', lines[1])
         self.assertEqual('/lambda/view\ttests.management.commands.test_show_urls.<lambda>', lines[2])
 
@@ -72,29 +82,45 @@ class ShowUrlsTests(TestCase):
     def test_should_show_urls_in_json_format(self, m_stdout):
         call_command('show_urls', '--format=json')
 
-        self.assertJSONEqual(m_stdout.getvalue(), [
+        json = [
             {"url": "/lambda/view", "module": "tests.management.commands.test_show_urls.<lambda>", "name": "", "decorators": ""},
-            {"url": "/function/based/", "module": "tests.management.commands.test_show_urls.function_based_view", "name": "function-based-view", "decorators": ""},
-            {"url": "/class/based/", "module": "tests.management.commands.test_show_urls.ClassView", "name": "class-based-view", "decorators": ""}
-        ])
+            {"url": "/function/based/", "module": "tests.management.commands.test_show_urls.function_based_view", "name": "function-based-view", "decorators": ""}
+        ]
+
+        if VERSION >= (4, 0):
+            json.append({"url": "/class/based/", "module": "tests.management.commands.test_show_urls.view", "name": "class-based-view", "decorators": ""})
+        else:
+            json.append({"url": "/class/based/", "module": "tests.management.commands.test_show_urls.ClassView", "name": "class-based-view", "decorators": ""})
+
+        self.assertJSONEqual(m_stdout.getvalue(), json)
         self.assertEqual(len(m_stdout.getvalue().splitlines()), 1)
 
     @patch('sys.stdout', new_callable=StringIO)
     def test_should_show_urls_in_pretty_json_format(self, m_stdout):
         call_command('show_urls', '--format=pretty-json')
 
-        self.assertJSONEqual(m_stdout.getvalue(), [
+        json = [
             {"url": "/lambda/view", "module": "tests.management.commands.test_show_urls.<lambda>", "name": "", "decorators": ""},
-            {"url": "/function/based/", "module": "tests.management.commands.test_show_urls.function_based_view", "name": "function-based-view", "decorators": ""},
-            {"url": "/class/based/", "module": "tests.management.commands.test_show_urls.ClassView", "name": "class-based-view", "decorators": ""}
-        ])
+            {"url": "/function/based/", "module": "tests.management.commands.test_show_urls.function_based_view", "name": "function-based-view", "decorators": ""}
+        ]
+
+        if VERSION >= (4, 0):
+            json.append({"url": "/class/based/", "module": "tests.management.commands.test_show_urls.view", "name": "class-based-view", "decorators": ""})
+        else:
+            json.append({"url": "/class/based/", "module": "tests.management.commands.test_show_urls.ClassView", "name": "class-based-view", "decorators": ""})
+
+        self.assertJSONEqual(m_stdout.getvalue(), json)
         self.assertEqual(len(m_stdout.getvalue().splitlines()), 20)
 
     @patch('sys.stdout', new_callable=StringIO)
     def test_should_show_urls_in_table_format(self, m_stdout):
         call_command('show_urls', '--format=table')
 
-        self.assertIn('/class/based/    | tests.management.commands.test_show_urls.ClassView           | class-based-view    |', m_stdout.getvalue())
+        if VERSION >= (4, 0):
+            self.assertIn('/class/based/    | tests.management.commands.test_show_urls.view                | class-based-view    |', m_stdout.getvalue())
+        else:
+            self.assertIn('/class/based/    | tests.management.commands.test_show_urls.ClassView           | class-based-view    |', m_stdout.getvalue())
+
         self.assertIn('/function/based/ | tests.management.commands.test_show_urls.function_based_view | function-based-view |', m_stdout.getvalue())
         self.assertIn('/lambda/view     | tests.management.commands.test_show_urls.<lambda>            |                     |', m_stdout.getvalue())
 
@@ -103,7 +129,12 @@ class ShowUrlsTests(TestCase):
         call_command('show_urls', '--format=aligned')
 
         lines = m_stdout.getvalue().splitlines()
-        self.assertEqual('/class/based/      tests.management.commands.test_show_urls.ClassView             class-based-view      ', lines[0])
+
+        if VERSION >= (4, 0):
+            self.assertEqual('/class/based/      tests.management.commands.test_show_urls.view                  class-based-view      ', lines[0])
+        else:
+            self.assertEqual('/class/based/      tests.management.commands.test_show_urls.ClassView             class-based-view      ', lines[0])
+
         self.assertEqual('/function/based/   tests.management.commands.test_show_urls.function_based_view   function-based-view   ', lines[1])
         self.assertEqual('/lambda/view       tests.management.commands.test_show_urls.<lambda>                                    ', lines[2])
 
@@ -112,6 +143,11 @@ class ShowUrlsTests(TestCase):
         call_command('show_urls', '--no-color')
 
         lines = m_stdout.getvalue().splitlines()
-        self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[0])
+
+        if VERSION >= (4, 0):
+            self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.view\tclass-based-view', lines[0])
+        else:
+            self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[0])
+
         self.assertEqual('/function/based/\ttests.management.commands.test_show_urls.function_based_view\tfunction-based-view', lines[1])
         self.assertEqual('/lambda/view\ttests.management.commands.test_show_urls.<lambda>', lines[2])
diff --git a/tests/management/commands/test_sqldsn.py b/tests/management/commands/test_sqldsn.py
index d3f8ee87..884cb5d4 100644
--- a/tests/management/commands/test_sqldsn.py
+++ b/tests/management/commands/test_sqldsn.py
@@ -81,6 +81,16 @@ db.sqlite3
 
         self.assertEqual(expected_result, m_stdout.getvalue())
 
+    @override_settings(DATABASES={'default': SQLITE3_DATABASE_SETTINGS})
+    @patch('sys.stdout', new_callable=StringIO)
+    def test_should_print_uri_for_default_sqlite3_database(self, m_stdout):
+        expected_result = """DSN for database 'default' with engine 'django.db.backends.sqlite3':
+sqlite:///db.sqlite3
+"""
+        call_command('sqldsn', '--style=uri')
+
+        self.assertEqual(expected_result, m_stdout.getvalue())
+
     @override_settings(DATABASES={'default': MYSQL_DATABASE_SETTINGS})
     @patch('sys.stdout', new_callable=StringIO)
     def test_should_print_quiet_info_for_mysql_database(self, m_stdout):
@@ -90,6 +100,24 @@ db.sqlite3
 
         self.assertEqual(expected_result, m_stdout.getvalue())
 
+    @override_settings(DATABASES={'default': MYSQL_DATABASE_SETTINGS})
+    @patch('sys.stdout', new_callable=StringIO)
+    def test_should_print_quiet_uri_for_mysql_database(self, m_stdout):
+        expected_result = """mysql://foo:bar@127.0.0.1:3306/dbatabase
+"""
+        call_command('sqldsn', '-q', '--style=uri')
+
+        self.assertEqual(expected_result, m_stdout.getvalue())
+
+    @override_settings(DATABASES={'default': MYSQL_DATABASE_SETTINGS})
+    @patch('sys.stdout', new_callable=StringIO)
+    def test_should_print_quiet_args_for_mysql_database(self, m_stdout):
+        expected_result = """-h "127.0.0.1" -D "dbatabase" -u "foo" -p "bar" -P 3306
+"""
+        call_command('sqldsn', '-q', '--style=args')
+
+        self.assertEqual(expected_result, m_stdout.getvalue())
+
     @override_settings(DATABASES={'default': POSTGRESQL_DATABASE_SETTINGS})
     @patch('sys.stdout', new_callable=StringIO)
     def test_should_print_all_info_for_postgresql_database(self, m_stdout):
@@ -160,7 +188,7 @@ host="127.0.0.1", db="dbatabase", user="foo", passwd="bar", port="3306"'''
         test_sqlite3 = """DSN for database 'test' with engine 'django.db.backends.sqlite3':
 db.sqlite3"""
         unknown = """DSN for database 'unknown' with engine 'django.db.backends.unknown':
-Unknown database, cant generate DSN"""
+Unknown database, can't generate DSN"""
 
         call_command('sqldsn', '--all')
 
@@ -189,7 +217,7 @@ host="127.0.0.1", db="dbatabase", user="foo", passwd="bar", port="3306"'''
         test_sqlite3 = """DSN for database 'test' with engine 'django.db.backends.sqlite3':
 db.sqlite3"""
         unknown = """DSN for database 'unknown' with engine 'django.db.backends.unknown':
-Unknown database, cant generate DSN"""
+Unknown database, can't generate DSN"""
 
         call_command('sqldsn', '--all', '--style=all')
 
diff --git a/tests/management/test_modelviz.py b/tests/management/test_modelviz.py
index db498b86..2ba86024 100644
--- a/tests/management/test_modelviz.py
+++ b/tests/management/test_modelviz.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 from django.test import SimpleTestCase
-from django_extensions.management.modelviz import generate_graph_data
+from django_extensions.management.modelviz import generate_graph_data, ON_DELETE_COLORS
 
 
 class ModelVizTests(SimpleTestCase):
@@ -25,3 +25,31 @@ class ModelVizTests(SimpleTestCase):
             'parent_cafe': u'Café latte',
         }
         self.assertEqual(expected, fields)
+
+    def test_on_delete_color_coding(self):
+        app_labels = ['django_extensions']
+        data = generate_graph_data(app_labels, color_code_deletions=True)
+
+        models = data['graphs'][0]['models']
+
+        for model in models:
+            relations = [x for x in model['relations'] if x['type'] in ('ForeignKey', 'OneToOneField')]
+
+            for relation in relations:
+                field = [x['field'] for x in model['fields'] if x['name'] == relation['name']][0]
+                on_delete = getattr(field.remote_field, 'on_delete', None)
+                expected_color = ON_DELETE_COLORS[on_delete]
+
+                self.assertIn('color={}'.format(expected_color), relation['arrows'])
+
+    def test_disabled_on_delete_color_coding(self):
+        app_labels = ['django_extensions']
+        data = generate_graph_data(app_labels)
+
+        models = data['graphs'][0]['models']
+
+        for model in models:
+            relations = [x for x in model['relations'] if x['type'] in ('ForeignKey', 'OneToOneField')]
+
+            for relation in relations:
+                self.assertNotIn('color=', relation['arrows'])
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 9d97e9f2..76608547 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -1,8 +1,10 @@
 # -*- coding: utf-8 -*-
+from unittest.mock import patch
+
 from django.core.exceptions import ValidationError
-from django.test import TestCase
+from django.test import TestCase, SimpleTestCase
 
-from django_extensions.validators import NoControlCharactersValidator, NoWhitespaceValidator
+from django_extensions.validators import NoControlCharactersValidator, NoWhitespaceValidator, HexValidator
 
 
 class NoControlCharactersValidatorTests(TestCase):
@@ -72,3 +74,82 @@ class NoWhiteSpaceValidatorTests(TestCase):
         result = self.validator(value_without_leading_or_trailing_whitespaces)
 
         self.assertIsNone(result)
+
+
+class TestHexValidator(SimpleTestCase):
+    def test_custom_message_and_code(self):
+        self.validator = HexValidator(message='message', code='code')
+
+        self.assertEqual(self.validator.message, 'message')
+        self.assertEqual(self.validator.code, 'code')
+
+    def test_equality_of_objs_with_obj_of_different_type(self):
+        self.assertNotEqual(TypeError(), HexValidator())
+
+    def test_equality_of_objs_with_different_code(self):
+        self.assertNotEqual(HexValidator(code='1'), HexValidator(code='a'))
+
+    def test_equality_of_objs_with_different_message(self):
+        self.assertNotEqual(HexValidator(code='code', message='a'), HexValidator(code='code', message='acb'))
+
+    def test_equality_of_objs_with_same_code_and_message(self):
+        self.assertEqual(HexValidator(code='c', message='m'), HexValidator(code='c', message='m'))
+
+    def test_fixed_length(self):
+        value = 'abcd'
+        self.validator = HexValidator(length=5)
+
+        with self.assertRaises(ValidationError) as err:
+            self.validator(value)
+
+        self.assertEqual(str(err.exception), "['Invalid length. Must be 5 characters.']")
+        self.assertEqual(err.exception.code, 'hex_only_length')
+
+    def test_min_length(self):
+        value = 'a'
+        self.validator = HexValidator(min_length=5)
+
+        with self.assertRaises(ValidationError) as err:
+            self.validator(value)
+
+        self.assertEqual(str(err.exception), "['Ensure that there are more than 5 characters.']")
+        self.assertEqual(err.exception.code, 'hex_only_min_length')
+
+    def test_with_max_length(self):
+        value = 'abcd'
+        self.validator = HexValidator(max_length=2)
+
+        with self.assertRaises(ValidationError) as err:
+            self.validator(value)
+
+        self.assertEqual(str(err.exception), "['Ensure that there are no more than 2 characters.']")
+        self.assertEqual(err.exception.code, 'hex_only_max_length')
+
+    def test_invalid_type(self):
+        value = 1
+        with patch('django_extensions.validators.force_str', return_value=1):
+            self.validator = HexValidator()
+
+            with self.assertRaises(ValidationError) as err:
+                self.validator(value)
+
+        self.assertEqual(str(err.exception), "['Only a hex string is allowed.']")
+        self.assertEqual(err.exception.code, 'hex_only')
+
+    def test_invalid_hex(self):
+        value = '1'
+        self.validator = HexValidator()
+
+        with self.assertRaises(ValidationError) as err:
+            self.validator(value)
+
+        self.assertEqual(str(err.exception), "['Only a hex string is allowed.']")
+        self.assertEqual(err.exception.code, 'hex_only')
+
+    def test_valid_hex(self):
+        value = 'b901ef'
+        self.validator = HexValidator()
+
+        result = self.validator(value)
+
+        self.assertIsNone(result)
diff --git a/tests/testapp/models.py b/tests/testapp/models.py
index 141580da..e044babc 100644
--- a/tests/testapp/models.py
+++ b/tests/testapp/models.py
@@ -43,6 +43,20 @@ class Note(models.Model):
         app_label = 'django_extensions'
 
 
+class Neighborhood(models.Model):
+    name = models.CharField(max_length=50)
+
+    class Meta:
+        app_label = 'django_extensions'
+
+
+class Bank(models.Model):
+    name = models.CharField(max_length=50)
+
+    class Meta:
+        app_label = 'django_extensions'
+
+
 class Person(models.Model):
     name = models.ForeignKey(Name, on_delete=models.CASCADE)
     age = models.PositiveIntegerField()
@@ -54,6 +68,8 @@ class Person(models.Model):
         on_delete=models.CASCADE,
     )
     clubs = models.ManyToManyField(Club, through='testapp.Membership')
+    neighborhood = models.ForeignKey(Neighborhood, on_delete=models.SET_NULL, null=True)
+    current_bank = models.ForeignKey(Bank, on_delete=models.PROTECT, null=True)
 
     class Meta:
         app_label = 'django_extensions'
diff --git a/tests/testapp/urls.py b/tests/testapp/urls.py
index f6c2848e..da1031ba 100644
--- a/tests/testapp/urls.py
+++ b/tests/testapp/urls.py
@@ -16,7 +16,7 @@ Including another URLconf
     1. Import the include() function: from django.conf.urls import url, include
     2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
 """
-from django.conf.urls import url
+from django.urls import path
 from django.contrib import admin
 from django.contrib.auth import views as auth_views
 
@@ -24,7 +24,7 @@ login_view = auth_views.LoginView.as_view() if hasattr(auth_views, 'LoginView')
 logout_view = auth_views.LogoutView.as_view() if hasattr(auth_views, 'LogoutView') else auth_views.logout
 
 urlpatterns = [
-    url(r'^login/$', login_view, {'template_name': 'login.html'}, name="login"),
-    url(r'^logout/$', logout_view, name="logout"),
-    url(r'^admin/', admin.site.urls),
+    path('login/', login_view, {'template_name': 'login.html'}, name="login"),
+    path('logout/', logout_view, name="logout"),
+    path('admin/', admin.site.urls),
 ]