Subclassing the ForeignKey field type

42 views
Skip to first unread message

Tobia Conforto

unread,
Sep 15, 2011, 12:43:57 PM9/15/11
to django...@googlegroups.com
Hi all

I'm adding an XML mapping feature to Django db models.

The idea is to add as little XML mapping information as possible to existing models (such as: which fields get mapped to XML, what is their XPath...) in order to be able to automatically:
1. produce an XML representation of an object; 
2. parse a valid XML representation into a new object; and 
3. get an XSD schema of the model.

I'm well ahead. I have all three features working for some simple models. But I've hit a wall with regard to object relations, or ForeignKey fields.

Apparently any subclasses of ForeignKey are completely ignored by Django at runtime, even if they declare __metaclass__ = SubfieldBase

I guess they would need a special kind of metaclass, which I guess has not been written yet.

Can anybody help me understand what piece is failing?
How can I make it work?

-Tobia


Here is a (non) working example:

### test/models.py:

from django.db.models import *

# subclassing works for any simple field type, why not related fields?
class MyForeignKey(ForeignKey):
__metaclass__ = SubfieldBase

def __init__(self, *args, **kwargs):
ForeignKey.__init__(self, *args, **kwargs)

class TestSub(Model):
pass

class Test(Model):
sub = MyForeignKey(TestSub)

### shell:

>>> from test.models import Test, TestSub
>>> ts = TestSub.objects.create()
>>> t = Test.objects.create(sub=ts)
Traceback (most recent call last):
  ...
AttributeError: 'Test' object has no attribute 'sub_id'
>>>

Michal Petrucha

unread,
Sep 19, 2011, 7:02:19 PM9/19/11
to django...@googlegroups.com

I'd say the problem here is that when you use the SubfieldBase
metaclass, your field subclass acts as a descriptor for the field and
attaches itself to the model class. This exploits the fact that
regular field types don't do any special voodoo regarding model
instances.

With ForeignKeys, however, the situation is quite a lot different. In
this case the field attaches a RelatedDescriptor to the model which
handles the object accessible through the field's name and takes care
of storing the actual field's value in another attribute of the model
instance (its name with '_id' appended, hence sub_id in your example).

What happens with your code is that the SubfieldBase metaclass
overrides the RelatedDescriptor, so the value you pass here::

>>> t = Test.objects.create(sub=ts)

gets stored directly in the 'sub' attribute and 'sub_id', which is
where Django expects the raw value for ForeignKeys, is not set at all.


As for what you are doing, I don't think subclassing ForeignKeys is
the way to go. Currently they defer most of the work to their target
fields, you might try to do something similar with the features you're
trying to add.

Michal

signature.asc

Tobia Conforto

unread,
Sep 28, 2011, 9:15:43 AM9/28/11
to django...@googlegroups.com
Koniiiik wrote:
> As for what you are doing, I don't think subclassing ForeignKeys is
> the way to go. Currently they defer most of the work to their target
> fields, you might try to do something similar with the features you're
> trying to add.

Problem solved: I just didn't have to put __metaclass__ = SubfieldBase in my ForeignKey subclass.

Tobia

guyf

unread,
Oct 24, 2011, 4:07:00 AM10/24/11
to Django users
Hi Tobia,

I have just discovered your post and I am trying to do something very
similar (create and XML doc from an object hierarchy).
I have started by trying to use the @toxml decorator and override the
__serialize__ method on my model. See (partially working) code below.

@toxml
class Risk(models.Model):
name = models.CharField(max_length=50, blank=True)
user = models.ForeignKey(User, related_name='risk')

def __unicode__(self):
return self.name

def __serialize__(self, stream):
stream.startElement(self._meta.object_name, {})
for field in self._meta.fields:
stream.startElement(field.name, {})
stream.characters(field.value_to_string(self))
stream.endElement(field.name)
for relobj in self._meta.get_all_related_objects():
o = relobj.name.partition(':')
m = models.get_model(o[0], o[2])
stream.startElement(m._meta.object_name, {})
for f in m._meta.fields:
stream.startElement(f.name, {})
stream.characters(f.value_to_string(m))
stream.endElement(f.name)
stream.endElement(o[2])
stream.endElement(self._meta.object_name)

My problem is 'stream.characters(f.value_to_string(m))' raises an
exception when the field is a Foreign Key.

However reading your question I am wondering if I am using the right
approach at all. Could you let me know if you think your method is
better and maybe provide a few code snippets to point me in the right
direction (if you think I am on the wrong track).

Thanks, Guy.

Tobia Conforto

unread,
Oct 24, 2011, 5:00:54 AM10/24/11
to Django users
Hi Guy

I do think my approach is better. Remember the Django motto "Don't
Repeat Yourself"! The serialization logic belongs to a library class,
not in your models. I can't give you my entire code (it belongs to my
employer) but I can point you to the right direction.

-Tobia

# your model:

class Risk(XmlModel):
# optionally supply an xpath to Xml fields:
# id = XmlAutoField(primary_key=True, xpath='@id')
name = XmlCharField(max_length=50, blank=True)
user = XmlForeignKey(User, related_name='risk')

# library classes:

class XmlModel:
class Meta:
abstract = True
def to_xml(self):
# here goes generic code (written and tested only once!) to
serialize
# any XmlModel instance to xml (I used ElementTree by the way).
# iterate over fields and only serialize instances of XmlBaseField,
# using special logic for instances of models.ForeignKey and
# related.ManyToOneRel, calling to_xml() on the related objects.

class XmlBaseField(object):
def __init__(self, *args, **kwargs):
# accept 'xpath' keyword argument, pop it and store it in self,
# defaulting to the field name if not provided, then call super

class XmlCharField(XmlBaseField, CharField):
__metaclass__ = SubfieldBase
def __init__(self, *args, **kwargs):
# just call super here

class XmlForeignKey(XmlBaseField, ForeignKey):
# no __metaclass__ goes here, which was my stumbling block
def __init__(self, *args, **kwargs):
# just call super here
Reply all
Reply to author
Forward
0 new messages