astorije astorije - 1 month ago 13
Python Question

Django custom model fields: to_python() not called

I am quite new to Python and Django, and totally new on Stack Overflow, so I hope I won't break any rules here and I respect the question format.

I am facing a problem trying to implement a custom model field with Django (Python 3.3.0, Django 1.5a1), and I didn't find any similar topics, I am actually quite stuck on this one...

So there is a Player, he has got a Hand (of Card). The Hand inherits from CardContainer, which is basically a list of cards with some (hidden here) helper functions.
Here is the corresponding code:

from django.db import models


class Card:
def __init__(self, id):
self.id = id


class CardContainer:
def __init__(self, cards=None):
if cards is None:
cards = []
self.cards = cards


class Hand(CardContainer):
def __init__(self, cards=None):
super(Hand, self).__init__(cards)


class CardContainerField(models.CommaSeparatedIntegerField):
__metaclass__ = models.SubfieldBase

def __init__(self, cls, *args, **kwargs):
if not issubclass(cls, CardContainer):
raise TypeError('{} is not a subclass of CardContainer'.format(cls))

self.cls = cls
kwargs['max_length'] = 10
super(CardContainerField, self).__init__(*args, **kwargs)

def to_python(self, value):
if not value:
return self.cls()

if isinstance(value, self.cls):
return value

if isinstance(value, list):
return self.cls([i if isinstance(i, Card) else Card(i) for i in value])

# String: '1,2,3,...'
return self.cls([Card(int(i)) for i in value.split(',')])

def get_prep_value(self, value):
if value is None:
return ''

return ','.join([str(card.id) for card in value.cards])


class Player(models.Model):
hand = CardContainerField(Hand)


But when I get a player, lets say, like this:
Player.objects.get(id=3).hand
, instead of getting a
Hand
instance (or even a
CardContainer
instance at all!), I am just getting a comma-separated string of integers like "1,2,3", which is fine in the database (it is the format I'd like to see IN the database)...

It seems to me that to_python doesn't get called, so the returned data is the raw value, hence the string. When I searched for this type of problems, people missed the
__metaclass__ = models.SubfieldBase
... I hoped I could have missed that too but, hey, it would have been too simple!
Did I miss something trivial, or am I wrong for the whole thing? :D

Thanks a lot!!

Answer

In python 3 the module-global __metaclass__ variable is no longer supported. You must use:

class CardContainerField(models.CommaSeparatedIntegerField, metaclass=models.SubfieldBase):
   ...
Comments