Ticket #29294: raw_id_widget.py

File raw_id_widget.py, 4.4 KB (added by Jurrian Tromp, 2 years ago)
Line 
1from django.contrib.admin import widgets
2from django.contrib.admin.options import get_ul_class
3from django.contrib.admin.widgets import AutocompleteSelect
4from django.forms import boundfield, models
5from django.urls import reverse
6from django.urls.exceptions import NoReverseMatch
7from django.utils.text import Truncator
8
9
10class 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
50class 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
67class 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
74class 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 })
Back to Top