5

I have an app using raw_id on both ForeignKeyField and ManyToManyField. The admin displays the value of the foreign key on the right of the edit box.

Unfortunatey, it doesn't work with ManyToMany. I've checked the code and I think that it is the normal behavior. However I would like to know if someone has an easy tip to change this behavior?

Thanks in advance

Update: I've tried to subclass the ManyToManyRawIdWidget but I don't know how to say that the raw_id_fields should use my custom widget. formfield_overrides doesn't seem to work with raw_id fields

luc
  • 41,928
  • 25
  • 127
  • 172

2 Answers2

10

Finally I succeed to make it working. Here is the updated Django2.0 version

from django.contrib.admin.widgets import ManyToManyRawIdWidget
from django.utils.encoding import smart_str
from django.urls import reverse
from django.utils.html import escape, mark_safe


class VerboseManyToManyRawIdWidget(ManyToManyRawIdWidget):
    """
    A Widget for displaying ManyToMany ids in the "raw_id" interface rather 
    than in a <select multiple> box. Display user-friendly value like the ForeignKeyRawId widget
    """

    def __init__(self, remote_field, attrs=None, *args, **kwargs):
        super().__init__(remote_field, attrs, *args, **kwargs)

    def label_and_url_for_value(self, value):
        values = value
        str_values = []
        field = self.rel.get_related_field()
        key = field.name
        fk_model = self.rel.model
        app_label = fk_model._meta.app_label
        class_name = fk_model._meta.object_name.lower()
        for the_value in values:
            try:
                obj = fk_model._default_manager.using(self.db).get(**{key: the_value})
                url = reverse('admin:{0}_{1}_change'.format(app_label, class_name), args=[obj.id])
                label = escape(smart_str(obj))
                elt = '<a href="{0}" {1}>{2}</a>'.format(
                    url,
                    'onclick="return showAddAnotherPopup(this);" target="_blank"',
                    label
                )
                str_values += [elt]
            except fk_model.DoesNotExist:
                str_values += [u'???']
        return mark_safe(', '.join(str_values)), ''


class MyAdmin(admin.ModelAdmin):
     ...
     def formfield_for_dbfield(self, db_field, **kwargs):
         if db_field.name in ('groups', ):
             kwargs['widget'] = VerboseManyToManyRawIdWidget(db_field.remote_field, self.admin_site)
         else:
             return super().formfield_for_dbfield(db_field, **kwargs)
         kwargs.pop('request')
         return db_field.formfield(**kwargs)

Unfortunately, I've spend a bounty for nothing ;-)

UPDATE : this snippet is now compatible with Django 2.0. See also http://djangosnippets.org/snippets/2108/

luc
  • 41,928
  • 25
  • 127
  • 172
1

This works for Django 1.11 and higher

from django.contrib.admin.sites import site
from django.contrib.admin.widgets import ManyToManyRawIdWidget
from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.safestring import mark_safe


class VerboseManyToManyRawIdWidget(ManyToManyRawIdWidget):

    def label_and_url_for_value(self, value):
        result = []
        for v in value:
            key = self.rel.get_related_field().name
            try:
                obj = self.rel.model._default_manager.using(self.db).get(**{key: v})
            except (ValueError, self.rel.model.DoesNotExist):
                return '', ''

            try:
                url = reverse(
                    '{}:{}_{}_change'.format(self.admin_site.name, obj._meta.app_label,
                                             obj._meta.object_name.lower()),
                    args=(obj.pk,))
            except NoReverseMatch:
                url = ''  # Admin not registered for target model.

            result.append('<strong><a href="{}">{}</a></strong>'.format(url,  str(obj)))

        return mark_safe('; '.join(result)), ''


class VerboseRawIdManyToManyAdminMixin:
    def formfield_for_dbfield(self, db_field, **kwargs):
        if db_field.name in self.raw_id_fields:
            kwargs.pop('request', None)
            if db_field.rel.__class__.__name__ == 'ManyToManyRel':
                kwargs['widget'] = VerboseManyToManyRawIdWidget(db_field.rel, site)
            return db_field.formfield(**kwargs)
        return super().formfield_for_dbfield(db_field, **kwargs)

Also there is Django app for that purpose django-salmonella

Mark Mishyn
  • 3,921
  • 2
  • 28
  • 30