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)
Thanks for the tip!
ReplyDeleteNote 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
Thank you for your very nice article, do not forget to read my articles also
ReplyDeletegambar 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.
Very helpfull!
ReplyDeleteso wath am i doing in the view ??
ReplyDeleteReally helpful. Ty
ReplyDelete