| 1 | from django.contrib.admin import widgets
|
|---|
| 2 | from django.contrib.admin.options import get_ul_class
|
|---|
| 3 | from django.contrib.admin.widgets import AutocompleteSelect
|
|---|
| 4 | from django.forms import boundfield, models
|
|---|
| 5 | from django.urls import reverse
|
|---|
| 6 | from django.urls.exceptions import NoReverseMatch
|
|---|
| 7 | from django.utils.text import Truncator
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 | class ForeignKeyRawIdWidget(widgets.ForeignKeyRawIdWidget):
|
|---|
| 11 | def format_value(self, value):
|
|---|
| 12 | """Try to return the `pk` if value is an object, otherwise just return
|
|---|
| 13 | the value as fallback."""
|
|---|
| 14 |
|
|---|
| 15 | if value == '' or value is None:
|
|---|
| 16 | return None
|
|---|
| 17 |
|
|---|
| 18 | try:
|
|---|
| 19 | return str(value.pk)
|
|---|
| 20 | except AttributeError:
|
|---|
| 21 | return str(value)
|
|---|
| 22 |
|
|---|
| 23 | def label_and_url_for_value(self, value):
|
|---|
| 24 | """Instead of the original we do not have do a `get()` anymore instead
|
|---|
| 25 | access the instance directly so when value is prefetched this will
|
|---|
| 26 | prevent additional queries."""
|
|---|
| 27 |
|
|---|
| 28 | try:
|
|---|
| 29 | pk = value.pk
|
|---|
| 30 | meta = value._meta
|
|---|
| 31 | except AttributeError:
|
|---|
| 32 | # Fallback for compatibility with plain pk values
|
|---|
| 33 | return super().label_and_url_for_value(value)
|
|---|
| 34 |
|
|---|
| 35 | try:
|
|---|
| 36 | url = reverse(
|
|---|
| 37 | '%s:%s_%s_change' % (
|
|---|
| 38 | self.admin_site.name,
|
|---|
| 39 | meta.app_label,
|
|---|
| 40 | meta.object_name.lower(),
|
|---|
| 41 | ),
|
|---|
| 42 | args=(pk,)
|
|---|
| 43 | )
|
|---|
| 44 | except NoReverseMatch:
|
|---|
| 45 | url = '' # Admin not registered for target model.
|
|---|
| 46 |
|
|---|
| 47 | return Truncator(value).words(14), url
|
|---|
| 48 |
|
|---|
| 49 |
|
|---|
| 50 | class BoundField(boundfield.BoundField):
|
|---|
| 51 | def value(self):
|
|---|
| 52 | """Return the instance instead of plain value if possible.
|
|---|
| 53 |
|
|---|
| 54 | In order for `ForeignKeyRawIdWidget` to access the model instance directly
|
|---|
| 55 | we grab if from the form if available."""
|
|---|
| 56 |
|
|---|
| 57 | if type(self.field.widget) == ForeignKeyRawIdWidget:
|
|---|
| 58 | try:
|
|---|
| 59 | return getattr(self.form.instance, self.name)
|
|---|
| 60 | except AttributeError:
|
|---|
| 61 | pass
|
|---|
| 62 |
|
|---|
| 63 | # Otherwise default behaviour
|
|---|
| 64 | return super().value()
|
|---|
| 65 |
|
|---|
| 66 |
|
|---|
| 67 | class ModelChoiceField(models.ModelChoiceField):
|
|---|
| 68 | def get_bound_field(self, form, field_name):
|
|---|
| 69 | """Return our custom `BoundField`."""
|
|---|
| 70 |
|
|---|
| 71 | return BoundField(form, self, field_name)
|
|---|
| 72 |
|
|---|
| 73 |
|
|---|
| 74 | class RawIdWidgetAdminMixin:
|
|---|
| 75 | def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
|---|
| 76 | """ModelAdmin mixin that uses a custom `ForeignKeyRawIdWidget`.
|
|---|
| 77 |
|
|---|
| 78 | This prevents extra queries when the queryset has been prefetched using
|
|---|
| 79 | `prefetch_related()`. Only works when `raw_id_fields` is filled."""
|
|---|
| 80 |
|
|---|
| 81 | if db_field.name not in self.raw_id_fields:
|
|---|
| 82 | # If we are not using raw_id_fields then skip the whole thing
|
|---|
| 83 | return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
|---|
| 84 |
|
|---|
| 85 | db = kwargs.get('using')
|
|---|
| 86 |
|
|---|
| 87 | if 'widget' not in kwargs:
|
|---|
| 88 | if db_field.name in self.get_autocomplete_fields(request):
|
|---|
| 89 | kwargs['widget'] = AutocompleteSelect(db_field.remote_field, self.admin_site, using=db)
|
|---|
| 90 | elif db_field.name in self.raw_id_fields:
|
|---|
| 91 | # Using our modified ForeignKeyRawIdWidget here instead
|
|---|
| 92 | kwargs['widget'] = ForeignKeyRawIdWidget(db_field.remote_field, self.admin_site, using=db)
|
|---|
| 93 | elif db_field.name in self.radio_fields:
|
|---|
| 94 | kwargs['widget'] = widgets.AdminRadioSelect(attrs={
|
|---|
| 95 | 'class': get_ul_class(self.radio_fields[db_field.name]),
|
|---|
| 96 | })
|
|---|
| 97 | kwargs['empty_label'] = _('None') if db_field.blank else None
|
|---|
| 98 |
|
|---|
| 99 | if 'queryset' not in kwargs:
|
|---|
| 100 | queryset = self.get_field_queryset(db, db_field, request)
|
|---|
| 101 | if queryset is not None:
|
|---|
| 102 | kwargs['queryset'] = queryset
|
|---|
| 103 |
|
|---|
| 104 | if isinstance(db_field.remote_field.model, str):
|
|---|
| 105 | raise ValueError("Cannot create form field for %r yet, because "
|
|---|
| 106 | "its related model %r has not been loaded yet" %
|
|---|
| 107 | (db_field.name, db_field.remote_field.model))
|
|---|
| 108 | return super(type(db_field), db_field).formfield(**{
|
|---|
| 109 | # Using our modified ModelChoiceField here instead
|
|---|
| 110 | 'form_class': ModelChoiceField,
|
|---|
| 111 | 'queryset': db_field.remote_field.model._default_manager.using(db),
|
|---|
| 112 | 'to_field_name': db_field.remote_field.field_name,
|
|---|
| 113 | **kwargs,
|
|---|
| 114 | 'blank': db_field.blank,
|
|---|
| 115 | })
|
|---|