Skip to content

Commit

Permalink
ensure order of formset media. fixes #71
Browse files Browse the repository at this point in the history
  • Loading branch information
fdintino committed Aug 13, 2018
1 parent 6f6f4f5 commit 3bd7aaf
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 42 deletions.
88 changes: 88 additions & 0 deletions nested_admin/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""
A backport of the fix in Django 2.0 that retains the order of form media.
See https://github.com/django/django/commit/c19b56f633e172b3c02094cbe12d28865ee57772
and https://code.djangoproject.com/ticket/28377
"""
import warnings

import django.forms

try:
from django.forms.widgets import MediaOrderConflictWarning
except ImportError:
class MediaOrderConflictWarning(RuntimeWarning):
pass

MergeSafeMedia = None
else:
MergeSafeMedia = django.forms.Media


if MergeSafeMedia is None:
class MergeSafeMedia(django.forms.Media):
def __init__(self, media=None, css=None, js=None):
if media is not None:
css = getattr(media, '_css', {})
js = getattr(media, '_js', [])
else:
if css is None:
css = {}
if js is None:
js = []
self._css = css
self._js = js

@staticmethod
def merge(list_1, list_2):
"""
Merge two lists while trying to keep the relative order of the elements.
Warn if the lists have the same two elements in a different relative
order.
For static assets it can be important to have them included in the DOM
in a certain order. In JavaScript you may not be able to reference a
global or in CSS you might want to override a style.
"""
# Start with a copy of list_1.
combined_list = list(list_1)
last_insert_index = len(list_1)
# Walk list_2 in reverse, inserting each element into combined_list if
# it doesn't already exist.
for path in reversed(list_2):
try:
# Does path already exist in the list?
index = combined_list.index(path)
except ValueError:
# Add path to combined_list since it doesn't exist.
combined_list.insert(last_insert_index, path)
else:
if index > last_insert_index:
warnings.warn(
'Detected duplicate Media files in an opposite order:\n'
'%s\n%s' % (combined_list[last_insert_index], combined_list[index]),
MediaOrderConflictWarning,
)
# path already exists in the list. Update last_insert_index so
# that the following elements are inserted in front of this one.
last_insert_index = index
return combined_list

def __add__(self, other):
combined = MergeSafeMedia()
combined._js = self.merge(self._js, other._js)
combined._css = {
medium: self.merge(self._css.get(medium, []), other._css.get(medium, []))
for medium in set(self._css.keys()) | set(other._css.keys())
}
return combined

def add_js(self, data):
if data:
new_media = self + MergeSafeMedia(js=data)
self._js = new_media._js

def add_css(self, data):
if data:
new_media = self + MergeSafeMedia(css=data)
self._css = new_media._css
8 changes: 4 additions & 4 deletions nested_admin/nested.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
except ImportError:
# Django <= 1.9
from django.core.urlresolvers import reverse
from django import forms
from django.template.defaultfilters import capfirst
from django.utils import six
from django.utils.six.moves import zip
from django.utils.translation import ugettext
from django.contrib.admin.options import ModelAdmin, InlineModelAdmin

from .compat import MergeSafeMedia
from .formsets import NestedInlineFormSet, NestedBaseGenericInlineFormSet
from django.contrib.admin.options import ModelAdmin, InlineModelAdmin


__all__ = (
Expand Down Expand Up @@ -63,7 +63,7 @@ def __iter__(self):
yield inline_admin_form

def _media(self):
media = self.opts.media + self.formset.media
media = MergeSafeMedia(self.formset.media) + self.opts.media
for fs in self:
media = media + fs.media
for inline in (getattr(fs.form, 'inlines', None) or []):
Expand All @@ -73,7 +73,7 @@ def _media(self):
static_url = staticfiles_storage.url
server_data_js = reverse('nesting_server_data')
min_ext = '' if getattr(settings, 'NESTED_ADMIN_DEBUG', False) else '.min'
return media + forms.Media(
return media + MergeSafeMedia(
js=(
server_data_js,
static_url('nested_admin/dist/nested_admin%s.js' % min_ext),
Expand Down
42 changes: 41 additions & 1 deletion nested_admin/tests/admin_widgets/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from .models import (
TestAdminWidgetsRoot, TestAdminWidgetsM2M, TestAdminWidgetsRelated1,
TestAdminWidgetsRelated2, TestAdminWidgetsA, TestAdminWidgetsB,
TestAdminWidgetsC0, TestAdminWidgetsC1)
TestAdminWidgetsC0, TestAdminWidgetsC1,
TestWidgetMediaOrderRoot, TestWidgetMediaOrderA, TestWidgetMediaOrderB,
TestWidgetMediaOrderC0, TestWidgetMediaOrderC1)


class TestAdminWidgetsC0Inline(NestedStackedInline):
Expand Down Expand Up @@ -60,3 +62,41 @@ class TestAdminWidgetsRootAdmin(NestedModelAdmin):
admin.site.register(TestAdminWidgetsRelated1, NestedModelAdmin)
admin.site.register(TestAdminWidgetsRelated2, NestedModelAdmin)
admin.site.register(TestAdminWidgetsM2M, NestedModelAdmin)


class TestWidgetMediaOrderC0Inline(NestedStackedInline):
model = TestWidgetMediaOrderC0
sortable_field_name = "position"
extra = 0
inline_classes = ("collapse", "open", "grp-collapse", "grp-open",)


class TestWidgetMediaOrderC1Inline(NestedStackedInline):
model = TestWidgetMediaOrderC1
prepopulated_fields = {'slug': ('name', )}
filter_horizontal = ['m2m']
extra = 0
inline_classes = ("collapse", "open", "grp-collapse", "grp-open",)
raw_id_fields = ['fk2']
autocomplete_lookup_fields = {'fk': ['fk2']}


class TestWidgetMediaOrderBInline(NestedStackedInline):
model = TestWidgetMediaOrderB
inlines = [TestWidgetMediaOrderC0Inline, TestWidgetMediaOrderC1Inline]
sortable_field_name = "position"
extra = 1
inline_classes = ("collapse", "open", "grp-collapse", "grp-open",)


class TestWidgetMediaOrderAInline(NestedStackedInline):
model = TestWidgetMediaOrderA
inlines = [TestWidgetMediaOrderBInline]
sortable_field_name = "position"
extra = 1
inline_classes = ("collapse", "open", "grp-collapse", "grp-open",)


@admin.register(TestWidgetMediaOrderRoot)
class TestWidgetMediaOrderRootAdmin(NestedModelAdmin):
inlines = [TestWidgetMediaOrderAInline]
89 changes: 89 additions & 0 deletions nested_admin/tests/admin_widgets/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def __str__(self):
return self.name


@python_2_unicode_compatible
class TestAdminWidgetsA(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
Expand All @@ -53,7 +54,11 @@ class TestAdminWidgetsA(models.Model):
class Meta:
ordering = ('position', )

def __str__(self):
return self.name


@python_2_unicode_compatible
class TestAdminWidgetsB(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
Expand All @@ -70,7 +75,12 @@ class TestAdminWidgetsB(models.Model):
class Meta:
ordering = ('position', )

def __str__(self):
parent_name = self.parent.name if self.parent else '?'
return "%s - %s" % (parent_name, self.name)


@python_2_unicode_compatible
class TestAdminWidgetsC0(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
Expand All @@ -87,7 +97,12 @@ class TestAdminWidgetsC0(models.Model):
class Meta:
ordering = ('position', )

def __str__(self):
parent_name = self.parent.name if self.parent else '?'
return "%s - %s" % (parent_name, self.name)


@python_2_unicode_compatible
class TestAdminWidgetsC1(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
Expand All @@ -103,3 +118,77 @@ class TestAdminWidgetsC1(models.Model):

class Meta:
ordering = ('position', )

def __str__(self):
parent_name = self.parent.name if self.parent else '?'
return "%s - %s" % (parent_name, self.name)


class TestWidgetMediaOrderRoot(models.Model):
name = models.CharField(max_length=200)


@python_2_unicode_compatible
class TestWidgetMediaOrderA(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
parent = ForeignKey(TestWidgetMediaOrderRoot, on_delete=CASCADE)
position = models.PositiveIntegerField()

class Meta:
ordering = ('position', )

def __str__(self):
return self.name


@python_2_unicode_compatible
class TestWidgetMediaOrderB(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
parent = ForeignKey(TestWidgetMediaOrderA, on_delete=CASCADE)
position = models.PositiveIntegerField()

class Meta:
ordering = ('position', )

def __str__(self):
parent_name = self.parent.name if self.parent else '?'
return "%s - %s" % (parent_name, self.name)


@python_2_unicode_compatible
class TestWidgetMediaOrderC0(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
parent = ForeignKey(TestWidgetMediaOrderB, on_delete=CASCADE)
position = models.PositiveIntegerField()

class Meta:
ordering = ('position', )

def __str__(self):
parent_name = self.parent.name if self.parent else '?'
return "%s - %s" % (parent_name, self.name)


@python_2_unicode_compatible
class TestWidgetMediaOrderC1(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
parent = ForeignKey(TestWidgetMediaOrderB, on_delete=CASCADE)
position = models.PositiveIntegerField()
date = models.DateTimeField(blank=True, null=True)
upload = models.FileField(blank=True, null=True, upload_to='foo')
fk1 = models.ForeignKey(TestAdminWidgetsRelated1, blank=True, null=True,
on_delete=CASCADE, related_name='+')
fk2 = models.ForeignKey(TestAdminWidgetsRelated1, blank=True, null=True,
on_delete=CASCADE, related_name='+')
m2m = models.ManyToManyField(TestAdminWidgetsM2M, blank=True)

class Meta:
ordering = ('position', )

def __str__(self):
parent_name = self.parent.name if self.parent else '?'
return "%s - %s" % (parent_name, self.name)
Loading

0 comments on commit 3bd7aaf

Please sign in to comment.