[Python-modules-commits] [python-django] 01/03: Imported Upstream version 1.7.6

Raphaël Hertzog hertzog at moszumanska.debian.org
Mon Mar 9 20:59:07 UTC 2015


This is an automated email from the git hooks/post-receive script.

hertzog pushed a commit to branch debian/sid
in repository python-django.

commit c2ec6c3209ee7f232e8bfca3d922a810bd3078d5
Author: Raphaël Hertzog <hertzog at debian.org>
Date:   Mon Mar 9 21:40:21 2015 +0100

    Imported Upstream version 1.7.6
---
 Django.egg-info/PKG-INFO                          |  2 +-
 Django.egg-info/SOURCES.txt                       |  1 +
 PKG-INFO                                          |  2 +-
 django/__init__.py                                |  2 +-
 django/contrib/admin/helpers.py                   |  2 +-
 django/db/backends/schema.py                      |  6 +-
 django/db/models/fields/related.py                |  3 +
 docs/howto/auth-remote-user.txt                   | 12 ++++
 docs/howto/custom-template-tags.txt               |  2 +-
 docs/howto/deployment/wsgi/modwsgi.txt            | 12 +++-
 docs/intro/tutorial01.txt                         |  4 +-
 docs/ref/class-based-views/generic-date-based.txt |  4 --
 docs/ref/templates/builtins.txt                   |  8 ++-
 docs/releases/1.7.6.txt                           | 30 +++++++++
 docs/releases/index.txt                           |  1 +
 docs/topics/auth/default.txt                      | 82 ++++++++++++++++++++---
 docs/topics/forms/modelforms.txt                  | 21 +++++-
 tests/admin_views/admin.py                        |  2 +-
 tests/admin_views/models.py                       |  7 ++
 tests/admin_views/tests.py                        |  9 +++
 tests/m2m_regress/tests.py                        |  4 ++
 tests/schema/models.py                            |  9 +++
 tests/schema/tests.py                             | 36 +++++++++-
 23 files changed, 224 insertions(+), 37 deletions(-)

diff --git a/Django.egg-info/PKG-INFO b/Django.egg-info/PKG-INFO
index 8457a3f..21d6b3f 100644
--- a/Django.egg-info/PKG-INFO
+++ b/Django.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: Django
-Version: 1.7.5
+Version: 1.7.6
 Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
 Home-page: http://www.djangoproject.com/
 Author: Django Software Foundation
diff --git a/Django.egg-info/SOURCES.txt b/Django.egg-info/SOURCES.txt
index da86442..f67cbb5 100644
--- a/Django.egg-info/SOURCES.txt
+++ b/Django.egg-info/SOURCES.txt
@@ -4023,6 +4023,7 @@ docs/releases/1.7.2.txt
 docs/releases/1.7.3.txt
 docs/releases/1.7.4.txt
 docs/releases/1.7.5.txt
+docs/releases/1.7.6.txt
 docs/releases/1.7.txt
 docs/releases/index.txt
 docs/releases/security.txt
diff --git a/PKG-INFO b/PKG-INFO
index 8457a3f..21d6b3f 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: Django
-Version: 1.7.5
+Version: 1.7.6
 Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
 Home-page: http://www.djangoproject.com/
 Author: Django Software Foundation
diff --git a/django/__init__.py b/django/__init__.py
index 0e4c6e8..77b09d9 100644
--- a/django/__init__.py
+++ b/django/__init__.py
@@ -1,4 +1,4 @@
-VERSION = (1, 7, 5, 'final', 0)
+VERSION = (1, 7, 6, 'final', 0)
 
 
 def get_version(*args, **kwargs):
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 4f6bbe5..6ea34e2 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -193,7 +193,7 @@ class AdminReadonlyField(object):
                     if getattr(attr, "allow_tags", False):
                         result_repr = mark_safe(result_repr)
                     else:
-                        result_repr = linebreaksbr(result_repr)
+                        result_repr = linebreaksbr(result_repr, autoescape=True)
             else:
                 if isinstance(f.rel, ManyToManyRel) and value is not None:
                     result_repr = ", ".join(map(six.text_type, value.all()))
diff --git a/django/db/backends/schema.py b/django/db/backends/schema.py
index 54e6e06..4a5637c 100644
--- a/django/db/backends/schema.py
+++ b/django/db/backends/schema.py
@@ -675,9 +675,9 @@ class BaseDatabaseSchemaEditor(object):
                 }
             )
         # Does it have a foreign key?
-        if new_field.rel and \
-           (fks_dropped or (old_field.rel and not old_field.db_constraint)) and \
-           new_field.db_constraint:
+        if (new_field.rel and
+                (fks_dropped or not old_field.rel or not old_field.db_constraint) and
+                new_field.db_constraint):
             self.execute(self._create_fk_sql(model, new_field, "_fk_%(to_table)s_%(to_column)s"))
         # Rebuild FKs that pointed to us if we previously had to drop them
         if old_field.primary_key and new_field.primary_key and old_type != new_type:
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 344b238..c4cf4b7 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -855,6 +855,9 @@ def create_many_related_manager(superclass, rel):
             )
         do_not_call_in_templates = True
 
+        def __str__(self):
+            return repr(self)
+
         def _build_remove_filters(self, removed_vals):
             filters = Q(**{self.source_field_name: self.related_val})
             # No need to add a subquery condition if removed_vals is a QuerySet without
diff --git a/docs/howto/auth-remote-user.txt b/docs/howto/auth-remote-user.txt
index dc96a98..8d1d44a 100644
--- a/docs/howto/auth-remote-user.txt
+++ b/docs/howto/auth-remote-user.txt
@@ -50,6 +50,18 @@ With this setup, ``RemoteUserMiddleware`` will detect the username in
 ``request.META['REMOTE_USER']`` and will authenticate and auto-login that user
 using the :class:`~django.contrib.auth.backends.RemoteUserBackend`.
 
+Be aware that this particular setup disables authentication with the default
+``ModelBackend``. This means that if the ``REMOTE_USER`` value is not set
+then the user is unable to log in, even using Django's admin interface.
+Adding ``'django.contrib.auth.backends.ModelBackend'`` to the
+``AUTHENTICATION_BACKENDS`` list will use ``ModelBackend`` as a fallback
+if ``REMOTE_USER`` is absent, which will solve these issues.
+
+Django's user management, such as the views in ``contrib.admin`` and
+the :djadmin:`createsuperuser` management command, doesn't integrate with
+remote users. These interfaces work with users stored in the database
+regardless of ``AUTHENTICATION_BACKENDS``.
+
 .. note::
    Since the ``RemoteUserBackend`` inherits from ``ModelBackend``, you will
    still have all of the same permissions checking that is implemented in
diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt
index 8132634..00bf7b1 100644
--- a/docs/howto/custom-template-tags.txt
+++ b/docs/howto/custom-template-tags.txt
@@ -290,7 +290,7 @@ Template filter code falls into one of two situations:
 
    Be careful, though. You need to do more than just mark the output as
    safe. You need to ensure it really *is* safe, and what you do depends on
-   whether auto-escaping is in effect. The idea is to write filters than
+   whether auto-escaping is in effect. The idea is to write filters that
    can operate in templates where auto-escaping is either on or off in
    order to make things easier for your template authors.
 
diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt
index 042555e..d994ef1 100644
--- a/docs/howto/deployment/wsgi/modwsgi.txt
+++ b/docs/howto/deployment/wsgi/modwsgi.txt
@@ -83,7 +83,9 @@ your Python path as well. To do this, add an additional path to your
 ``WSGIPythonPath`` directive, with multiple paths separated by a colon (``:``)
 if using a UNIX-like system, or a semicolon (``;``) if using Windows. If any
 part of a directory path contains a space character, the complete argument
-string to ``WSGIPythonPath`` must be quoted::
+string to ``WSGIPythonPath`` must be quoted:
+
+.. code-block:: apache
 
     WSGIPythonPath /path/to/mysite.com:/path/to/your/venv/lib/python2.X/site-packages
 
@@ -103,7 +105,9 @@ Django instance to run in it, you will need to add appropriate
 ``WSGIDaemonProcess`` and ``WSGIProcessGroup`` directives. A further change
 required to the above configuration if you use daemon mode is that you can't
 use ``WSGIPythonPath``; instead you should use the ``python-path`` option to
-``WSGIDaemonProcess``, for example::
+``WSGIDaemonProcess``, for example:
+
+.. code-block:: apache
 
     WSGIDaemonProcess example.com python-path=/path/to/mysite.com:/path/to/venv/lib/python2.7/site-packages
     WSGIProcessGroup example.com
@@ -137,7 +141,9 @@ static media, and others using the mod_wsgi interface to Django.
 This example sets up Django at the site root, but explicitly serves
 ``robots.txt``, ``favicon.ico``, any CSS file, and anything in the
 ``/static/`` and ``/media/`` URL space as a static file. All other URLs
-will be served using mod_wsgi::
+will be served using mod_wsgi:
+
+.. code-block:: apache
 
     Alias /robots.txt /path/to/mysite.com/static/robots.txt
     Alias /favicon.ico /path/to/mysite.com/static/favicon.ico
diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt
index 0a80c84..6f4a307 100644
--- a/docs/intro/tutorial01.txt
+++ b/docs/intro/tutorial01.txt
@@ -670,8 +670,8 @@ of this object. Let's fix that by editing the ``Question`` model (in the
             return self.choice_text
 
 It's important to add :meth:`~django.db.models.Model.__str__` methods to your
-models, not only for your own sanity when dealing with the interactive prompt,
-but also because objects' representations are used throughout Django's
+models, not only for your own convenience when dealing with the interactive
+prompt, but also because objects' representations are used throughout Django's
 automatically-generated admin.
 
 .. admonition:: ``__str__`` or ``__unicode__``?
diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt
index c772443..d9b131e 100644
--- a/docs/ref/class-based-views/generic-date-based.txt
+++ b/docs/ref/class-based-views/generic-date-based.txt
@@ -256,7 +256,6 @@ MonthArchiveView
         class ArticleMonthArchiveView(MonthArchiveView):
             queryset = Article.objects.all()
             date_field = "pub_date"
-            make_object_list = True
             allow_future = True
 
     **Example myapp/urls.py**::
@@ -349,7 +348,6 @@ WeekArchiveView
         class ArticleWeekArchiveView(WeekArchiveView):
             queryset = Article.objects.all()
             date_field = "pub_date"
-            make_object_list = True
             week_format = "%W"
             allow_future = True
 
@@ -464,7 +462,6 @@ DayArchiveView
         class ArticleDayArchiveView(DayArchiveView):
             queryset = Article.objects.all()
             date_field = "pub_date"
-            make_object_list = True
             allow_future = True
 
     **Example myapp/urls.py**::
@@ -538,7 +535,6 @@ TodayArchiveView
         class ArticleTodayArchiveView(TodayArchiveView):
             queryset = Article.objects.all()
             date_field = "pub_date"
-            make_object_list = True
             allow_future = True
 
     **Example myapp/urls.py**::
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index a68b302..823e958 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -2608,6 +2608,8 @@ get_media_prefix
 Similar to the :ttag:`get_static_prefix`, ``get_media_prefix`` populates a
 template variable with the media prefix :setting:`MEDIA_URL`, e.g.::
 
-    <script type="text/javascript" charset="utf-8">
-    var media_path = '{% get_media_prefix %}';
-    </script>
+    {% load static %}
+    <body data-media-url="{% get_media_prefix %}">
+
+By storing the value in a data attribute, we ensure it's escaped appropriately
+if we want to use it in a JavaScript context.
diff --git a/docs/releases/1.7.6.txt b/docs/releases/1.7.6.txt
new file mode 100644
index 0000000..7f54da1
--- /dev/null
+++ b/docs/releases/1.7.6.txt
@@ -0,0 +1,30 @@
+==========================
+Django 1.7.6 release notes
+==========================
+
+*March 9, 2015*
+
+Django 1.7.6 fixes a security issue and several bugs in 1.7.5.
+
+Mitigated an XSS attack via properties in ``ModelAdmin.readonly_fields``
+========================================================================
+
+The :attr:`ModelAdmin.readonly_fields
+<django.contrib.admin.ModelAdmin.readonly_fields>` attribute in the Django
+admin allows displaying model fields and model attributes. While the former
+were correctly escaped, the latter were not. Thus untrusted content could be
+injected into the admin, presenting an exploitation vector for XSS attacks.
+
+In this vulnerability, every model attribute used in ``readonly_fields`` that
+is not an actual model field (e.g. a :class:`property`) will **fail to be
+escaped** even if that attribute is not marked as safe. In this release,
+autoescaping is now correctly applied.
+
+Bugfixes
+========
+
+* Fixed crash when coercing ``ManyRelatedManager`` to a string
+  (:ticket:`24352`).
+
+* Fixed a bug that prevented migrations from adding a foreign key constraint
+  when converting an existing field to a foreign key (:ticket:`24447`).
diff --git a/docs/releases/index.txt b/docs/releases/index.txt
index d1dd0bf..e1cf808 100644
--- a/docs/releases/index.txt
+++ b/docs/releases/index.txt
@@ -25,6 +25,7 @@ versions of the documentation contain the release notes for any later releases.
 .. toctree::
    :maxdepth: 1
 
+   1.7.6
    1.7.5
    1.7.4
    1.7.3
diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
index ba9516d..89af55f 100644
--- a/docs/topics/auth/default.txt
+++ b/docs/topics/auth/default.txt
@@ -356,8 +356,8 @@ If you have an authenticated user you want to attach to the current session
 
 .. admonition:: Calling ``authenticate()`` first
 
-    When you're manually logging a user in, you *must* call
-    :func:`~django.contrib.auth.authenticate()` before you call
+    When you're manually logging a user in, you *must* successfully authenticate
+    the user with :func:`~django.contrib.auth.authenticate()` before you call
     :func:`~django.contrib.auth.login()`.
     :func:`~django.contrib.auth.authenticate()`
     sets an attribute on the :class:`~django.contrib.auth.models.User` noting
@@ -664,18 +664,78 @@ Django provides several views that you can use for handling login, logout, and
 password management. These make use of the :ref:`stock auth forms
 <built-in-auth-forms>` but you can pass in your own forms as well.
 
-Django provides no default template for the authentication views - however the
-template context is documented for each view below.
+Django provides no default template for the authentication views. You should
+create your own templates for the views you want to use. The template context
+is documented in each view, see :ref:`all-authentication-views`.
 
-The built-in views all return
-a :class:`~django.template.response.TemplateResponse` instance, which allows
-you to easily customize the response data before rendering.  For more details,
-see the :doc:`TemplateResponse documentation </ref/template-response>`.
+.. _using-the-views:
 
-Most built-in authentication views provide a URL name for easier reference. See
-:doc:`the URL documentation </topics/http/urls>` for details on using named URL
-patterns.
+Using the views
+~~~~~~~~~~~~~~~
 
+There are different methods to implement these views in your project. The
+easiest way is to include the provided URLconf in ``django.contrib.auth.urls``
+in your own URLconf, for example::
+
+    urlpatterns = [
+        url('^', include('django.contrib.auth.urls'))
+    ]
+
+This will include the following URL patterns::
+
+    ^login/$ [name='login']
+    ^logout/$ [name='logout']
+    ^password_change/$ [name='password_change']
+    ^password_change/done/$ [name='password_change_done']
+    ^password_reset/$ [name='password_reset']
+    ^password_reset/done/$ [name='password_reset_done']
+    ^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$ [name='password_reset_confirm']
+    ^reset/done/$ [name='password_reset_complete']
+
+The views provide a URL name for easier reference. See :doc:`the URL
+documentation </topics/http/urls>` for details on using named URL patterns.
+
+If you want more control over your URLs, you can reference a specific view in
+your URLconf::
+
+    urlpatterns = [
+        url('^change-password/', 'django.contrib.auth.views.password_change')
+    ]
+
+The views have optional arguments you can use to alter the behavior of the
+view. For example, if you want to change the template name a view uses, you can
+provide the ``template_name`` argument. A way to do this is to provide keyword
+arguments in the URLconf, these will be passed on to the view. For example::
+
+    urlpatterns = [
+        url(
+            '^change-password/',
+            'django.contrib.auth.views.password_change',
+            {'template_name': 'change-password.html'}
+        )
+    ]
+
+All views return a :class:`~django.template.response.TemplateResponse`
+instance, which allows you to easily customize the response data before
+rendering. A way to do this is to wrap a view in your own view::
+
+    from django.contrib.auth import views
+
+    def change_password(request):
+        template_response = views.password_change(request)
+        # Do something with `template_response`
+        return template_response
+
+For more details, see the :doc:`TemplateResponse documentation
+</ref/template-response>`.
+
+.. _all-authentication-views:
+
+All authentication views
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is a list with all the views ``django.contrib.auth`` provides. For
+implementation details see :ref:`using-the-views`.
 
 .. function:: login(request, [template_name, redirect_field_name, authentication_form, current_app, extra_context])
 
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
index 57934df..223f740 100644
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -232,7 +232,7 @@ Overriding the clean() method
 You can override the ``clean()`` method on a model form to provide additional
 validation in the same way you can on a normal form.
 
-A model form instance bound to a model object will contain an ``instance``
+A model form instance attached to a model object will contain an ``instance``
 attribute that gives its methods access to that specific model instance.
 
 .. warning::
@@ -681,6 +681,21 @@ There are a couple of things to note, however.
   a default field. To opt-out from default fields, see
   :ref:`controlling-fields-with-fields-and-exclude`.
 
+Providing initial values
+------------------------
+
+As with regular forms, it's possible to specify initial data for forms by
+specifying an ``initial`` parameter when instantiating the form. Initial
+values provided this way will override both initial values from the form field
+and values from an attached model instance. For example::
+
+    >>> article = Article.objects.get(pk1=)
+    >>> article.headline
+    'My headline'
+    >>> form = ArticleForm(initial={'headline': 'Initial headline'), instance=article)
+    >>> form['pub_date'].value()
+    'Initial headline'
+
 .. _modelforms-factory:
 
 ModelForm factory function
@@ -853,8 +868,8 @@ As with regular formsets, it's possible to :ref:`specify initial data
 <formsets-initial-data>` for forms in the formset by specifying an ``initial``
 parameter when instantiating the model formset class returned by
 :func:`~django.forms.models.modelformset_factory`. However, with model
-formsets, the initial values only apply to extra forms, those that aren't bound
-to an existing object instance.
+formsets, the initial values only apply to extra forms, those that aren't
+attached to an existing model instance.
 
 .. _saving-objects-in-the-formset:
 
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index 9a05df6..15c1384 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -860,7 +860,7 @@ class GetFormsetsArgumentCheckingAdmin(admin.ModelAdmin):
 site = admin.AdminSite(name="admin")
 site.register(Article, ArticleAdmin)
 site.register(CustomArticle, CustomArticleAdmin)
-site.register(Section, save_as=True, inlines=[ArticleInline])
+site.register(Section, save_as=True, inlines=[ArticleInline], readonly_fields=['name_property'])
 site.register(ModelWithStringPrimaryKey)
 site.register(Color)
 site.register(Thing, ThingAdmin)
diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
index 341b7ae..f41b4de 100644
--- a/tests/admin_views/models.py
+++ b/tests/admin_views/models.py
@@ -22,6 +22,13 @@ class Section(models.Model):
     """
     name = models.CharField(max_length=100)
 
+    @property
+    def name_property(self):
+        """
+        A property that simply returns the name. Used to test #24461
+        """
+        return self.name
+
 
 @python_2_unicode_compatible
 class Article(models.Model):
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 96da7c4..e610642 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -3862,6 +3862,15 @@ class ReadonlyTest(TestCase):
         self.assertContains(response, '<label for="id_public">Overridden public label:</label>', html=True)
         self.assertNotContains(response, "Some help text for the date (with unicode ŠĐĆŽćžšđ)")
 
+    def test_correct_autoescaping(self):
+        """
+        Make sure that non-field readonly elements are properly autoescaped (#24461)
+        """
+        section = Section.objects.create(name='<a>evil</a>')
+        response = self.client.get(reverse('admin:admin_views_section_change', args=(section.pk,)))
+        self.assertNotContains(response, "<a>evil</a>", status_code=200)
+        self.assertContains(response, "<a>evil</a>", status_code=200)
+
 
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
 class LimitChoicesToInAdminTest(TestCase):
diff --git a/tests/m2m_regress/tests.py b/tests/m2m_regress/tests.py
index d1f85ac..a1431d0 100644
--- a/tests/m2m_regress/tests.py
+++ b/tests/m2m_regress/tests.py
@@ -109,3 +109,7 @@ class M2MRegressionTests(TestCase):
         worksheet.lines = hi
         self.assertEqual(1, worksheet.lines.count())
         self.assertEqual(1, hi.count())
+
+    def test_many_related_manager_str(self):
+        worksheet = Worksheet.objects.create(id=1)
+        self.assertIn('ManyRelatedManager', str(worksheet.lines))
diff --git a/tests/schema/models.py b/tests/schema/models.py
index 6eba7cc..b933175 100644
--- a/tests/schema/models.py
+++ b/tests/schema/models.py
@@ -87,6 +87,15 @@ class BookWithM2M(models.Model):
         apps = new_apps
 
 
+class BookWithoutFK(models.Model):
+    author = models.IntegerField()
+    title = models.CharField(max_length=100, db_index=True)
+    pub_date = models.DateTimeField()
+
+    class Meta:
+        apps = new_apps
+
+
 class TagThrough(models.Model):
     book = models.ForeignKey("schema.BookWithM2MThrough")
     tag = models.ForeignKey("schema.TagM2MTest")
diff --git a/tests/schema/tests.py b/tests/schema/tests.py
index 996cec1..cc95d55 100644
--- a/tests/schema/tests.py
+++ b/tests/schema/tests.py
@@ -11,7 +11,7 @@ from .fields import CustomManyToManyField, InheritedManyToManyField
 from .models import (Author, AuthorWithDefaultHeight, AuthorWithM2M, Book, BookWithLongName,
     BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename,
     UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorTag, AuthorWithM2MThrough,
-    AuthorWithEvenLongerName, BookWeak, Note, BookWithO2O)
+    AuthorWithEvenLongerName, BookWeak, Note, BookWithO2O, BookWithoutFK)
 
 
 class SchemaTests(TransactionTestCase):
@@ -29,7 +29,7 @@ class SchemaTests(TransactionTestCase):
         Author, AuthorWithM2M, Book, BookWithLongName, BookWithSlug,
         BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, UniqueTest,
         Thing, TagThrough, BookWithM2MThrough, AuthorWithEvenLongerName,
-        BookWeak, BookWithO2O,
+        BookWeak, BookWithO2O, BookWithoutFK,
     ]
 
     # Utility functions
@@ -537,6 +537,38 @@ class SchemaTests(TransactionTestCase):
             self.fail("No FK constraint for author_id found")
 
     @unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support")
+    def test_alter_to_fk(self):
+        """
+        #24447 - Tests adding a FK constraint for an existing column
+        """
+        # Create the tables
+        with connection.schema_editor() as editor:
+            editor.create_model(Author)
+            editor.create_model(BookWithoutFK)
+        # Ensure no FK constraint exists
+        constraints = self.get_constraints(BookWithoutFK._meta.db_table)
+        for name, details in constraints.items():
+            if details['foreign_key']:
+                self.fail('Found an unexpected FK constraint to %s' % details['columns'])
+        new_field = ForeignKey(Author)
+        new_field.set_attributes_from_name("author")
+        with connection.schema_editor() as editor:
+            editor.alter_field(
+                BookWithoutFK,
+                BookWithoutFK._meta.get_field_by_name("author")[0],
+                new_field,
+                strict=True,
+            )
+        constraints = self.get_constraints(BookWithoutFK._meta.db_table)
+        # Ensure FK constraint exists
+        for name, details in constraints.items():
+            if details['foreign_key'] and details['columns'] == ["author_id"]:
+                self.assertEqual(details['foreign_key'], ('schema_author', 'id'))
+                break
+        else:
+            self.fail("No FK constraint for author_id found")
+
+    @unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support")
     def test_alter_o2o_to_fk(self):
         """
         #24163 - Tests altering of OneToOneField to ForeignKey

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-django.git



More information about the Python-modules-commits mailing list