From 02c07be95c47efaab9da7422c33ee76142f11336 Mon Sep 17 00:00:00 2001
From: Tim Graham <timograham@gmail.com>
Date: Mon, 31 Dec 2018 09:18:26 -0500
Subject: [PATCH] Fixed #30050 -- Fixed
 InlineModelAdmin.has_change_permission() called with non-None obj during add.

Thanks andreage for the report and suggested fix.
---
 django/contrib/admin/options.py |  2 +-
 docs/releases/2.1.5.txt         |  4 ++++
 tests/admin_views/tests.py      | 19 +++++++++++++++++++
 3 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 532f6ec1f3..e5d79d2c6d 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1956,7 +1956,7 @@ class ModelAdmin(BaseModelAdmin):
 
             # Bypass validation of each view-only inline form (since the form's
             # data won't be in request.POST), unless the form was deleted.
-            if not inline.has_change_permission(request, obj):
+            if not inline.has_change_permission(request, obj if change else None):
                 for index, form in enumerate(formset.initial_forms):
                     if user_deleted_form(request, obj, formset, index):
                         continue
diff --git a/docs/releases/2.1.5.txt b/docs/releases/2.1.5.txt
index 73850e01b5..27ffbc7510 100644
--- a/docs/releases/2.1.5.txt
+++ b/docs/releases/2.1.5.txt
@@ -22,3 +22,7 @@ Bugfixes
 * Fixed a regression in Django 2.1.4 (which enabled keep-alive connections)
   where request body data isn't properly consumed for such connections
   (:ticket:`30015`).
+
+* Fixed a regression in Django 2.1.4 where
+  ``InlineModelAdmin.has_change_permission()`` is incorrectly called with a
+  non-``None`` ``obj`` argument during an object add (:ticket:`30050`).
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 8f15642ecf..d4bd873d2a 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -2,6 +2,7 @@ import datetime
 import os
 import re
 import unittest
+from unittest import mock
 from urllib.parse import parse_qsl, urljoin, urlparse
 
 import pytz
@@ -1733,6 +1734,24 @@ class AdminViewPermissionsTest(TestCase):
         # make sure the view removes test cookie
         self.assertIs(self.client.session.test_cookie_worked(), False)
 
+    @mock.patch('django.contrib.admin.options.InlineModelAdmin.has_change_permission')
+    def test_add_view_with_view_only_inlines(self, has_change_permission):
+        """User with add permission to a section but view-only for inlines."""
+        self.viewuser.user_permissions.add(get_perm(Section, get_permission_codename('add', Section._meta)))
+        self.client.force_login(self.viewuser)
+        # Valid POST creates a new section.
+        data = {
+            'name': 'New obj',
+            'article_set-TOTAL_FORMS': 0,
+            'article_set-INITIAL_FORMS': 0,
+        }
+        response = self.client.post(reverse('admin:admin_views_section_add'), data)
+        self.assertRedirects(response, reverse('admin:index'))
+        self.assertEqual(Section.objects.latest('id').name, data['name'])
+        # InlineModelAdmin.has_change_permission()'s obj argument is always
+        # None during object add.
+        self.assertEqual([obj for (request, obj), _ in has_change_permission.call_args_list], [None, None])
+
     def test_change_view(self):
         """Change view should restrict access and allow users to edit items."""
         change_dict = {'title': 'Ikke fordømt',