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

File generic-foreign-key-widget.diff, 11.2 KB (added by Alex, 6 years ago)
  • django/contrib/admin/media/js/admin/RelatedObjectLookups.js

    diff --git a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
    index d201f39..4bcff7f 100644
    a b function showRelatedObjectLookupPopup(triggeringLink) { 
    4141    return false;
    4242}
    4343
     44function showGenericRelatedObjectLookupPopup(ct_select, triggering_link, url_base) {
     45    var url = content_types[ct_select.options[ct_select.selectedIndex].value];
     46    if (url != undefined) {
     47        triggering_link.href = url_base + url;
     48        return showRelatedObjectLookupPopup(triggering_link);
     49    }
     50    return false;
     51}
     52
    4453function dismissRelatedLookupPopup(win, chosenId) {
    4554    var name = windowname_to_id(win.name);
    4655    var elem = document.getElementById(name);
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index 3d60b9d..4ba49bc 100644
    a b class BaseModelAdmin(object): 
    3838    filter_horizontal = ()
    3939    radio_fields = {}
    4040    prepopulated_fields = {}
     41    generic_fields = ()
    4142
    4243    def formfield_for_dbfield(self, db_field, **kwargs):
    4344        """
    class BaseModelAdmin(object): 
    6364            else:
    6465                # Otherwise, use the default select widget.
    6566                return db_field.formfield(**kwargs)
     67       
     68        # For generic foreign keys marked as generic_fields we use a special widget
     69        if db_field.name in [f.fk_field for f in self.model._meta.virtual_fields if f.name in self.generic_fields]:
     70            for gfk in self.model._meta.virtual_fields:
     71                if gfk.fk_field == db_field.name:
     72                    break
     73            return db_field.formfield(label=capfirst(gfk.name.replace('_', ' ')), widget=widgets.GenericForeignKeyRawIdWidget(gfk.ct_field, self.admin_site._registry.keys()))
    6674
    6775        # For DateTimeFields, use a special field and widget.
    6876        if isinstance(db_field, models.DateTimeField):
  • django/contrib/admin/validation.py

    diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
    index ccade8a..779c91d 100644
    a b def validate(cls, model): 
    102102        if not isinstance(getattr(cls, attr), bool):
    103103            raise ImproperlyConfigured("'%s.%s' should be a boolean."
    104104                    % (cls.__name__, attr))
     105   
     106    if hasattr(cls, 'generic_fields'):
     107        check_isseq(cls, 'generic_fields', cls.generic_fields)
     108        for i, field in enumerate(cls.generic_fields):
     109            if field not in [f.name for f in model._meta.virtual_fields]:
     110                raise ImproperlyConfigured("Item number %d in %s.generic_fields" \
     111                    "is not a GenericForeignKey on %s" % (i, cls.__name__,
     112                    model.__name__))
    105113
    106114    # inlines = []
    107115    if hasattr(cls, 'inlines'):
  • django/contrib/admin/widgets.py

    diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
    index 291bee0..aa69d70 100644
    a b class AdminCommaSeparatedIntegerFieldWidget(forms.TextInput): 
    278278        if attrs is not None:
    279279            final_attrs.update(attrs)
    280280        super(AdminCommaSeparatedIntegerFieldWidget, self).__init__(attrs=final_attrs)
     281
     282class GenericForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
     283    def __init__(self, ct_field, cts = [], attrs=None):
     284        self.ct_field = ct_field
     285        self.cts = cts
     286        forms.TextInput.__init__(self, attrs)
     287   
     288    def render(self, name, value, attrs=None):
     289        if attrs is None:
     290            attrs = {}
     291        related_url = '../../../'
     292        params = self.url_parameters()
     293        if params:
     294            url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.iteritems()])
     295        else:
     296            url = ''
     297        if 'class' not in attrs:
     298            attrs['class'] = 'vForeignKeyRawIdAdminField'
     299        output = [forms.TextInput.render(self, name, value, attrs)]
     300        output.append("""
     301            <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');"> """
     302             % {'related': related_url, 'url': url, 'name': name, 'ct_field': self.ct_field})
     303        output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
     304        if value:
     305            output.append(self.label_for_value(value))
     306       
     307        from django.contrib.contenttypes.models import ContentType
     308        content_types = """
     309        <script type="text/javascript">
     310        var content_types = new Array();
     311        %s
     312        </script>
     313        """ % ('\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]))
     314        return mark_safe(u''.join(output) + content_types)
     315   
     316    def url_parameters(self):
     317        return {}
  • docs/ref/contrib/admin.txt

    diff --git a/docs/ref/contrib/admin.txt b/docs/ref/contrib/admin.txt
    index f24dc46..7fb1ae4 100644
    a b list of fields that should be displayed as a horizontal filter interface. See 
    238238Same as ``filter_horizontal``, but is a vertical display of the filter
    239239interface.
    240240
     241``generic_fields``
     242~~~~~~~~~~~~~~~~~~
     243
     244This is a list of ``GenericForeignKeys`` that will have an interface similar to
     245that of ``raw_id_fields``.
     246
    241247``list_display``
    242248~~~~~~~~~~~~~~~~
    243249
  • tests/regressiontests/admin_widgets/models.py

    diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
    index ddfc6c2..bf478f0 100644
    a b  
    22from django.conf import settings
    33from django.db import models
    44from django.core.files.storage import default_storage
     5from django.contrib.contenttypes import generic
     6from django.contrib.contenttypes.models import ContentType
    57
    68class Member(models.Model):
    79    name = models.CharField(max_length=100)
    class Inventory(models.Model): 
    4143   def __unicode__(self):
    4244      return self.name
    4345
     46class Image(models.Model):
     47    image = models.ImageField(upload_to='images')
     48    description = models.CharField(max_length=200)
     49
     50    content_type = models.ForeignKey(ContentType)
     51    object_id = models.PositiveIntegerField()
     52    object = generic.GenericForeignKey('content_type', 'object_id')
     53   
     54    def __unicode__(self):
     55        return u"%s - %s" % (self.image.name, self.description[:100])
     56
    4457__test__ = {'WIDGETS_TESTS': """
    4558>>> from datetime import datetime
    4659>>> from django.utils.html import escape, conditional_escape
    __test__ = {'WIDGETS_TESTS': """ 
    4861>>> from django.contrib.admin.widgets import FilteredSelectMultiple, AdminSplitDateTime
    4962>>> from django.contrib.admin.widgets import AdminFileWidget, ForeignKeyRawIdWidget, ManyToManyRawIdWidget
    5063>>> from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
     64>>> from django.contrib.admin.widgets import GenericForeignKeyRawIdWidget
    5165
    5266Calling conditional_escape on the output of widget.render will simulate what
    5367happens in the template. This is easier than setting up a template and context
    True 
    116130>>> child_of_hidden = Inventory.objects.create(barcode=94, name='Child of hidden', parent=hidden)
    117131>>> print w.render('test', child_of_hidden.parent_id, attrs={})
    118132<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>
     133
     134>>> print GenericForeignKeyRawIdWidget(Image._meta.virtual_fields[0].ct_field).render('test', '', {})
     135    <input type="text" name="test" class="vForeignKeyRawIdAdminField" />
     136                <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>
     137            <script type="text/javascript">
     138            var content_types = new Array();
     139            </script>
     140
     141>>> print GenericForeignKeyRawIdWidget(Image._meta.virtual_fields[0].ct_field, [x.model_class() for x in ContentType.objects.filter(pk__lt=3)]).render('test', '', {})
     142<input type="text" name="test" class="vForeignKeyRawIdAdminField" />
     143            <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>
     144    <script type="text/javascript">
     145    var content_types = new Array();
     146        content_types[1] = 'contenttypes/contenttype/';
     147content_types[2] = 'auth/permission/';
     148    </script>
     149   
    119150""" % {
    120151    'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
    121152    'STORAGE_URL': default_storage.url(''),
  • tests/regressiontests/modeladmin/models.py

    diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py
    index 3a7d3f0..eba5061 100644
    a b from datetime import date 
    33
    44from django.db import models
    55from django.contrib.auth.models import User
     6from django.contrib.contenttypes import generic
     7from django.contrib.contenttypes.models import ContentType
    68
    79class Band(models.Model):
    810    name = models.CharField(max_length=100)
    class ValidationTestModel(models.Model): 
    3537class ValidationTestInlineModel(models.Model):
    3638    parent = models.ForeignKey(ValidationTestModel)
    3739
     40class Image(models.Model):
     41    image = models.ImageField(upload_to='pictures')
     42    description = models.TextField()
     43   
     44    content_type = models.ForeignKey(ContentType)
     45    object_id = models.PositiveIntegerField()
     46    object = generic.GenericForeignKey('content_type', 'object_id')
     47
     48
    3849__test__ = {'API_TESTS': """
    3950
    4051>>> from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
    ImproperlyConfigured: 'ValidationTestInline.formset' does not inherit from BaseM 
    912923...     inlines = [ValidationTestInline]
    913924>>> validate(ValidationTestModelAdmin, ValidationTestModel)
    914925
     926>>> class GenericModelAdmin(ModelAdmin):
     927...     generic_fields = None
     928>>> validate(GenericModelAdmin, Image)
     929Traceback (most recent call last):
     930...
     931ImproperlyConfigured: 'GenericModelAdmin.generic_fields' must be a list or tuple.
     932>>> class GenericModelAdmin(ModelAdmin):
     933...     generic_fields = ["abc"]
     934>>> validate(GenericModelAdmin, Image)
     935Traceback (most recent call last):
     936...
     937ImproperlyConfigured: Item number 0 in GenericModelAdmin.generic_fieldsis not a GenericForeignKey on Image
     938
     939>>> class GenericModelAdmin(ModelAdmin):
     940...     generic_fields = ["object"]
     941>>> validate(GenericModelAdmin, Image)
     942
    915943"""
    916944}
Back to Top