Saturday, 1 February 2014

Displaying Django GenericForeignKey As Single Form Field

First published on adamalton.co.uk on 31st March 2010.

The Django content types framework lets you have Generic Foreign Key fields, which allow you to create a foreign key to any object (record) in your database by combining the object id with an identifier for the table in which the object lives. This is great, until you want to make it editable in a form. The GenericForeignKey reference is stored as 2 separate fields (object id and type id), and so when you view this in a standard model form you get a drop down of content types, and a text field in which to type the id of the object that you want. This is fairly meaningless to a user. Even if you know what the 2 fields mean, you still need a way of looking up the id of the object that you want.

So we want to be able to combine these 2 fields into a single, meaningful field, that lets the user simply select which object they want. The context in which I wrote this code is rather complex, and not worth going into here. So what is below is a simplified version, using an imaginary AttachableNote model as an example of something which may have a generic foriegn key to link itself to any other object in the site.

import re
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class AttachableNote(models.Model):
    """ A model which stores a text note.
        It can be attached to any object via its GenericForeignKey field.
    """
    text = models.TextField()
    object_id = models.PositiveIntegerField()
    object_type = models.ForeignKey(ContentType)
    generic_obj = generic.GenericForeignKey('object_type', 'object_id')

class AttachableNoteForm(forms.ModelForm):
    """ Form for creating an AttachableNote. """
    
    #GenericForeignKey form field, will hold combined object_type and object_id
    generic_obj = forms.ChoiceField()
    
    def __init__(self, *args, **kwargs):
        super(AttachableNoteForm, self).__init__(*args, **kwargs)
        #combine object_type and object_id into a single 'generic_obj' field
        #getall the objects that we want the user to be able to choose from
        available_objects = list(SomeModel.objects.all()) #put your stuff here
        available_objects += list(SomeOtherModel.objects.filter(field=value))
        #now create our list of choices for the <select> field
        object_choices = []
        for obj in available_objects:
            type_id = ContentType.objects.get_for_model(obj.__class__).id
            obj_id = obj.id
            form_value = "type:%s-id:%s" % (type_id, obj_id) #e.g."type:12-id:3"
            display_text = str(obj)
            object_choices.append([form_value, display_text])
        self.fields['generic_obj'].choices = object_choices
    
    class Meta:
        model = AttachableNote
        fields = [
            "text",
            "generic_obj"
        ]
    
    def save(self, *args, **kwargs):
        #get object_type and object_id values from combined generic_obj field
        object_string = self.cleaned_data['generic_obj']
        matches = re.match("type:(\d+)-id:(\d+)", object_string).groups()
        object_type_id = matches[0] #get 45 from "type:45-id:38"
        object_id = matches[1] #get 38 from "type:45-id:38"
        object_type = ContentType.objects.get(id=object_type_id)
        self.cleaned_data['object_type'] = object_type_id
        self.cleaned_data['object_id'] = object_id
        self.instance.object_id = object_id
        self.instance.object_type = object_type
        return super(AttachableNoteForm, self).save(*args, **kwargs)

5 comments:

  1. Thanks for the tip!

    Note that in AttachableNoteForm.__init__, you don't set the initial value for the choice field. This can be done with:

    if self.instance:
    initial = "type:%s-id:%s" % (self.instance.content_type_id, self.instance.object_id)
    self.fields['content_object'].initial = initial

    ReplyDelete
  2. Thank you for your very nice article, do not forget to read my articles also
    gambar lucu
    kata kata cinta
    kata kata galau
    kata kata lucu
    kata kata mutiara
    dp bbm cinta
    cara menghilangkan jerawat
    kata kata mutiara cinta
    are deliberately presented to the loyal readers.

    ReplyDelete
  3. so wath am i doing in the view ??

    ReplyDelete