parent
1135ca5741
commit
61a86e9a1d
29 changed files with 817 additions and 90 deletions
@ -0,0 +1,4 @@ |
||||
# -*- coding: utf-8 -*- |
||||
|
||||
|
||||
__version__ = '0.7.0' |
||||
@ -0,0 +1,353 @@ |
||||
# -*- coding: utf-8 -*- |
||||
from operator import attrgetter |
||||
import sys |
||||
from django.db import connections |
||||
from django.db import router |
||||
from django.db.models import signals |
||||
from django.db.models.fields.related import add_lazy_relation, create_many_related_manager |
||||
from django.db.models.fields.related import ManyToManyField, ReverseManyRelatedObjectsDescriptor |
||||
from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT |
||||
from django.conf import settings |
||||
from django.utils.functional import curry |
||||
from sortedm2m.forms import SortedMultipleChoiceField |
||||
|
||||
|
||||
if sys.version_info[0] < 3: |
||||
string_types = basestring |
||||
else: |
||||
string_types = str |
||||
|
||||
|
||||
SORT_VALUE_FIELD_NAME = 'sort_value' |
||||
|
||||
|
||||
def create_sorted_many_to_many_intermediate_model(field, klass): |
||||
from django.db import models |
||||
managed = True |
||||
if isinstance(field.rel.to, string_types) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT: |
||||
to_model = field.rel.to |
||||
to = to_model.split('.')[-1] |
||||
def set_managed(field, model, cls): |
||||
field.rel.through._meta.managed = model._meta.managed or cls._meta.managed |
||||
add_lazy_relation(klass, field, to_model, set_managed) |
||||
elif isinstance(field.rel.to, string_types): |
||||
to = klass._meta.object_name |
||||
to_model = klass |
||||
managed = klass._meta.managed |
||||
else: |
||||
to = field.rel.to._meta.object_name |
||||
to_model = field.rel.to |
||||
managed = klass._meta.managed or to_model._meta.managed |
||||
name = '%s_%s' % (klass._meta.object_name, field.name) |
||||
if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT or to == klass._meta.object_name: |
||||
from_ = 'from_%s' % to.lower() |
||||
to = 'to_%s' % to.lower() |
||||
else: |
||||
from_ = klass._meta.object_name.lower() |
||||
to = to.lower() |
||||
meta = type(str('Meta'), (object,), { |
||||
'db_table': field._get_m2m_db_table(klass._meta), |
||||
'managed': managed, |
||||
'auto_created': klass, |
||||
'app_label': klass._meta.app_label, |
||||
'unique_together': (from_, to), |
||||
'ordering': (field.sort_value_field_name,), |
||||
'verbose_name': '%(from)s-%(to)s relationship' % {'from': from_, 'to': to}, |
||||
'verbose_name_plural': '%(from)s-%(to)s relationships' % {'from': from_, 'to': to}, |
||||
}) |
||||
# Construct and return the new class. |
||||
def default_sort_value(name): |
||||
model = models.get_model(klass._meta.app_label, name) |
||||
return model._default_manager.count() |
||||
|
||||
default_sort_value = curry(default_sort_value, name) |
||||
|
||||
return type(str(name), (models.Model,), { |
||||
'Meta': meta, |
||||
'__module__': klass.__module__, |
||||
from_: models.ForeignKey(klass, related_name='%s+' % name), |
||||
to: models.ForeignKey(to_model, related_name='%s+' % name), |
||||
field.sort_value_field_name: models.IntegerField(default=default_sort_value), |
||||
'_sort_field_name': field.sort_value_field_name, |
||||
'_from_field_name': from_, |
||||
'_to_field_name': to, |
||||
}) |
||||
|
||||
|
||||
def create_sorted_many_related_manager(superclass, rel): |
||||
RelatedManager = create_many_related_manager(superclass, rel) |
||||
|
||||
class SortedRelatedManager(RelatedManager): |
||||
def get_query_set(self): |
||||
# We use ``extra`` method here because we have no other access to |
||||
# the extra sorting field of the intermediary model. The fields |
||||
# are hidden for joins because we set ``auto_created`` on the |
||||
# intermediary's meta options. |
||||
try: |
||||
return self.instance._prefetched_objects_cache[self.prefetch_cache_name] |
||||
except (AttributeError, KeyError): |
||||
return super(SortedRelatedManager, self).\ |
||||
get_query_set().\ |
||||
extra(order_by=['%s.%s' % ( |
||||
rel.through._meta.db_table, |
||||
rel.through._sort_field_name, |
||||
)]) |
||||
|
||||
if not hasattr(RelatedManager, '_get_fk_val'): |
||||
@property |
||||
def _fk_val(self): |
||||
return self._pk_val |
||||
|
||||
def get_prefetch_query_set(self, instances): |
||||
# mostly a copy of get_prefetch_query_set from ManyRelatedManager |
||||
# but with addition of proper ordering |
||||
db = self._db or router.db_for_read(instances[0].__class__, instance=instances[0]) |
||||
query = {'%s__pk__in' % self.query_field_name: |
||||
set(obj._get_pk_val() for obj in instances)} |
||||
qs = super(RelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**query) |
||||
|
||||
# M2M: need to annotate the query in order to get the primary model |
||||
# that the secondary model was actually related to. We know that |
||||
# there will already be a join on the join table, so we can just add |
||||
# the select. |
||||
|
||||
# For non-autocreated 'through' models, can't assume we are |
||||
# dealing with PK values. |
||||
fk = self.through._meta.get_field(self.source_field_name) |
||||
source_col = fk.column |
||||
join_table = self.through._meta.db_table |
||||
connection = connections[db] |
||||
qn = connection.ops.quote_name |
||||
qs = qs.extra(select={'_prefetch_related_val': |
||||
'%s.%s' % (qn(join_table), qn(source_col))}, |
||||
order_by=['%s.%s' % ( |
||||
rel.through._meta.db_table, |
||||
rel.through._sort_field_name, |
||||
)]) |
||||
select_attname = fk.rel.get_related_field().get_attname() |
||||
return (qs, |
||||
attrgetter('_prefetch_related_val'), |
||||
attrgetter(select_attname), |
||||
False, |
||||
self.prefetch_cache_name) |
||||
|
||||
def _add_items(self, source_field_name, target_field_name, *objs): |
||||
# source_field_name: the PK fieldname in join_table for the source object |
||||
# target_field_name: the PK fieldname in join_table for the target object |
||||
# *objs - objects to add. Either object instances, or primary keys of object instances. |
||||
|
||||
# If there aren't any objects, there is nothing to do. |
||||
from django.db.models import Model |
||||
if objs: |
||||
new_ids = [] |
||||
for obj in objs: |
||||
if isinstance(obj, self.model): |
||||
if not router.allow_relation(obj, self.instance): |
||||
raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % |
||||
(obj, self.instance._state.db, obj._state.db)) |
||||
if hasattr(self, '_get_fk_val'): # Django>=1.5 |
||||
fk_val = self._get_fk_val(obj, target_field_name) |
||||
if fk_val is None: |
||||
raise ValueError('Cannot add "%r": the value for field "%s" is None' % |
||||
(obj, target_field_name)) |
||||
new_ids.append(self._get_fk_val(obj, target_field_name)) |
||||
else: # Django<1.5 |
||||
new_ids.append(obj.pk) |
||||
elif isinstance(obj, Model): |
||||
raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj)) |
||||
else: |
||||
new_ids.append(obj) |
||||
db = router.db_for_write(self.through, instance=self.instance) |
||||
vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True) |
||||
vals = vals.filter(**{ |
||||
source_field_name: self._fk_val, |
||||
'%s__in' % target_field_name: new_ids, |
||||
}) |
||||
for val in vals: |
||||
if val in new_ids: |
||||
new_ids.remove(val) |
||||
_new_ids = [] |
||||
for pk in new_ids: |
||||
if pk not in _new_ids: |
||||
_new_ids.append(pk) |
||||
new_ids = _new_ids |
||||
new_ids_set = set(new_ids) |
||||
|
||||
if self.reverse or source_field_name == self.source_field_name: |
||||
# Don't send the signal when we are inserting the |
||||
# duplicate data row for symmetrical reverse entries. |
||||
signals.m2m_changed.send(sender=rel.through, action='pre_add', |
||||
instance=self.instance, reverse=self.reverse, |
||||
model=self.model, pk_set=new_ids_set, using=db) |
||||
# Add the ones that aren't there already |
||||
sort_field_name = self.through._sort_field_name |
||||
sort_field = self.through._meta.get_field_by_name(sort_field_name)[0] |
||||
for obj_id in new_ids: |
||||
self.through._default_manager.using(db).create(**{ |
||||
'%s_id' % source_field_name: self._fk_val, # Django 1.5 compatibility |
||||
'%s_id' % target_field_name: obj_id, |
||||
sort_field_name: sort_field.get_default(), |
||||
}) |
||||
if self.reverse or source_field_name == self.source_field_name: |
||||
# Don't send the signal when we are inserting the |
||||
# duplicate data row for symmetrical reverse entries. |
||||
signals.m2m_changed.send(sender=rel.through, action='post_add', |
||||
instance=self.instance, reverse=self.reverse, |
||||
model=self.model, pk_set=new_ids_set, using=db) |
||||
|
||||
return SortedRelatedManager |
||||
|
||||
|
||||
class ReverseSortedManyRelatedObjectsDescriptor(ReverseManyRelatedObjectsDescriptor): |
||||
@property |
||||
def related_manager_cls(self): |
||||
return create_sorted_many_related_manager( |
||||
self.field.rel.to._default_manager.__class__, |
||||
self.field.rel |
||||
) |
||||
|
||||
|
||||
class SortedManyToManyField(ManyToManyField): |
||||
''' |
||||
Providing a many to many relation that remembers the order of related |
||||
objects. |
||||
|
||||
Accept a boolean ``sorted`` attribute which specifies if relation is |
||||
ordered or not. Default is set to ``True``. If ``sorted`` is set to |
||||
``False`` the field will behave exactly like django's ``ManyToManyField``. |
||||
''' |
||||
def __init__(self, to, sorted=True, **kwargs): |
||||
self.sorted = sorted |
||||
self.sort_value_field_name = kwargs.pop( |
||||
'sort_value_field_name', |
||||
SORT_VALUE_FIELD_NAME) |
||||
super(SortedManyToManyField, self).__init__(to, **kwargs) |
||||
if self.sorted: |
||||
self.help_text = kwargs.get('help_text', None) |
||||
|
||||
def contribute_to_class(self, cls, name): |
||||
if not self.sorted: |
||||
return super(SortedManyToManyField, self).contribute_to_class(cls, name) |
||||
|
||||
# To support multiple relations to self, it's useful to have a non-None |
||||
# related name on symmetrical relations for internal reasons. The |
||||
# concept doesn't make a lot of sense externally ("you want me to |
||||
# specify *what* on my non-reversible relation?!"), so we set it up |
||||
# automatically. The funky name reduces the chance of an accidental |
||||
# clash. |
||||
if self.rel.symmetrical and (self.rel.to == "self" or self.rel.to == cls._meta.object_name): |
||||
self.rel.related_name = "%s_rel_+" % name |
||||
|
||||
super(ManyToManyField, self).contribute_to_class(cls, name) |
||||
|
||||
# The intermediate m2m model is not auto created if: |
||||
# 1) There is a manually specified intermediate, or |
||||
# 2) The class owning the m2m field is abstract. |
||||
if not self.rel.through and not cls._meta.abstract: |
||||
self.rel.through = create_sorted_many_to_many_intermediate_model(self, cls) |
||||
|
||||
# Add the descriptor for the m2m relation |
||||
setattr(cls, self.name, ReverseSortedManyRelatedObjectsDescriptor(self)) |
||||
|
||||
# Set up the accessor for the m2m table name for the relation |
||||
self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) |
||||
|
||||
# Populate some necessary rel arguments so that cross-app relations |
||||
# work correctly. |
||||
if isinstance(self.rel.through, string_types): |
||||
def resolve_through_model(field, model, cls): |
||||
field.rel.through = model |
||||
add_lazy_relation(cls, self, self.rel.through, resolve_through_model) |
||||
|
||||
if hasattr(cls._meta, 'duplicate_targets'): # Django<1.5 |
||||
if isinstance(self.rel.to, string_types): |
||||
target = self.rel.to |
||||
else: |
||||
target = self.rel.to._meta.db_table |
||||
cls._meta.duplicate_targets[self.column] = (target, "m2m") |
||||
|
||||
def formfield(self, **kwargs): |
||||
defaults = {} |
||||
if self.sorted: |
||||
defaults['form_class'] = SortedMultipleChoiceField |
||||
defaults.update(kwargs) |
||||
return super(SortedManyToManyField, self).formfield(**defaults) |
||||
|
||||
|
||||
# Add introspection rules for South database migrations |
||||
# See http://south.aeracode.org/docs/customfields.html |
||||
try: |
||||
import south |
||||
except ImportError: |
||||
south = None |
||||
|
||||
if south is not None and 'south' in settings.INSTALLED_APPS: |
||||
from south.modelsinspector import add_introspection_rules |
||||
add_introspection_rules( |
||||
[( |
||||
(SortedManyToManyField,), |
||||
[], |
||||
{"sorted": ["sorted", {"default": True}]}, |
||||
)], |
||||
[r'^sortedm2m\.fields\.SortedManyToManyField'] |
||||
) |
||||
|
||||
# Monkeypatch South M2M actions to create the sorted through model. |
||||
# FIXME: This doesn't detect if you changed the sorted argument to the field. |
||||
import south.creator.actions |
||||
from south.creator.freezer import model_key |
||||
|
||||
class AddM2M(south.creator.actions.AddM2M): |
||||
SORTED_FORWARDS_TEMPLATE = ''' |
||||
# Adding SortedM2M table for field %(field_name)s on '%(model_name)s' |
||||
db.create_table(%(table_name)r, ( |
||||
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), |
||||
(%(left_field)r, models.ForeignKey(orm[%(left_model_key)r], null=False)), |
||||
(%(right_field)r, models.ForeignKey(orm[%(right_model_key)r], null=False)), |
||||
(%(sort_field)r, models.IntegerField()) |
||||
)) |
||||
db.create_unique(%(table_name)r, [%(left_column)r, %(right_column)r])''' |
||||
|
||||
def console_line(self): |
||||
if isinstance(self.field, SortedManyToManyField) and self.field.sorted: |
||||
return " + Added SortedM2M table for %s on %s.%s" % ( |
||||
self.field.name, |
||||
self.model._meta.app_label, |
||||
self.model._meta.object_name, |
||||
) |
||||
else: |
||||
return super(AddM2M, self).console_line() |
||||
|
||||
def forwards_code(self): |
||||
if isinstance(self.field, SortedManyToManyField) and self.field.sorted: |
||||
return self.SORTED_FORWARDS_TEMPLATE % { |
||||
"model_name": self.model._meta.object_name, |
||||
"field_name": self.field.name, |
||||
"table_name": self.field.m2m_db_table(), |
||||
"left_field": self.field.m2m_column_name()[:-3], # Remove the _id part |
||||
"left_column": self.field.m2m_column_name(), |
||||
"left_model_key": model_key(self.model), |
||||
"right_field": self.field.m2m_reverse_name()[:-3], # Remove the _id part |
||||
"right_column": self.field.m2m_reverse_name(), |
||||
"right_model_key": model_key(self.field.rel.to), |
||||
"sort_field": self.field.sort_value_field_name, |
||||
} |
||||
else: |
||||
return super(AddM2M, self).forwards_code() |
||||
|
||||
class DeleteM2M(AddM2M): |
||||
def console_line(self): |
||||
return " - Deleted M2M table for %s on %s.%s" % ( |
||||
self.field.name, |
||||
self.model._meta.app_label, |
||||
self.model._meta.object_name, |
||||
) |
||||
|
||||
def forwards_code(self): |
||||
return AddM2M.backwards_code(self) |
||||
|
||||
def backwards_code(self): |
||||
return AddM2M.forwards_code(self) |
||||
|
||||
south.creator.actions.AddM2M = AddM2M |
||||
south.creator.actions.DeleteM2M = DeleteM2M |
||||
@ -0,0 +1,104 @@ |
||||
# -*- coding: utf-8 -*- |
||||
import sys |
||||
from itertools import chain |
||||
from django import forms |
||||
from django.conf import settings |
||||
from django.db.models.query import QuerySet |
||||
from django.template.loader import render_to_string |
||||
from django.utils.encoding import force_text |
||||
from django.utils.html import conditional_escape, escape |
||||
from django.utils.safestring import mark_safe |
||||
|
||||
|
||||
if sys.version_info[0] < 3: |
||||
iteritems = lambda d: iter(d.iteritems()) |
||||
string_types = basestring, |
||||
str_ = unicode |
||||
else: |
||||
iteritems = lambda d: iter(d.items()) |
||||
string_types = str, |
||||
str_ = str |
||||
|
||||
|
||||
STATIC_URL = getattr(settings, 'STATIC_URL', settings.MEDIA_URL) |
||||
|
||||
|
||||
class SortedCheckboxSelectMultiple(forms.CheckboxSelectMultiple): |
||||
class Media: |
||||
js = ( |
||||
STATIC_URL + 'sortedm2m/widget.js', |
||||
STATIC_URL + 'sortedm2m/jquery-ui.js', |
||||
) |
||||
css = {'screen': ( |
||||
STATIC_URL + 'sortedm2m/widget.css', |
||||
)} |
||||
|
||||
def build_attrs(self, attrs=None, **kwargs): |
||||
attrs = super(SortedCheckboxSelectMultiple, self).\ |
||||
build_attrs(attrs, **kwargs) |
||||
classes = attrs.setdefault('class', '').split() |
||||
classes.append('sortedm2m') |
||||
attrs['class'] = ' '.join(classes) |
||||
return attrs |
||||
|
||||
def render(self, name, value, attrs=None, choices=()): |
||||
if value is None: value = [] |
||||
has_id = attrs and 'id' in attrs |
||||
final_attrs = self.build_attrs(attrs, name=name) |
||||
|
||||
# Normalize to strings |
||||
str_values = [force_text(v) for v in value] |
||||
|
||||
selected = [] |
||||
unselected = [] |
||||
|
||||
for i, (option_value, option_label) in enumerate(chain(self.choices, choices)): |
||||
# If an ID attribute was given, add a numeric index as a suffix, |
||||
# so that the checkboxes don't all have the same ID attribute. |
||||
if has_id: |
||||
final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i)) |
||||
label_for = ' for="%s"' % conditional_escape(final_attrs['id']) |
||||
else: |
||||
label_for = '' |
||||
|
||||
cb = forms.CheckboxInput(final_attrs, check_test=lambda value: value in str_values) |
||||
option_value = force_text(option_value) |
||||
rendered_cb = cb.render(name, option_value) |
||||
option_label = conditional_escape(force_text(option_label)) |
||||
item = {'label_for': label_for, 'rendered_cb': rendered_cb, 'option_label': option_label, 'option_value': option_value} |
||||
if option_value in str_values: |
||||
selected.append(item) |
||||
else: |
||||
unselected.append(item) |
||||
|
||||
# re-order `selected` array according str_values which is a set of `option_value`s in the order they should be shown on screen |
||||
ordered = [] |
||||
for value in str_values: |
||||
for select in selected: |
||||
if value == select['option_value']: |
||||
ordered.append(select) |
||||
selected = ordered |
||||
|
||||
html = render_to_string( |
||||
'sortedm2m/sorted_checkbox_select_multiple_widget.html', |
||||
{'selected': selected, 'unselected': unselected}) |
||||
return mark_safe(html) |
||||
|
||||
def value_from_datadict(self, data, files, name): |
||||
value = data.get(name, None) |
||||
if isinstance(value, string_types): |
||||
return [v for v in value.split(',') if v] |
||||
return value |
||||
|
||||
|
||||
class SortedMultipleChoiceField(forms.ModelMultipleChoiceField): |
||||
widget = SortedCheckboxSelectMultiple |
||||
|
||||
def clean(self, value): |
||||
queryset = super(SortedMultipleChoiceField, self).clean(value) |
||||
if value is None or not isinstance(queryset, QuerySet): |
||||
return queryset |
||||
object_list = dict(( |
||||
(str_(key), value) |
||||
for key, value in iteritems(queryset.in_bulk(value)))) |
||||
return [object_list[str_(pk)] for pk in value] |
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 552 B |
@ -0,0 +1,50 @@ |
||||
.sortedm2m-container { |
||||
margin-right: 10px; |
||||
width: 570px; |
||||
} |
||||
|
||||
.sortedm2m-container p.selector-filter { |
||||
width: 570px; |
||||
padding: 0; |
||||
margin: 0; |
||||
} |
||||
|
||||
.sortedm2m-container p.selector-filter input { |
||||
width: 532px; |
||||
margin: 5px 4px; |
||||
} |
||||
|
||||
ul.sortedm2m { |
||||
display: block; |
||||
width: 554px; |
||||
min-height: 200px; |
||||
max-height: 400px; |
||||
overflow-x: hidden; |
||||
overflow-y: auto; |
||||
margin: 0; |
||||
padding: 6px 8px; |
||||
list-style-type: none; |
||||
text-align: left; |
||||
} |
||||
|
||||
ul.sortedm2m li { |
||||
list-style-type: none; |
||||
text-align: left; |
||||
width: 550px; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
white-space: pre; |
||||
} |
||||
|
||||
ul.sortedm2m li, ul.sortedm2m label { |
||||
cursor: move; |
||||
} |
||||
|
||||
/* required to work properly in django admin */ |
||||
body.change-form .sortedm2m-container { |
||||
float: left; |
||||
} |
||||
.module ul.sortedm2m { |
||||
margin: 0; |
||||
padding: 6px 8px; |
||||
} |
||||
@ -0,0 +1,91 @@ |
||||
if (jQuery === undefined) { |
||||
jQuery = django.jQuery; |
||||
} |
||||
|
||||
(function ($) { |
||||
$(function () { |
||||
$('.sortedm2m').parents('ul').each(function () { |
||||
$(this).addClass('sortedm2m'); |
||||
var checkboxes = $(this).find('input[type=checkbox]'); |
||||
var id = checkboxes.first().attr('id').match(/^(.*)_\d+$/)[1]; |
||||
var name = checkboxes.first().attr('name'); |
||||
checkboxes.removeAttr('name'); |
||||
$(this).before('<input type="hidden" id="' + id + '" name="' + name + '" />'); |
||||
var that = this; |
||||
var recalculate_value = function () { |
||||
var values = []; |
||||
$(that).find(':checked').each(function () { |
||||
values.push($(this).val()); |
||||
}); |
||||
$('#' + id).val(values.join(',')); |
||||
} |
||||
recalculate_value(); |
||||
checkboxes.change(recalculate_value); |
||||
$(this).sortable({ |
||||
axis: 'y', |
||||
//containment: 'parent',
|
||||
update: recalculate_value |
||||
}); |
||||
}); |
||||
|
||||
$('.sortedm2m-container .selector-filter input').each(function () { |
||||
$(this).bind('input', function() { |
||||
var search = $(this).val().toLowerCase(); |
||||
var $el = $(this).closest('.selector-filter'); |
||||
var $container = $el.siblings('ul').each(function() { |
||||
// walk over each child list el and do name comparisons
|
||||
$(this).children().each(function() { |
||||
var curr = $(this).find('label').text().toLowerCase(); |
||||
if (curr.indexOf(search) === -1) { |
||||
$(this).css('display', 'none'); |
||||
} else { |
||||
$(this).css('display', 'inherit'); |
||||
}; |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
if (window.showAddAnotherPopup) { |
||||
var django_dismissAddAnotherPopup = window.dismissAddAnotherPopup; |
||||
window.dismissAddAnotherPopup = function (win, newId, newRepr) { |
||||
// newId and newRepr are expected to have previously been escaped by
|
||||
// django.utils.html.escape.
|
||||
newId = html_unescape(newId); |
||||
newRepr = html_unescape(newRepr); |
||||
var name = windowname_to_id(win.name); |
||||
var elem = $('#' + name); |
||||
var sortedm2m = elem.siblings('ul.sortedm2m'); |
||||
if (sortedm2m.length == 0) { |
||||
// no sortedm2m widget, fall back to django's default
|
||||
// behaviour
|
||||
return django_dismissAddAnotherPopup.apply(this, arguments); |
||||
} |
||||
|
||||
if (elem.val().length > 0) { |
||||
elem.val(elem.val() + ','); |
||||
} |
||||
elem.val(elem.val() + newId); |
||||
|
||||
var id_template = ''; |
||||
var maxid = 0; |
||||
sortedm2m.find('li input').each(function () { |
||||
var match = this.id.match(/^(.+)_(\d+)$/); |
||||
id_template = match[1]; |
||||
id = parseInt(match[2]); |
||||
if (id > maxid) maxid = id; |
||||
}); |
||||
|
||||
var id = id_template + '_' + (maxid + 1); |
||||
var new_li = $('<li/>').append( |
||||
$('<label/>').attr('for', id).append( |
||||
$('<input class="sortedm2m" type="checkbox" checked="checked" />').attr('id', id).val(newId) |
||||
).append($('<span/>').text(' ' + newRepr)) |
||||
); |
||||
sortedm2m.append(new_li); |
||||
|
||||
win.close(); |
||||
}; |
||||
} |
||||
}); |
||||
})(jQuery); |
||||
@ -0,0 +1,23 @@ |
||||
{% load i18n static %} |
||||
<div class="sortedm2m-container"> |
||||
|
||||
<p class="selector-filter"> |
||||
<img src="{% static "sortedm2m/selector-search.gif" %}" alt="" title="{% trans "Type into this box to filter down the list." %}" /> |
||||
<input type="text" placeholder="{% trans "Filter" %}" /> |
||||
</p> |
||||
|
||||
<ul> |
||||
{% for row in selected %} |
||||
<li><label{{ row.label_for|safe }}>{{ row.rendered_cb }} {{ row.option_label }}</label></li> |
||||
{% endfor %} |
||||
|
||||
{% for row in unselected %} |
||||
<li><label{{ row.label_for|safe }}>{{ row.rendered_cb }} {{ row.option_label }}</label></li> |
||||
{% endfor %} |
||||
</ul> |
||||
|
||||
<p class="help"> |
||||
{% trans "Choose items and order by drag & drop." %} |
||||
</p> |
||||
|
||||
</div> |
||||
Loading…
Reference in new issue