[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