[Python-modules-commits] [python-django-extensions] 03/07: New upstream version 1.8.0
Brian May
bam at moszumanska.debian.org
Tue Jul 4 22:16:07 UTC 2017
This is an automated email from the git hooks/post-receive script.
bam pushed a commit to branch debian/master
in repository python-django-extensions.
commit 11535caf18ea7a5b4ff29bc6f351ee05f57d9e21
Author: Brian May <bam at debian.org>
Date: Wed Jul 5 08:09:18 2017 +1000
New upstream version 1.8.0
---
.gitignore | 3 +-
.pre-commit-config.yaml | 8 +-
.travis.yml | 107 ++--
CHANGELOG.md | 97 ++++
Makefile | 2 +-
README.rst | 4 -
django_extensions/__init__.py | 2 +-
django_extensions/admin/__init__.py | 20 +-
django_extensions/admin/widgets.py | 5 +-
django_extensions/compat.py | 9 -
django_extensions/db/fields/__init__.py | 92 ++--
django_extensions/db/fields/json.py | 4 -
.../management/commands/admin_generator.py | 2 +-
.../management/commands/clear_cache.py | 20 +-
.../management/commands/create_command.py | 4 +
.../commands/delete_squashed_migrations.py | 180 +++++++
.../management/commands/dumpscript.py | 18 +-
.../management/commands/export_emails.py | 91 ++--
.../management/commands/generate_secret_key.py | 17 +-
.../management/commands/graph_models.py | 11 +-
.../management/commands/pipchecker.py | 41 +-
.../management/commands/runprofileserver.py | 10 +-
.../management/commands/runserver_plus.py | 58 +-
.../management/commands/shell_plus.py | 53 +-
django_extensions/management/commands/show_urls.py | 5 +-
django_extensions/management/commands/sqldiff.py | 12 +-
django_extensions/management/commands/sync_s3.py | 8 +-
django_extensions/management/commands/syncdata.py | 5 +-
.../management/commands/unreferenced_files.py | 14 +-
.../management/commands/validate_templates.py | 22 +-
django_extensions/management/modelviz.py | 586 ++++++++++++---------
django_extensions/management/shells.py | 11 +-
django_extensions/management/utils.py | 32 ++
django_extensions/mongodb/fields/__init__.py | 19 +-
.../widgets/foreignkey_searchinput.html | 14 +-
django_extensions/utils/text.py | 14 +-
django_extensions/utils/validatingtemplatetags.py | 100 ----
docs/command_extensions.rst | 19 +
docs/conf.py | 4 +-
docs/creating_release.txt | 4 +-
docs/delete_squashed_migrations.rst | 30 ++
docs/dumpscript.rst | 11 +-
docs/field_extensions.rst | 14 +-
docs/graph_models.rst | 10 +
docs/index.rst | 1 +
docs/installation_instructions.rst | 87 ++-
docs/runserver_plus.rst | 2 +-
docs/shell_plus.rst | 28 +-
manage.py | 12 +
setup.cfg | 2 +-
setup.py | 11 +-
tests/management/commands/test_export_emails.py | 121 +++++
tests/management/commands/test_sync_s3.py | 69 +++
tests/management/test_modelviz.py | 14 -
tests/test_autoslug_fields.py | 43 +-
tests/test_management_command.py | 1 +
tests/test_uuid_field.py | 52 --
tests/testapp/fixtures/group.json | 10 +
tests/testapp/fixtures/user.json | 54 ++
tests/testapp/models.py | 33 +-
tox.ini | 17 +-
61 files changed, 1547 insertions(+), 802 deletions(-)
diff --git a/.gitignore b/.gitignore
index b53881f..1c50e7a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@ venv*
.coverage
.cache/
django-sample-app*/
-
+*.swp
+*.swo
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 965cbfb..a348eee 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
- repo: git://github.com/pre-commit/pre-commit-hooks
- sha: 493665a5fc24deb2204fc48b8398be77e6c9e5d5
+ sha: v0.7.0
hooks:
- id: trailing-whitespace
- id: check-added-large-files
@@ -19,13 +19,13 @@
- id: name-tests-test
args:
- --django
- exclude: '^tests/testapp|^tests/management/'
+ exclude: ^tests/testapp|^tests/management/
- repo: git://github.com/Lucas-C/pre-commit-hooks.git
- sha: 0f5055d31f39ea3ac1f5335a0cca320200cd0c51
+ sha: c25201a00e6b0514370501050cf2a8538ac12270
hooks:
- id: forbid-crlf
- repo: git://github.com/trbs/pre-commit-hooks-trbs.git
- sha: c94ad5f83406a96b0750869e8930669af074975b
+ sha: e233916fb2b4b9019b4a3cc0497994c7926fe36b
hooks:
- id: forbid-executables
exclude: manage.py|setup.py
diff --git a/.travis.yml b/.travis.yml
index aa867a5..dd165aa 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,54 +1,77 @@
language: python
-python:
- - 3.5
-
sudo: false
-env:
- - TOX_ENV=py27-flake8
- - TOX_ENV=py34-flake8
- - TOX_ENV=py35-flake8
- - TOX_ENV=precommit
- - TOX_ENV=py27-dj18
- - TOX_ENV=py27-dj19
- - TOX_ENV=py27-dj110
- - TOX_ENV=py27-djmaster
- - TOX_ENV=py32-dj18
- - TOX_ENV=py33-dj18
- - TOX_ENV=py34-dj18
- - TOX_ENV=py34-dj19
- - TOX_ENV=py34-dj110
- - TOX_ENV=py34-djmaster
- - TOX_ENV=py35-dj18
- - TOX_ENV=py35-dj19
- - TOX_ENV=py35-dj110
- - TOX_ENV=py35-djmaster
- - TOX_ENV=pypy-dj18
- - TOX_ENV=pypy-dj19
- - TOX_ENV=pypy-dj110
- - TOX_ENV=pypy-djmaster
- # - TOX_ENV=pypy3-dj18
- # - TOX_ENV=pypy3-dj19
- # - TOX_ENV=pypy3-dj110
- - TOX_ENV=pypy3-djmaster
-
matrix:
fast_finish: true
+ include:
+ - python: 2.7
+ env: TOX_ENV=py27-flake8
+ - python: 3.4
+ env: TOX_ENV=py34-flake8
+ - python: 3.5
+ env: TOX_ENV=py35-flake8
+ - python: 3.6
+ env: TOX_ENV=py36-flake8
+ - python: 3.6
+ env: TOX_ENV=precommit
+ - python: 3.6
+ env: TOX_ENV=safety
+ - python: 2.7
+ env: TOX_ENV=py27-dj18
+ - python: 2.7
+ env: TOX_ENV=py27-dj110
+ - python: 2.7
+ env: TOX_ENV=py27-dj111
+ - python: 3.3
+ env: TOX_ENV=py33-dj18
+ - python: 3.4
+ env: TOX_ENV=py34-dj18
+ - python: 3.4
+ env: TOX_ENV=py34-dj110
+ - python: 3.4
+ env: TOX_ENV=py34-dj111
+ - python: 3.5
+ env: TOX_ENV=py35-dj18
+ - python: 3.5
+ env: TOX_ENV=py35-dj110
+ - python: 3.5
+ env: TOX_ENV=py35-dj111
+ - python: 3.5
+ env: TOX_ENV=py35-djmaster
+ - python: 3.6
+ env: TOX_ENV=py36-dj18
+ - python: 3.6
+ env: TOX_ENV=py36-dj110
+ - python: 3.6
+ env: TOX_ENV=py36-dj111
+ - python: 3.6
+ env: TOX_ENV=py36-djmaster
+ - python: pypy
+ env: TOX_ENV=pypy-dj18
+ - python: pypy
+ env: TOX_ENV=pypy-dj110
+ - python: pypy
+ env: TOX_ENV=pypy-dj111
+ - python: pypy3.5-5.8.0
+ env: TOX_ENV=pypy3-dj18
+ - python: pypy3.5-5.8.0
+ env: TOX_ENV=pypy3-dj110
+ - python: pypy3.5-5.8.0
+ env: TOX_ENV=pypy3-djmaster
+
allow_failures:
- - env: TOX_ENV=py32-dj18
- - env: TOX_ENV=py35-djmaster
- - env: TOX_ENV=py34-djmaster
- - env: TOX_ENV=py27-djmaster
- - env: TOX_ENV=pypy-djmaster
- # pypy3 is not >=3.4 compatible yet
- # - env: TOX_ENV=pypy3-dj19
- # - env: TOX_ENV=pypy3-dj110
- - env: TOX_ENV=pypy3-djmaster
+ - python: 3.5
+ env: TOX_ENV=py35-djmaster
+ - python: 3.6
+ env: TOX_ENV=py36-djmaster
+ - python: pypy3.5-5.8.0
+ env: TOX_ENV=pypy3-dj110
+ - python: pypy3.5-5.8.0
+ env: TOX_ENV=pypy3-djmaster
install:
- # virtualenv<14.0.0 is for Python 3.2 compatibility
- - pip install "virtualenv<14.0.0" tox coveralls
+ - pip install virtualenv tox coveralls
script:
- tox -e $TOX_ENV
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fecd442..8da6c99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,103 @@
Changelog
=========
+1.8.0
+-----
+
+UUIDField has been removed after being deprecated.
+
+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
+
+
+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 algoritme from django
+ - Docs: fix grammer 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
+
+
+1.7.7
+-----
+
+Changes:
+ - Improvement: admin_generator, use decorator style for registring 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)
+
+
+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
+
+
+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
+
+
1.7.3
-----
diff --git a/Makefile b/Makefile
index e6c4a1e..335ad19 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,7 @@ clean-test:
rm -fr htmlcov/
test:
- python setup.py test --pytest-args="tests django_extensions --ds=tests.testapp.settings --cov=django_extensions"
+ python setup.py test
coverage:
coverage run --source django_extensions setup.py test
diff --git a/README.rst b/README.rst
index a455ca1..0943760 100644
--- a/README.rst
+++ b/README.rst
@@ -13,10 +13,6 @@
:target: https://pypi.python.org/pypi/django-extensions/
:alt: Latest PyPI version
-.. image:: https://img.shields.io/pypi/dm/django-extensions.svg
- :target: https://pypi.python.org/pypi/django-extensions/
- :alt: Number of PyPI downloads
-
.. image:: https://img.shields.io/pypi/wheel/django-extensions.svg
:target: https://pypi.python.org/pypi/django-extensions/
:alt: Supports Wheel format
diff --git a/django_extensions/__init__.py b/django_extensions/__init__.py
index 92443a5..2a6ac94 100644
--- a/django_extensions/__init__.py
+++ b/django_extensions/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-VERSION = (1, 7, 4)
+VERSION = (1, 8, 0)
# Dynamically calculate the version based on VERSION tuple
if len(VERSION) > 2 and VERSION[2] is not None:
diff --git a/django_extensions/admin/__init__.py b/django_extensions/admin/__init__.py
index 4e25bb4..eb3fddb 100644
--- a/django_extensions/admin/__init__.py
+++ b/django_extensions/admin/__init__.py
@@ -15,12 +15,12 @@ from django.db.models.query import QuerySet
from django.utils.encoding import smart_str
from django.utils.translation import ugettext as _
from django.utils.text import get_text_list
-from django.contrib.admin import ModelAdmin
+from django.contrib import admin
from django_extensions.admin.widgets import ForeignKeySearchInput
-class ForeignKeyAutocompleteAdmin(ModelAdmin):
+class ForeignKeyAutocompleteAdminMixin(object):
"""Admin class for models using the autocomplete feature.
There are two additional fields:
@@ -60,7 +60,7 @@ class ForeignKeyAutocompleteAdmin(ModelAdmin):
return [
url(r'foreignkey_autocomplete/$', wrap(self.foreignkey_autocomplete),
name='%s_%s_autocomplete' % (self.model._meta.app_label, self.model._meta.model_name))
- ] + super(ForeignKeyAutocompleteAdmin, self).get_urls()
+ ] + super(ForeignKeyAutocompleteAdminMixin, self).get_urls()
def foreignkey_autocomplete(self, request):
"""
@@ -151,4 +151,16 @@ class ForeignKeyAutocompleteAdmin(ModelAdmin):
help_text = six.u('%s %s' % (kwargs['help_text'], help_text))
kwargs['widget'] = ForeignKeySearchInput(db_field.rel, self.related_search_fields[db_field.name])
kwargs['help_text'] = help_text
- return super(ForeignKeyAutocompleteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+ return super(ForeignKeyAutocompleteAdminMixin, self).formfield_for_dbfield(db_field, **kwargs)
+
+
+class ForeignKeyAutocompleteAdmin(ForeignKeyAutocompleteAdminMixin, admin.ModelAdmin):
+ pass
+
+
+class ForeignKeyAutocompleteTabularInline(ForeignKeyAutocompleteAdminMixin, admin.TabularInline):
+ pass
+
+
+class ForeignKeyAutocompleteStackedInline(ForeignKeyAutocompleteAdminMixin, admin.StackedInline):
+ pass
diff --git a/django_extensions/admin/widgets.py b/django_extensions/admin/widgets.py
index 969cc2b..a6ca1cb 100644
--- a/django_extensions/admin/widgets.py
+++ b/django_extensions/admin/widgets.py
@@ -4,7 +4,10 @@ from six.moves import urllib
from django import forms
from django.contrib.admin.sites import site
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
-from django.core.urlresolvers import reverse
+try:
+ from django.urls import reverse
+except ImportError:
+ from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe
from django.utils.text import Truncator
diff --git a/django_extensions/compat.py b/django_extensions/compat.py
index 94ff8bf..57583d9 100644
--- a/django_extensions/compat.py
+++ b/django_extensions/compat.py
@@ -33,15 +33,6 @@ def load_tag_library(libname):
return None
-def add_to_builtins_compat(name):
- if django.VERSION < (1, 9):
- from django.template.base import add_to_builtins
- add_to_builtins(name)
- else:
- from django.template import engines
- engines['django'].engine.builtins.append(name)
-
-
def get_template_setting(template_key, default=None):
""" Read template settings pre and post django 1.8 """
templates_var = getattr(settings, 'TEMPLATES', None)
diff --git a/django_extensions/db/fields/__init__.py b/django_extensions/db/fields/__init__.py
index 0f71a10..8e7c118 100644
--- a/django_extensions/db/fields/__init__.py
+++ b/django_extensions/db/fields/__init__.py
@@ -5,7 +5,6 @@ Django Extensions additional model fields
import re
import six
import string
-import warnings
try:
import uuid
@@ -19,14 +18,16 @@ try:
except ImportError:
HAS_SHORT_UUID = False
+from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.models import DateTimeField, CharField, SlugField
+from django.db.models.constants import LOOKUP_SEP
from django.template.defaultfilters import slugify
from django.utils.crypto import get_random_string
from django.utils.encoding import force_text
-MAX_UNIQUE_QUERY_ATTEMPTS = 100
+MAX_UNIQUE_QUERY_ATTEMPTS = getattr(settings, 'EXTENSIONS_MAX_UNIQUE_QUERY_ATTEMPTS', 100)
class UniqueFieldMixin(object):
@@ -79,7 +80,12 @@ class AutoSlugField(UniqueFieldMixin, SlugField):
Required arguments:
populate_from
- Specifies which field or list of fields the slug is populated from.
+ Specifies which field, list of fields, or model method
+ the slug will be populated from.
+
+ populate_from can traverse a ForeignKey relationship
+ by using Django ORM syntax:
+ populate_from = 'related_model__field'
Optional arguments:
@@ -108,6 +114,7 @@ class AutoSlugField(UniqueFieldMixin, SlugField):
self.check_is_bool('overwrite')
self.allow_duplicates = kwargs.pop('allow_duplicates', False)
self.check_is_bool('allow_duplicates')
+ self.max_unique_query_attempts = kwargs.pop('max_unique_query_attempts', MAX_UNIQUE_QUERY_ATTEMPTS)
super(AutoSlugField, self).__init__(*args, **kwargs)
def _slug_strip(self, value):
@@ -129,7 +136,7 @@ class AutoSlugField(UniqueFieldMixin, SlugField):
def slug_generator(self, original_slug, start):
yield original_slug
- for i in range(start, MAX_UNIQUE_QUERY_ATTEMPTS):
+ for i in range(start, self.max_unique_query_attempts):
slug = original_slug
end = '%s%s' % (self.separator, i)
end_len = len(end)
@@ -138,8 +145,7 @@ class AutoSlugField(UniqueFieldMixin, SlugField):
slug = self._slug_strip(slug)
slug = '%s%s' % (slug, end)
yield slug
- raise RuntimeError('max slug attempts for %s exceeded (%s)' %
- (original_slug, MAX_UNIQUE_QUERY_ATTEMPTS))
+ raise RuntimeError('max slug attempts for %s exceeded (%s)' % (original_slug, self.max_unique_query_attempts))
def create_slug(self, model_instance, add):
# get fields to populate from and slug field to set
@@ -149,7 +155,7 @@ class AutoSlugField(UniqueFieldMixin, SlugField):
if add or self.overwrite:
# slugify the original field content and set next step to 2
- slug_for_field = lambda field: self.slugify_func(getattr(model_instance, field))
+ slug_for_field = lambda lookup_value: self.slugify_func(self.get_slug_fields(model_instance, lookup_value))
slug = self.separator.join(map(slug_for_field, self._populate_from))
start = 2
else:
@@ -174,6 +180,22 @@ class AutoSlugField(UniqueFieldMixin, SlugField):
return super(AutoSlugField, self).find_unique(
model_instance, slug_field, self.slug_generator(original_slug, start))
+ def get_slug_fields(self, model_instance, lookup_value):
+ lookup_value_path = lookup_value.split(LOOKUP_SEP)
+ attr = model_instance
+ for elem in lookup_value_path:
+ try:
+ attr = getattr(attr, elem)
+ except AttributeError:
+ raise AttributeError(
+ "value {} in AutoSlugField's 'populate_from' argument {} returned an error - {} has no attribute {}".format(
+ elem, lookup_value, attr, elem))
+
+ if callable(attr):
+ return "%s" % attr()
+
+ return attr
+
def pre_save(self, model_instance, add):
value = force_text(self.create_slug(model_instance, add))
return value
@@ -244,6 +266,7 @@ class RandomCharField(UniqueFieldMixin, CharField):
self.check_is_bool('include_alpha')
self.include_punctuation = kwargs.pop('include_punctuation', False)
self.check_is_bool('include_punctuation')
+ self.max_unique_query_attempts = kwargs.pop('max_unique_query_attempts', MAX_UNIQUE_QUERY_ATTEMPTS)
# Set unique=False unless it's been set manually.
if 'unique' not in kwargs:
@@ -252,10 +275,9 @@ class RandomCharField(UniqueFieldMixin, CharField):
super(RandomCharField, self).__init__(*args, **kwargs)
def random_char_generator(self, chars):
- for i in range(MAX_UNIQUE_QUERY_ATTEMPTS):
+ for i in range(self.max_unique_query_attempts):
yield ''.join(get_random_string(self.length, chars))
- raise RuntimeError('max random character attempts exceeded (%s)' %
- MAX_UNIQUE_QUERY_ATTEMPTS)
+ raise RuntimeError('max random character attempts exceeded (%s)' % self.max_unique_query_attempts)
def pre_save(self, model_instance, add):
if not add and getattr(model_instance, self.attname) != '':
@@ -367,8 +389,8 @@ class UUIDVersionError(Exception):
pass
-class UUIDField(CharField):
- """ UUIDField
+class UUIDFieldMixin(object):
+ """ UUIDFieldMixin
By default uses UUID version 4 (randomly generated UUID).
@@ -377,23 +399,29 @@ class UUIDField(CharField):
"""
DEFAULT_MAX_LENGTH = 36
- def __init__(self, verbose_name=None, name=None, auto=True, version=4, node=None, clock_seq=None, namespace=None, uuid_name=None, *args, **kwargs):
- warnings.warn("Django 1.8 features a native UUIDField, this UUIDField will be removed after Django 1.7 becomes unsupported.", DeprecationWarning)
-
+ def __init__(
+ self, verbose_name=None, name=None, auto=True, version=4,
+ node=None, clock_seq=None, namespace=None, uuid_name=None, *args,
+ **kwargs):
if not HAS_UUID:
raise ImproperlyConfigured("'uuid' module is required for UUIDField. (Do you have Python 2.5 or higher installed ?)")
+
kwargs.setdefault('max_length', self.DEFAULT_MAX_LENGTH)
+
if auto:
self.empty_strings_allowed = False
kwargs['blank'] = True
kwargs.setdefault('editable', False)
+
self.auto = auto
self.version = version
self.node = node
self.clock_seq = clock_seq
self.namespace = namespace
self.uuid_name = uuid_name or name
- super(UUIDField, self).__init__(verbose_name=verbose_name, *args, **kwargs)
+
+ super(UUIDFieldMixin, self).__init__(
+ verbose_name=verbose_name, *args, **kwargs)
def create_uuid(self):
if not self.version or self.version == 4:
@@ -410,7 +438,8 @@ class UUIDField(CharField):
raise UUIDVersionError("UUID version %s is not valid." % self.version)
def pre_save(self, model_instance, add):
- value = super(UUIDField, self).pre_save(model_instance, add)
+ value = super(UUIDFieldMixin, self).pre_save(model_instance, add)
+
if self.auto and add and value is None:
value = force_text(self.create_uuid())
setattr(model_instance, self.attname, value)
@@ -419,15 +448,17 @@ class UUIDField(CharField):
if self.auto and not value:
value = force_text(self.create_uuid())
setattr(model_instance, self.attname, value)
+
return value
def formfield(self, **kwargs):
if self.auto:
return None
- return super(UUIDField, self).formfield(**kwargs)
+ return super(UUIDFieldMixin, self).formfield(**kwargs)
def deconstruct(self):
- name, path, args, kwargs = super(UUIDField, self).deconstruct()
+ name, path, args, kwargs = super(UUIDFieldMixin, self).deconstruct()
+
if kwargs.get('max_length', None) == self.DEFAULT_MAX_LENGTH:
del kwargs['max_length']
if self.auto is not True:
@@ -442,30 +473,11 @@ class UUIDField(CharField):
kwargs['namespace'] = self.namespace
if self.uuid_name is not None:
kwargs['uuid_name'] = self.name
- return name, path, args, kwargs
-
-class PostgreSQLUUIDField(UUIDField):
- def __init__(self, *args, **kwargs):
- warnings.warn("Django 1.8 features a native UUIDField, this UUIDField will be removed after Django 1.7 becomes unsupported.", DeprecationWarning)
- super(PostgreSQLUUIDField, self).__init__(*args, **kwargs)
-
- def db_type(self, connection=None):
- return "UUID"
-
- def get_db_prep_value(self, value, connection, prepared=False):
- if isinstance(value, six.integer_types):
- value = uuid.UUID(int=value)
- elif isinstance(value, (six.string_types, six.binary_type)):
- if len(value) == 16:
- value = uuid.UUID(bytes=value)
- else:
- value = uuid.UUID(value)
- return super(PostgreSQLUUIDField, self).get_db_prep_value(
- value, connection, prepared=False)
+ return name, path, args, kwargs
-class ShortUUIDField(UUIDField):
+class ShortUUIDField(UUIDFieldMixin, CharField):
""" ShortUUIDFied
Generates concise (22 characters instead of 36), unambiguous, URL-safe UUIDs.
diff --git a/django_extensions/db/fields/json.py b/django_extensions/db/fields/json.py
index baa5318..f6b30af 100644
--- a/django_extensions/db/fields/json.py
+++ b/django_extensions/db/fields/json.py
@@ -14,7 +14,6 @@ from __future__ import absolute_import
import json
import six
-import warnings
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
@@ -55,9 +54,6 @@ class JSONField(models.TextField):
JSON objects seamlessly. Main thingy must be a dict object."""
def __init__(self, *args, **kwargs):
- warnings.warn("Django 1.9 features a native JsonField, this JSONField will "
- "be removed somewhere after Django 1.8 becomes unsupported.",
- DeprecationWarning)
kwargs['default'] = kwargs.get('default', dict)
models.TextField.__init__(self, *args, **kwargs)
diff --git a/django_extensions/management/commands/admin_generator.py b/django_extensions/management/commands/admin_generator.py
index bd3e90d..23bb5c7 100644
--- a/django_extensions/management/commands/admin_generator.py
+++ b/django_extensions/management/commands/admin_generator.py
@@ -63,8 +63,8 @@ from .models import %(models)s
PRINT_ADMIN_CLASS = getattr(settings, 'PRINT_ADMIN_CLASS', '''
+ at admin.register(%(name)s)
class %(name)sAdmin(admin.ModelAdmin):%(class_)s
-admin.site.register(%(name)s, %(name)sAdmin)
''')
PRINT_ADMIN_PROPERTY = getattr(settings, 'PRINT_ADMIN_PROPERTY', '''
diff --git a/django_extensions/management/commands/clear_cache.py b/django_extensions/management/commands/clear_cache.py
index abf5b2d..3a1341a 100644
--- a/django_extensions/management/commands/clear_cache.py
+++ b/django_extensions/management/commands/clear_cache.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Author: AxiaCore S.A.S. http://axiacore.com
-from django.core.cache import cache
+from django.core.cache import DEFAULT_CACHE_ALIAS, caches
+from django.core.cache.backends.base import InvalidCacheBackendError
from django.core.management.base import BaseCommand
from django_extensions.management.utils import signalcommand
@@ -10,7 +11,18 @@ class Command(BaseCommand):
"""A simple management command which clears the site-wide cache."""
help = 'Fully clear site-wide cache.'
+ def add_arguments(self, parser):
+ parser.add_argument('--cache', action='append',
+ help='Name of cache to clear')
+
@signalcommand
- def handle(self, *args, **kwargs):
- cache.clear()
- self.stdout.write('Cache has been cleared!\n')
+ def handle(self, cache, *args, **kwargs):
+ if not cache:
+ cache = [DEFAULT_CACHE_ALIAS]
+ for key in cache:
+ try:
+ caches[key].clear()
+ except InvalidCacheBackendError:
+ self.stderr.write('Cache "%s" is invalid!\n' % key)
+ else:
+ self.stdout.write('Cache "%s" has been cleared!\n' % key)
diff --git a/django_extensions/management/commands/create_command.py b/django_extensions/management/commands/create_command.py
index 3e12295..483d68b 100644
--- a/django_extensions/management/commands/create_command.py
+++ b/django_extensions/management/commands/create_command.py
@@ -25,6 +25,10 @@ class Command(AppCommand):
default='sample',
help='The name to use for the management command')
parser.add_argument(
+ '--base', '-b', action='store', dest='base_command',
+ default='Base', help='The base class used for implementation of '
+ 'this command. Should be one of Base, App, Label, or NoArgs')
+ parser.add_argument(
'--dry-run', action='store_true', default=False,
help='Do not actually create any files')
diff --git a/django_extensions/management/commands/delete_squashed_migrations.py b/django_extensions/management/commands/delete_squashed_migrations.py
new file mode 100644
index 0000000..0abff52
--- /dev/null
+++ b/django_extensions/management/commands/delete_squashed_migrations.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+import os
+import inspect
+import re
+
+from django.core.management.base import BaseCommand, CommandError
+from django.db import DEFAULT_DB_ALIAS, connections
+from django.db.migrations.loader import AmbiguityError, MigrationLoader
+from django.utils import six
+
+REPLACES_REGEX = re.compile(r'\s+replaces\s*=\s*\[[^\]]+\]\s*')
+PYC = '.pyc'
+
+
+def py_from_pyc(pyc_fn):
+ return pyc_fn[:-len(PYC)] + '.py'
+
+
+class Command(BaseCommand):
+
+ help = "Deletes left over migrations that have been replaced by a "
+ "squashed migration and converts squashed migration into a normal "
+ "migration. Modifies your source tree! Use with care!"
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'app_label',
+ help='App label of the application to delete replaced migrations from.',
+ )
+ parser.add_argument(
+ 'squashed_migration_name', default=None, nargs='?',
+ help='The squashed migration to replace. '
+ 'If not specified defaults to the first found.'
+ )
+ parser.add_argument(
+ '--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(
+ '--dry-run', action='store_true', default=False,
+ help='Do not actually delete or change any files')
+
+ def handle(self, **options):
+ self.verbosity = options['verbosity']
+ self.interactive = options['interactive']
+ self.dry_run = options['dry_run']
+ app_label = options['app_label']
+ squashed_migration_name = options['squashed_migration_name']
+
+ # Load the current graph state, check the app and migration they asked for exists
+ loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
+ if app_label not in loader.migrated_apps:
+ raise CommandError(
+ "App '%s' does not have migrations (so delete_squashed_migrations on "
+ "it makes no sense)" % app_label
+ )
+
+ squashed_migration = None
+ if squashed_migration_name:
+ squashed_migration = self.find_migration(loader, app_label, squashed_migration_name)
+ if not squashed_migration.replaces:
+ raise CommandError(
+ "The migration %s %s is not a squashed migration." %
+ (squashed_migration.app_label, squashed_migration.name)
+ )
+ else:
+ leaf_nodes = loader.graph.leaf_nodes(app=app_label)
+ migration = loader.get_migration(*leaf_nodes[0])
+ previous_migrations = [
+ loader.get_migration(al, mn)
+ for al, mn in loader.graph.forwards_plan((migration.app_label, migration.name))
+ if al == migration.app_label
+ ]
+ migrations = previous_migrations + [migration]
+ for migration in migrations:
+ if migration.replaces:
+ squashed_migration = migration
+ break
+
+ if not squashed_migration:
+ raise CommandError(
+ "Cannot find a squashed migration in app '%s'." %
+ (app_label)
+ )
+
+ files_to_delete = []
+ for al, mn in squashed_migration.replaces:
+ try:
+ migration = loader.disk_migrations[al, mn]
+ except KeyError:
+ if self.verbosity > 0:
+ self.stderr.write("Couldn't find migration file for %s %s\n"
+ % (al, mn))
+ else:
+ pyc_file = inspect.getfile(migration.__class__)
+ files_to_delete.append(pyc_file)
+ if pyc_file.endswith(PYC):
+ py_file = py_from_pyc(pyc_file)
+ files_to_delete.append(py_file)
+
+ # Tell them what we're doing and optionally ask if we should proceed
+ if self.verbosity > 0 or self.interactive:
+ self.stdout.write(self.style.MIGRATE_HEADING("Will delete the following files:"))
+ for fn in files_to_delete:
+ self.stdout.write(" - %s" % fn)
+
+ if not self.confirm():
+ return
+
+ for fn in files_to_delete:
+ try:
+ if not self.dry_run:
+ os.remove(fn)
+ except OSError:
+ if self.verbosity > 0:
+ self.stderr.write("Couldn't delete %s\n" % (fn,))
+
+ # Try and delete replaces only if it's all on one line
+ squashed_migration_fn = inspect.getfile(squashed_migration.__class__)
+ if squashed_migration_fn.endswith(PYC):
+ squashed_migration_fn = py_from_pyc(squashed_migration_fn)
+ with open(squashed_migration_fn) as fp:
+ squashed_migration_lines = list(fp)
+
+ delete_lines = []
+ for i, line in enumerate(squashed_migration_lines):
+ if REPLACES_REGEX.match(line):
+ delete_lines.append(i)
+ if i > 0 and squashed_migration_lines[i - 1].strip() == '':
+ delete_lines.insert(0, i - 1)
+ break
+ if not delete_lines:
+ raise CommandError(
+ ("Couldn't find 'replaces =' line in file %s. "
+ "Please finish cleaning up manually.") % (squashed_migration_fn,)
+ )
+
+ if self.verbosity > 0 or self.interactive:
+ self.stdout.write(self.style.MIGRATE_HEADING(
+ "Will delete line %s%s from file %s" %
+ (delete_lines[0],
+ ' and ' + str(delete_lines[1]) if len(delete_lines) > 1 else "",
+ squashed_migration_fn)))
+
+ if not self.confirm():
+ return
+
+ for line_num in sorted(delete_lines, reverse=True):
+ del squashed_migration_lines[line_num]
+
+ with open(squashed_migration_fn, 'w') as fp:
+ if not self.dry_run:
+ fp.write("".join(squashed_migration_lines))
+
+ def confirm(self):
+ if self.interactive:
+ answer = None
+ while not answer or answer not in "yn":
+ answer = six.moves.input("Do you wish to proceed? [yN] ")
+ if not answer:
+ answer = "n"
+ break
+ else:
+ answer = answer[0].lower()
+ return answer == "y"
+ return True
+
+ def find_migration(self, loader, app_label, name):
+ try:
+ return loader.get_migration_by_prefix(app_label, name)
+ except AmbiguityError:
+ raise CommandError(
+ "More than one migration matches '%s' in app '%s'. Please be "
+ "more specific." % (name, app_label)
+ )
+ except KeyError:
+ raise CommandError(
+ "Cannot find a migration matching '%s' from app '%s'." %
+ (name, app_label)
+ )
diff --git a/django_extensions/management/commands/dumpscript.py b/django_extensions/management/commands/dumpscript.py
index c119190..c5d8807 100644
--- a/django_extensions/management/commands/dumpscript.py
+++ b/django_extensions/management/commands/dumpscript.py
@@ -664,7 +664,9 @@ def get_attribute_value(item, field, context, force=False, skip_autofield=True):
# content types in this script, as they can be generated again
# automatically.
# NB: Not sure if "is" will always work
- if field.rel.to is ContentType:
+ remote_field = field.remote_field if hasattr(field, 'remote_field') else field.rel # Remove me after Django 1.8 is unsupported
+ remote_field_model = remote_field.model if hasattr(remote_field, 'model') else remote_field.to # Remove me after Django 1.8 is unsupported
+ if remote_field_model is ContentType:
return 'ContentType.objects.get(app_label="%s", model="%s")' % (value.app_label, value.model)
# Generate an identifier (key) for this foreign object
@@ -711,13 +713,21 @@ def check_dependencies(model, model_queue, avaliable_models):
# For each ForeignKey or ManyToMany field, check that a link is possible
for field in model._meta.fields:
- if field.rel and field.rel.to.__name__ not in allowed_links:
- if field.rel.to not in avaliable_models:
+ remote_field = field.remote_field if hasattr(field, 'remote_field') else field.rel # Remove me after Django 1.8 is unsupported
+ if not remote_field:
+ continue
+ remote_field_model = remote_field.model if hasattr(remote_field, 'model') else remote_field.to # Remove me after Django 1.8 is unsupported
+ if remote_field_model.__name__ not in allowed_links:
+ if remote_field_model not in avaliable_models:
continue
return False
for field in model._meta.many_to_many:
- if field.rel and field.rel.to.__name__ not in allowed_links:
+ remote_field = field.remote_field if hasattr(field, 'remote_field') else field.rel # Remove me after Django 1.8 is unsupported
+ if not remote_field:
+ continue
... 2929 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-django-extensions.git
More information about the Python-modules-commits
mailing list