Code

Ticket #9976: generic-foreign-key-widget.diff

File generic-foreign-key-widget.diff, 11.2 KB (added by Alex, 6 years ago)
Line 
1diff --git a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
2index d201f39..4bcff7f 100644
3--- a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
4+++ b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
5@@ -41,6 +41,15 @@ function showRelatedObjectLookupPopup(triggeringLink) {
6     return false;
7 }
8 
9+function showGenericRelatedObjectLookupPopup(ct_select, triggering_link, url_base) {
10+    var url = content_types[ct_select.options[ct_select.selectedIndex].value];
11+    if (url != undefined) {
12+        triggering_link.href = url_base + url;
13+        return showRelatedObjectLookupPopup(triggering_link);
14+    }
15+    return false;
16+}
17+
18 function dismissRelatedLookupPopup(win, chosenId) {
19     var name = windowname_to_id(win.name);
20     var elem = document.getElementById(name);
21diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
22index 3d60b9d..4ba49bc 100644
23--- a/django/contrib/admin/options.py
24+++ b/django/contrib/admin/options.py
25@@ -38,6 +38,7 @@ class BaseModelAdmin(object):
26     filter_horizontal = ()
27     radio_fields = {}
28     prepopulated_fields = {}
29+    generic_fields = ()
30 
31     def formfield_for_dbfield(self, db_field, **kwargs):
32         """
33@@ -63,6 +64,13 @@ class BaseModelAdmin(object):
34             else:
35                 # Otherwise, use the default select widget.
36                 return db_field.formfield(**kwargs)
37+       
38+        # For generic foreign keys marked as generic_fields we use a special widget
39+        if db_field.name in [f.fk_field for f in self.model._meta.virtual_fields if f.name in self.generic_fields]:
40+            for gfk in self.model._meta.virtual_fields:
41+                if gfk.fk_field == db_field.name:
42+                    break
43+            return db_field.formfield(label=capfirst(gfk.name.replace('_', ' ')), widget=widgets.GenericForeignKeyRawIdWidget(gfk.ct_field, self.admin_site._registry.keys()))
44 
45         # For DateTimeFields, use a special field and widget.
46         if isinstance(db_field, models.DateTimeField):
47diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
48index ccade8a..779c91d 100644
49--- a/django/contrib/admin/validation.py
50+++ b/django/contrib/admin/validation.py
51@@ -102,6 +102,14 @@ def validate(cls, model):
52         if not isinstance(getattr(cls, attr), bool):
53             raise ImproperlyConfigured("'%s.%s' should be a boolean."
54                     % (cls.__name__, attr))
55+   
56+    if hasattr(cls, 'generic_fields'):
57+        check_isseq(cls, 'generic_fields', cls.generic_fields)
58+        for i, field in enumerate(cls.generic_fields):
59+            if field not in [f.name for f in model._meta.virtual_fields]:
60+                raise ImproperlyConfigured("Item number %d in %s.generic_fields" \
61+                    "is not a GenericForeignKey on %s" % (i, cls.__name__,
62+                    model.__name__))
63 
64     # inlines = []
65     if hasattr(cls, 'inlines'):
66diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
67index 291bee0..aa69d70 100644
68--- a/django/contrib/admin/widgets.py
69+++ b/django/contrib/admin/widgets.py
70@@ -278,3 +278,40 @@ class AdminCommaSeparatedIntegerFieldWidget(forms.TextInput):
71         if attrs is not None:
72             final_attrs.update(attrs)
73         super(AdminCommaSeparatedIntegerFieldWidget, self).__init__(attrs=final_attrs)
74+
75+class GenericForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
76+    def __init__(self, ct_field, cts = [], attrs=None):
77+        self.ct_field = ct_field
78+        self.cts = cts
79+        forms.TextInput.__init__(self, attrs)
80+   
81+    def render(self, name, value, attrs=None):
82+        if attrs is None:
83+            attrs = {}
84+        related_url = '../../../'
85+        params = self.url_parameters()
86+        if params:
87+            url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.iteritems()])
88+        else:
89+            url = ''
90+        if 'class' not in attrs:
91+            attrs['class'] = 'vForeignKeyRawIdAdminField'
92+        output = [forms.TextInput.render(self, name, value, attrs)]
93+        output.append("""
94+            <a href="%(related)s%(url)s" class="related-lookup" id="lookup_id_%(name)s" onclick="return showGenericRelatedObjectLookupPopup(document.getElementById('id_%(ct_field)s'), this, '%(related)s%(url)s');"> """
95+             % {'related': related_url, 'url': url, 'name': name, 'ct_field': self.ct_field})
96+        output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
97+        if value:
98+            output.append(self.label_for_value(value))
99+       
100+        from django.contrib.contenttypes.models import ContentType
101+        content_types = """
102+        <script type="text/javascript">
103+        var content_types = new Array();
104+        %s
105+        </script>
106+        """ % ('\n'.join(["content_types[%s] = '%s/%s/';" % (ContentType.objects.get_for_model(ct).id, ct._meta.app_label, ct._meta.object_name.lower()) for ct in self.cts]))
107+        return mark_safe(u''.join(output) + content_types)
108+   
109+    def url_parameters(self):
110+        return {}
111diff --git a/docs/ref/contrib/admin.txt b/docs/ref/contrib/admin.txt
112index f24dc46..7fb1ae4 100644
113--- a/docs/ref/contrib/admin.txt
114+++ b/docs/ref/contrib/admin.txt
115@@ -238,6 +238,12 @@ list of fields that should be displayed as a horizontal filter interface. See
116 Same as ``filter_horizontal``, but is a vertical display of the filter
117 interface.
118 
119+``generic_fields``
120+~~~~~~~~~~~~~~~~~~
121+
122+This is a list of ``GenericForeignKeys`` that will have an interface similar to
123+that of ``raw_id_fields``.
124+
125 ``list_display``
126 ~~~~~~~~~~~~~~~~
127 
128diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
129index ddfc6c2..bf478f0 100644
130--- a/tests/regressiontests/admin_widgets/models.py
131+++ b/tests/regressiontests/admin_widgets/models.py
132@@ -2,6 +2,8 @@
133 from django.conf import settings
134 from django.db import models
135 from django.core.files.storage import default_storage
136+from django.contrib.contenttypes import generic
137+from django.contrib.contenttypes.models import ContentType
138 
139 class Member(models.Model):
140     name = models.CharField(max_length=100)
141@@ -41,6 +43,17 @@ class Inventory(models.Model):
142    def __unicode__(self):
143       return self.name
144 
145+class Image(models.Model):
146+    image = models.ImageField(upload_to='images')
147+    description = models.CharField(max_length=200)
148+
149+    content_type = models.ForeignKey(ContentType)
150+    object_id = models.PositiveIntegerField()
151+    object = generic.GenericForeignKey('content_type', 'object_id')
152+   
153+    def __unicode__(self):
154+        return u"%s - %s" % (self.image.name, self.description[:100])
155+
156 __test__ = {'WIDGETS_TESTS': """
157 >>> from datetime import datetime
158 >>> from django.utils.html import escape, conditional_escape
159@@ -48,6 +61,7 @@ __test__ = {'WIDGETS_TESTS': """
160 >>> from django.contrib.admin.widgets import FilteredSelectMultiple, AdminSplitDateTime
161 >>> from django.contrib.admin.widgets import AdminFileWidget, ForeignKeyRawIdWidget, ManyToManyRawIdWidget
162 >>> from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
163+>>> from django.contrib.admin.widgets import GenericForeignKeyRawIdWidget
164 
165 Calling conditional_escape on the output of widget.render will simulate what
166 happens in the template. This is easier than setting up a template and context
167@@ -116,6 +130,23 @@ True
168 >>> child_of_hidden = Inventory.objects.create(barcode=94, name='Child of hidden', parent=hidden)
169 >>> print w.render('test', child_of_hidden.parent_id, attrs={})
170 <input type="text" name="test" value="93" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Hidden</strong>
171+
172+>>> print GenericForeignKeyRawIdWidget(Image._meta.virtual_fields[0].ct_field).render('test', '', {})
173+    <input type="text" name="test" class="vForeignKeyRawIdAdminField" />
174+                <a href="../../../" class="related-lookup" id="lookup_id_test" onclick="return showGenericRelatedObjectLookupPopup(document.getElementById('id_content_type'), this, '../../../');"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>
175+            <script type="text/javascript">
176+            var content_types = new Array();
177+            </script>
178+
179+>>> print GenericForeignKeyRawIdWidget(Image._meta.virtual_fields[0].ct_field, [x.model_class() for x in ContentType.objects.filter(pk__lt=3)]).render('test', '', {})
180+<input type="text" name="test" class="vForeignKeyRawIdAdminField" />
181+            <a href="../../../" class="related-lookup" id="lookup_id_test" onclick="return showGenericRelatedObjectLookupPopup(document.getElementById('id_content_type'), this, '../../../');"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>
182+    <script type="text/javascript">
183+    var content_types = new Array();
184+        content_types[1] = 'contenttypes/contenttype/';
185+content_types[2] = 'auth/permission/';
186+    </script>
187+   
188 """ % {
189     'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
190     'STORAGE_URL': default_storage.url(''),
191diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py
192index 3a7d3f0..eba5061 100644
193--- a/tests/regressiontests/modeladmin/models.py
194+++ b/tests/regressiontests/modeladmin/models.py
195@@ -3,6 +3,8 @@ from datetime import date
196 
197 from django.db import models
198 from django.contrib.auth.models import User
199+from django.contrib.contenttypes import generic
200+from django.contrib.contenttypes.models import ContentType
201 
202 class Band(models.Model):
203     name = models.CharField(max_length=100)
204@@ -35,6 +37,15 @@ class ValidationTestModel(models.Model):
205 class ValidationTestInlineModel(models.Model):
206     parent = models.ForeignKey(ValidationTestModel)
207 
208+class Image(models.Model):
209+    image = models.ImageField(upload_to='pictures')
210+    description = models.TextField()
211+   
212+    content_type = models.ForeignKey(ContentType)
213+    object_id = models.PositiveIntegerField()
214+    object = generic.GenericForeignKey('content_type', 'object_id')
215+
216+
217 __test__ = {'API_TESTS': """
218 
219 >>> from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
220@@ -912,5 +923,22 @@ ImproperlyConfigured: 'ValidationTestInline.formset' does not inherit from BaseM
221 ...     inlines = [ValidationTestInline]
222 >>> validate(ValidationTestModelAdmin, ValidationTestModel)
223 
224+>>> class GenericModelAdmin(ModelAdmin):
225+...     generic_fields = None
226+>>> validate(GenericModelAdmin, Image)
227+Traceback (most recent call last):
228+...
229+ImproperlyConfigured: 'GenericModelAdmin.generic_fields' must be a list or tuple.
230+>>> class GenericModelAdmin(ModelAdmin):
231+...     generic_fields = ["abc"]
232+>>> validate(GenericModelAdmin, Image)
233+Traceback (most recent call last):
234+...
235+ImproperlyConfigured: Item number 0 in GenericModelAdmin.generic_fieldsis not a GenericForeignKey on Image
236+
237+>>> class GenericModelAdmin(ModelAdmin):
238+...     generic_fields = ["object"]
239+>>> validate(GenericModelAdmin, Image)
240+
241 """
242 }