From 7d374ad5973ff5b14e996a38e078c7d46646ae5d Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Tue, 16 Aug 2005 18:08:37 +0000 Subject: [PATCH] Added raw_id_admin support to ManyToManyField objects; fixes #260 git-svn-id: http://code.djangoproject.com/svn/django/trunk@516 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../js/admin/RelatedObjectLookups.js | 7 +++- django/core/meta/fields.py | 39 +++++++++++++++---- django/views/admin/main.py | 25 +++++++++--- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/django/conf/admin_media/js/admin/RelatedObjectLookups.js b/django/conf/admin_media/js/admin/RelatedObjectLookups.js index 55bd59a850..0f68921745 100644 --- a/django/conf/admin_media/js/admin/RelatedObjectLookups.js +++ b/django/conf/admin_media/js/admin/RelatedObjectLookups.js @@ -9,7 +9,12 @@ function showRelatedObjectLookupPopup(triggeringLink) { } function dismissRelatedLookupPopup(win, chosenId) { - document.getElementById(win.name).value = chosenId; + var elem = document.getElementById(win.name); + if (elem.className.indexOf('vCommaSeparatedIntegerField') != -1 && elem.value) { + elem.value += ',' + chosenId; + } else { + document.getElementById(win.name).value = chosenId; + } win.close(); } diff --git a/django/core/meta/fields.py b/django/core/meta/fields.py index 87b5a34fa0..8cf61d3176 100644 --- a/django/core/meta/fields.py +++ b/django/core/meta/fields.py @@ -71,7 +71,10 @@ class Field(object): self.radio_admin = radio_admin self.help_text = help_text if rel and isinstance(rel, ManyToMany): - self.help_text += ' Hold down "Control", or "Command" on a Mac, to select more than one.' + if rel.raw_id_admin: + self.help_text += ' Separate multiple IDs with commas.' + else: + self.help_text += ' Hold down "Control", or "Command" on a Mac, to select more than one.' # Set db_index to True if the field has a relationship and doesn't explicitly set db_index. if db_index is None: @@ -572,17 +575,37 @@ class ManyToManyField(Field): num_in_admin=kwargs.pop('num_in_admin', 0), related_name=kwargs.pop('related_name', None), filter_interface=kwargs.pop('filter_interface', None), - limit_choices_to=kwargs.pop('limit_choices_to', None)) + limit_choices_to=kwargs.pop('limit_choices_to', None), + raw_id_admin=kwargs.pop('raw_id_admin', False)) + if kwargs["rel"].raw_id_admin: + kwargs.setdefault("validator_list", []).append(self.isValidIDList) Field.__init__(self, **kwargs) def get_manipulator_field_objs(self): - choices = self.get_choices(include_blank=False) - return [curry(formfields.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] - + if self.rel.raw_id_admin: + return [formfields.CommaSeparatedIntegerField] + else: + choices = self.get_choices(include_blank=False) + return [curry(formfields.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] + def get_m2m_db_table(self, original_opts): "Returns the name of the many-to-many 'join' table." return '%s_%s' % (original_opts.db_table, self.name) - + + def isValidIDList(self, field_data, all_data): + "Validates that the value is a valid list of foreign keys" + mod = self.rel.to.get_model_module() + try: + pks = map(int, field_data.split(',')) + except ValueError: + # the CommaSeparatedIntegerField validator will catch this error + return + objects = mod.get_in_bulk(pks) + if len(objects) != len(pks): + badkeys = [k for k in pks if k not in objects] + raise validators.ValidationError, "Please enter valid %s IDs (the value%s %r %s invalid)" % \ + (self.verbose_name, len(badkeys) > 1 and 's' or '', len(badkeys) == 1 and badkeys[0] or tuple(badkeys), len(badkeys) == 1 and "is" or "are") + class OneToOneField(IntegerField): def __init__(self, to, to_field=None, rel_name=None, **kwargs): kwargs['name'] = kwargs.get('name', 'id') @@ -631,13 +654,15 @@ class ManyToOne: class ManyToMany: def __init__(self, to, name, num_in_admin=0, related_name=None, - filter_interface=None, limit_choices_to=None): + filter_interface=None, limit_choices_to=None, raw_id_admin=False): self.to, self.name = to._meta, name self.num_in_admin = num_in_admin self.related_name = related_name self.filter_interface = filter_interface self.limit_choices_to = limit_choices_to or {} self.edit_inline = False + self.raw_id_admin = raw_id_admin + assert not (self.raw_id_admin and self.filter_interface), "ManyToMany relationships may not use both raw_id_admin and filter_interface" class OneToOne(ManyToOne): def __init__(self, to, name, field_name, num_in_admin=0, edit_inline=False, diff --git a/django/views/admin/main.py b/django/views/admin/main.py index 430ae15744..907adec31e 100644 --- a/django/views/admin/main.py +++ b/django/views/admin/main.py @@ -510,7 +510,7 @@ def _get_flattened_data(field, val): else: return {field.name: val} -use_raw_id_admin = lambda field: isinstance(field.rel, meta.ManyToOne) and field.rel.raw_id_admin +use_raw_id_admin = lambda field: isinstance(field.rel, (meta.ManyToOne, meta.ManyToMany)) and field.rel.raw_id_admin def _get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects): t = ['
'] @@ -722,8 +722,13 @@ def _get_admin_field(field_list, name_prefix, rel, add, change): if change and field.primary_key: t.append('{{ %soriginal.%s }}' % ((rel and name_prefix or ''), field.name)) if change and use_raw_id_admin(field): - obj_repr = '%soriginal.get_%s|truncatewords:"14"' % (rel and name_prefix or '', field.rel.name) - t.append('{%% if %s %%} {{ %s }}{%% endif %%}' % (obj_repr, obj_repr)) + if isinstance(field.rel, meta.ManyToOne): + if_bit = '%soriginal.get_%s' % (rel and name_prefix or '', field.rel.name) + obj_repr = if_bit + '|truncatewords:"14"' + elif isinstance(field.rel, meta.ManyToMany): + if_bit = '%soriginal.get_%s_list' % (rel and name_prefix or '', field.rel.name) + obj_repr = if_bit + '|join:", "|truncatewords:"14"' + t.append('{%% if %s %%} {{ %s }}{%% endif %%}' % (if_bit, obj_repr)) if field.help_text: t.append('

%s

\n' % field.help_text) t.append('
\n\n') @@ -766,6 +771,9 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p new_data.update(request.FILES) errors = manipulator.get_validation_errors(new_data) if not errors and not request.POST.has_key("_preview"): + for f in opts.many_to_many: + if f.rel.raw_id_admin: + new_data.setlist(f.name, new_data[f.name].split(",")) manipulator.do_html2python(new_data) new_object = manipulator.save(new_data) pk_value = getattr(new_object, opts.pk.name) @@ -804,7 +812,7 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p # In required many-to-many fields with only one available choice, # select that one available choice. for f in opts.many_to_many: - if not f.blank and not f.rel.edit_inline and len(manipulator[f.name].choices) == 1: + if not f.blank and not f.rel.edit_inline and not f.rel.raw_id_admin and len(manipulator[f.name].choices) == 1: new_data[f.name] = [manipulator[f.name].choices[0][0]] # Add default data for related objects. for rel_opts, rel_field in opts.get_inline_related_objects(): @@ -855,13 +863,18 @@ def change_stage(request, app_label, module_name, object_id): manipulator = mod.ChangeManipulator(object_id) except ObjectDoesNotExist: raise Http404 + inline_related_objects = opts.get_inline_related_objects() if request.POST: new_data = request.POST.copy() if opts.has_field_type(meta.FileField): new_data.update(request.FILES) + errors = manipulator.get_validation_errors(new_data) if not errors and not request.POST.has_key("_preview"): + for f in opts.many_to_many: + if f.rel.raw_id_admin: + new_data.setlist(f.name, new_data[f.name].split(",")) manipulator.do_html2python(new_data) new_object = manipulator.save(new_data) pk_value = getattr(new_object, opts.pk.name) @@ -904,7 +917,9 @@ def change_stage(request, app_label, module_name, object_id): for f in opts.fields: new_data.update(_get_flattened_data(f, getattr(obj, f.name))) for f in opts.many_to_many: - if not f.rel.edit_inline: + if f.rel.raw_id_admin: + new_data[f.name] = ",".join([str(i.id) for i in getattr(obj, 'get_%s_list' % f.rel.name)()]) + elif not f.rel.edit_inline: new_data[f.name] = [i.id for i in getattr(obj, 'get_%s_list' % f.rel.name)()] for rel_obj, rel_field in inline_related_objects: var_name = rel_obj.object_name.lower()