Skip to content

Commit

Permalink
Fix FileFields in deeply nested inlines
Browse files Browse the repository at this point in the history
Django 2.1+ implemented 'has_file_field', this value no longer is
hardcoded to 'True', but depends on 'is_multipart()'.

Django relies on 'has_file_field' to set form's enctype to multipart.
With false negatives in 'has_file_field', FileField changes are not
saved.

This commit adds support for nested_formsets to 'is_multipart()'.
  • Loading branch information
btknu authored and fdintino committed Mar 12, 2019
1 parent a8c50e3 commit 1a0dc3c
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 0 deletions.
52 changes: 52 additions & 0 deletions nested_admin/formsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,69 @@ def mutable_querydict(qd):
qd._mutable = orig_mutable


PATCH_FORM_IS_MULTIPART = ((2, 1) < django.VERSION < (3, 0))


class FixDjango2MultipartFormMixin(object):
def is_multipart(self, check_formset=True):
"""
Overridden is_multipart for Django 2.1 and 2.2 that returns the
formset's is_multipart by default.
Parameters
----------
check_formset : bool (default=True)
If ``False``, returns the form's original is_multipart value.
Exists to prevent infinite recursion in the formset's is_multipart
lookup.
"""
parent_formset = getattr(self, 'parent_formset', None)
if check_formset and parent_formset:
return parent_formset.is_multipart()
else:
return super(FixDjango2MultipartFormMixin, self).is_multipart()


class NestedInlineFormSetMixin(object):

is_nested = False

def __init__(self, *args, **kwargs):
super(NestedInlineFormSetMixin, self).__init__(*args, **kwargs)
if PATCH_FORM_IS_MULTIPART:
self.form = type(
self.form.__name__, (FixDjango2MultipartFormMixin, self.form), {
'parent_formset': self,
})

def _construct_form(self, i, **kwargs):
defaults = {}
if '-empty-' in self.prefix:
defaults['empty_permitted'] = True
defaults.update(kwargs)
return super(NestedInlineFormSetMixin, self)._construct_form(i, **defaults)

def is_multipart(self):
if not PATCH_FORM_IS_MULTIPART:
if super(NestedInlineFormSetMixin, self).is_multipart():
return True
else:
forms = [f for f in self]
if not forms:
if hasattr(type(self), 'empty_forms'):
forms = self.empty_forms # django-polymorphic compat
else:
forms = [self.empty_form]
for form in forms:
if form.is_multipart(check_formset=False):
return True

for nested_formset in getattr(self, 'nested_formsets', []):
if nested_formset.is_multipart():
return True

return False

def save(self, commit=True):
"""
Saves model instances for every form, adding and changing instances
Expand Down
1 change: 1 addition & 0 deletions nested_admin/tests/two_deep/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class Meta:

class StackedItem(ItemAbstract):
section = ForeignKey(StackedSection, related_name='item_set', on_delete=CASCADE)
upload = models.FileField(blank=True, null=True, upload_to='foo')

class Meta:
ordering = ('section', 'position')
Expand Down
25 changes: 25 additions & 0 deletions nested_admin/tests/two_deep/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os
import tempfile
import time
from unittest import skipIf, SkipTest

Expand Down Expand Up @@ -842,6 +844,29 @@ def test_add_item_inline_label_update(self):
inline_label = self.get_item([0, 1]).find_element_by_class_name('inline_label')
self.assertEqual(inline_label.text, '#2')

def test_upload_file(self):
group = self.root_model.objects.create(slug='group')

self.load_admin(group)

self.add_inline(slug="a")
self.add_inline(indexes=[0], name='A 0')

fd, path = tempfile.mkstemp()
try:
with os.fdopen(fd, 'w') as tmp:
tmp.write('Test file. Used as a payload for testing file uploads.')
with self.clickable_xpath('//input[@name="section_set-0-item_set-0-upload"]') as el:
el.send_keys(path)
self.save_form()
finally:
os.remove(path)

item_a_0 = self.item_cls.objects.get(name='A 0')
upload_name = 'foo/' + os.path.basename(path)

self.assertEqual(item_a_0.upload.name, upload_name, 'File upload failed')


class TestTabularInlineAdmin(InlineAdminTestCaseMixin, BaseNestedAdminTestCase):

Expand Down

0 comments on commit 1a0dc3c

Please sign in to comment.