Showing posts with label django-models. Show all posts
Showing posts with label django-models. Show all posts

Thursday, January 14, 2010

Accessing Inherited Models from the Parent in Django

One of the neat features of Django's ORM is Model inheritance (table-level). It allows several neat data design patterns to occur. Here's an example. Let's say we're developing a website for a game company. The company sells two types of products: board games and video games. All of the products will share some data in common, name and product_id for example, but we also need to store specific details about each. Using model inheritance we can do something as follows.

class Product(models.Model):
name = models.CharField(max_length=75)
product_id = models.SmallIntegerField()
price = models.DecimalField()

class BoardGame(Product):
num_of_players = models.SmallIntegerField()
game_type = models.CharField(max_length=50)

class VideoGame(Product):
PLATFORM_CHOICES = (
('wii', 'Wii),
('xb3', 'Xbox 360'),
('ps3', 'Playstation 3'),
)
platform = models.CharField(max_length=3, choices=PLATFORM_CHOICES)
In a real use-case scenario you'd most likely have more than 1 field per, but for this example I wanted to keep things simple.

The way Django implements this, if you were to query one of the child models, you'd be able to access the methods from the parent models...
b = BoardGame.objects.all()[1]
print b.name

>>> 'Djangopoly'
Another thing that's cool is child instances have a parent instance record. Using the "Djangopoloy" game from above, which is technically type BoardGame, one could still query Product and retrieve it.
p = Product.objects.get(name='Djangopoly')
This is really useful, but sometimes you need to go the opposite direction, and this is where Django's implementation stops. The link can't go from a Product model instance to a BoardGame. It can't retrieve state as if it was of type BoardGame.
print p.platform

>>> CAN'T DO THAT!
Because the need for this seems to be arising more often than not lately for me, I put together a re-usbale bit of code to overcome this limitation. I'll post the code below (a GitHub gist), but using it is actually quite simple.

It works by providing an abstract model that the parent model inherits from instead of models.Model:
from inheritance.models import ChildAwareModel

class Product(ChildAwareModel):
...

pass
Then, an inner class "Inheritance" is supplied to describe children of the model.
class Product(ChildAwareModel):
...

class Inheritance:
children = (
'myapp.models.BoardGame',
'myapp.mdoels.VideoGame',
)
Only children that need to be reversed to should be set. Once that is configured, a method "get_child_model()" will become available, and can be used like so:
p = Product.objects.get(name='Djangopoly')
b = p.get_child_model()
print b.num_of_players

>>> 4
I'm finding this particularly useful when I've created an aggregate type page -- that is a page that shows a summary of all the generic types (Product) -- but need to on user-click show them some type of product-specific detail.

The implementation for ChildAwareModel is below. Save it somewhere on your python path and enjoy. :)


ChildAwareModel Gist

Saturday, June 6, 2009

Determine Model Change v2

Well I got around to doing a 2nd revision on my model change code (being the weekend I was wondering if it would come to pass). Per a suggestion by "thepointer" (#django IRC on freenode), I switched the code from using Python's generic vars() to Django's interal _meta. Using an internal API is probably not the ultimate best, but _meta has been stable and unchanged for quite a while.

I also added a "human_friendly" mode, which will take the model change and attempt to turn it into an understandable statement (string) about what exactly has changed. It still returns it in a dictionary with the field name as the key.

from django.db import models

def determine_model_change(old_model, new_model, human_friendly=False, ignore_fields={}):
"""
Compares the two models against each other, returning a dictionary of
values that have changed (new value only).

Setting human_friendly=True will cause ignore internal fields like
SlugField. It will also attempt to parse a meaningful statement for
the model change. ie 'Event date is now 5/7/2009'
"""
not_human_fields = (
models.fields.SlugField,

models.fields.FilePathField,
models.IPAddressField,
models.FileField,
models.ImageField,
models.XMLField,
)
changed = {}

if isinstance(old_model, models.Model) and isinstance(new_model, models.Model):
for f in new_model._meta.fields:
if not f.name in ignore_fields:
new_value = getattr(new_model, f.name, '')
old_value = getattr(old_model, f.name, '')
if cmp(new_value, old_value) != 0:
if human_friendly:
if not type(f) in not_human_fields:
changed[f.name] = __verbose_field_change(old_model, new_model, f)
else:
changed[f.name] = new_value

return changed

def __verbose_field_change(old_model, new_model, field):
"""
Returns the human-friendly text for a field change
"""
value = getattr(new_model, field.name)
if isinstance(field, models.fields.DateField) or \
isinstance(field, models.fields.TimeField) or \
isinstance(field, models.fields.DateTimeField):

value = value.strftime('%b %d, %Y %I:%M %p')
return '%s %s has changed to %s' % (
old_model,
field.verbose_name,
value
)


# -------------
# Sample usage:
# -------------

>>>
>>> from happenings.models import *
>>> import copy, datetime
>>>
>>> event = Event.objects.get(pk=1)
>>> event.name = "My Birthday"
>>> event.start_time = datetime.datetime(2009, 7, 5, 0, 0)
>>>
>>> newevent = copy.copy(event)
>>> newevent.start_time = datetime.datetime(2009, 7, 15, 0, 0)
>>>
>>> determine_model_change(event, newevent)
{'start_time': datetime.datetime(2009, 7, 15, 0, 0)}
>>>
>>> determine_model_change(event, newevent, human_friendly=True)
{'start_time': 'My Birthday start time has changed to Jul 15, 2009 12:00 AM'}

Friday, June 5, 2009

Django Model Comparison

I started working more with django-notification today. However, my implementation (outside of Pynax) is to create a subscription app, particularly for subscribing to event or calendar updates. Using Django's signals, I've got most of it setup. What I needed was a way to tell the difference between the old and new event, so that the notification could actually tell you "The start time for Event X has changed to Y:ZX" instead of "An event you're watching has been updated!".

So to do that I needed some model comparision code. Here's version 1.


def determine_model_change(old_model, new_model):
"""
Compares the two models against each other, returning a dictionary of

values that have changed (new value only).
"""
new_values = vars(new_model)
old_values = vars(old_model)

changed = {}

for key in new_values:

# Skip internal and 'magical' properties
if not key.startswith('_'):
if key in old_values:

if cmp(new_values[key], old_values[key])!= 0:

changed[key] = new_values[key]
else:

# Save the new value if it is not in the old values
changed[key] = new_values[key]
return changed

# Sample run:
# >>> class SomeModel(models.Model):
# >>> name = models.CharField()
# >>> age = models.IntegerField()

# >>>
# >>> foo1 = SomeModel()
# >>> foo1.name = "Some Model Name"
# >>> foo1.age = 14
# >>>
# >>> foo2 = SomeModel()

# >>> foo2.name = "Foo Model 2"
# >>> foo2.age = 14
# >>>
# >>> determine_model_change(foo1, foo2)
# {'name': 'Foo Model 2'}

  © Blogger template 'Minimalist G' by Ourblogtemplates.com 2008

Back to TOP