Using roles with Django

104 views
Skip to first unread message

Chokchai Phatharamalai

unread,
Nov 21, 2010, 1:12:33 PM11/21/10
to object-co...@googlegroups.com
Dear all, 

I managed to get the Roles 0.8 working with Django 1.2.3. I just want to share how I did it and hope to get some advises on how to improve the solution.

When I first tried to apply Roles in a Django project, I found a __metaclass__ conflict. After spent a day reading 2 parts of "Metaclass Programming in Python", I managed to fixed the conflict by creating another metaclass (named ModelBaseRoleType in the sample code) which inherits from both django.db.models.base.ModelBase and roles.RoleType. Then I created a Carpenter role using ModelBaseRoleType as its __metaclass__ (instead of roles.RoleType). 

The conflict is solved; however, I found that a model cannot be saved after applied a role as its class name has been changed and Django seems to inspect that class name in order to find which database to save the object to. So I avoid that by either applying a role using roles.clone as "method" parameter or revoking the applied role from the object before calling save.

Hopefully I did not already confuse you with my English (it never as good as I want it to be >.<). Let's look at the code.

from django.db.models import Model, CharField
from django.db.models.base import ModelBase

from roles import RoleType

class ModelBaseRoleType(ModelBase, RoleType):
    """
    As every Django model which inherits from django.db.models.Model already has 
    ModelBase as its __metaclass__, applying a RoleType to it would causes a 
    conflict because a Python class can apply only 1 metaclass. 

    In order to resolve the conflict, a new type named ModelBaseRoleType is 
    created.

    ModelBaseRoleType inherits from both Django's ModelBase and Roles' RoleType

    >>> from django.db.models import Model 
    >>> from roles import RoleType, clone
    >>> john = Person()
    >>> john.name = 'John'
    >>> john.save() # no problem saving John
    >>> john.id
    1
    >>> #----------------------------------------
    >>> #--- 1st strategy, using clone method ---
    >>> #----------------------------------------
    >>> jack = Person()
    >>> jack.name = 'Jack'
    >>> carpenter_jack = Carpenter(jack, method=clone)
    >>> carpenter_jack
    <Person+Carpenter: Person+Carpenter object>
    >>> carpenter_jack.chop()
    'chop, chop'
    >>> jack.save()
    >>> jack.id
    2
    >>> #----------------------------------------
    >>> #--- 2nd strategy, using Role's revoke --
    >>> #----------------------------------------
    >>> jack = Person()
    >>> jack.name = 'Jack'
    >>> Carpenter(jack)
    <Person+Carpenter: Person+Carpenter object>
    >>> jack.chop()
    'chop, chop'
    >>> Carpenter.revoke(jack)
    <Person: Person object>
    >>> jack.save()
    >>> jack.id
    3
    """
    pass

class Person(Model):
    name = CharField(max_length=20)

    #def __init__(self, name='', *args, **kwargs):
    #    super(Person, self).__init__(args, kwargs)
    #    self.name = name

class Carpenter(object):
    __metaclass__ = ModelBaseRoleType

    def chop(self):
        return 'chop, chop'
I also have a tests.py file which contains the same set of tests as in the doctest above, but with more details.
from carpenter.models import Person, Carpenter
from roles import RoleType, clone

from django.test import TestCase
from django.db import DatabaseError

class TestCarpenter(TestCase):
    def test_assign_role(self):
        """
        Role Carpenter is being assigned to a person 
        """
        p = Person('juacompe')
        Carpenter(p)

    def test_save_error(self):
        """
        As applying a role to an object changes object type's name, resulting
        in DatabaseError when trying to save the object, the applied object
        must exits all roles before it can call save(). 

        There are 2 strategies provided by roles library: using clone when 
        applying roles and revoke all role before calling save.
        """
        jack = Person()
        jack.name = 'Jack'
        Carpenter(jack)
        self.failUnless(isinstance(jack, Person))
        self.failUnless(isinstance(jack, Carpenter))
        jack.chop()
        self.assertRaises(AttributeError, jack.save)

    def test_save_using_clone(self):
        """
        1st strategy, using clone method
        """
        jack = Person()
        jack.name = 'Jack'
        # using clone when applying a role creates another object that uses
        # the same __dict__ as original
        carpenter_jack = Carpenter(jack, method=clone)
        self.failUnless(isinstance(carpenter_jack, Person))
        self.failUnless(isinstance(carpenter_jack, Carpenter))
        carpenter_jack.chop()
        carpenter_jack.name = 'Jack the giant killer'
        # nonetheless carpenter_jack cannot save
        self.assertRaises(AttributeError, carpenter_jack.save)
        # jack still can
        jack.save()
        self.assertEquals(1, jack.id)
        # changes applied to carpenter_jack affects jack
        self.assertEquals('Jack the giant killer', jack.name)
        
    def test_save_using_revoke(self):
        """
        2nd strategy, using Role's revoke
        """ 
        jack = Person()
        jack.name = 'Jack'
        Carpenter(jack)
        self.failUnless(isinstance(jack, Person))
        self.failUnless(isinstance(jack, Carpenter))
        jack.chop()
        jack.name = 'Jack the giant killer'
        # jack with Carpenter role cannot save
        self.assertRaises(AttributeError, jack.save)
        # revoke jack first
        Carpenter.revoke(jack)
        # then he can save
        jack.save()
        self.assertEquals(1, jack.id)
        self.assertEquals('Jack the giant killer', jack.name)

Any suggestions how can I improve the solution? Any comments or suggestions would be highly appreciated.

references:
- Metaclass Programming in Python Part 1, http://gnosis.cx/publish/programming/metaclass_1.html
- Metaclass Programming in Python Part 2, http://gnosis.cx/publish/programming/metaclass_2.html 


Thank you and best regards,
Chokchai Phatharamalai
Hyper-Productivity Seeker, Proteus Technology and
CTO, RC International School

Arjan Molenaar

unread,
Nov 21, 2010, 3:21:38 PM11/21/10
to object-co...@googlegroups.com, Benjamin Scherrey
Hi,

On 21 Nov 2010, at 19:12, Chokchai Phatharamalai wrote:

> Dear all,
>
> I managed to get the Roles 0.8 working with Django 1.2.3. I just want to share how I did it and hope to get some advises on how to improve the solution.
>
> When I first tried to apply Roles in a Django project, I found a __metaclass__ conflict. After spent a day reading 2 parts of "Metaclass Programming in Python", I managed to fixed the conflict by creating another metaclass (named ModelBaseRoleType in the sample code) which inherits from both django.db.models.base.ModelBase and roles.RoleType. Then I created a Carpenter role using ModelBaseRoleType as its __metaclass__ (instead of roles.RoleType).

This is what I recently tried and it seemed to work. This is the default way to fix a metaclass conflict.

> The conflict is solved; however, I found that a model cannot be saved after applied a role as its class name has been changed and Django seems to inspect that class name in order to find which database to save the object to.

Okay. So that's the caveat. :)

> So I avoid that by either applying a role using roles.clone as "method" parameter or revoking the applied role from the object before calling save.

In my example, the saving did not happen when the role is applied, hence I did not experience this problem. Personally I'm not a big fan of using the clone method, since it only clones the instance dict. Hence stuff like properties (and Django is all about properties in the model) do not work properly. I'd suggest revoking the roles before saving (which is simple if you use the in a 'with' block).

See: https://github.com/amolenaar/roles/blob/django/django_dci/account/tests.py (line 43-55).

> [snip code blocks]

> Any suggestions how can I improve the solution? Any comments or suggestions would be highly appreciated.

I'd settle for the revoke() method. If this poses to be a problem we can do something about the class name generation ;).

> references:
> - Metaclass Programming in Python Part 1, http://gnosis.cx/publish/programming/metaclass_1.html
> - Metaclass Programming in Python Part 2, http://gnosis.cx/publish/programming/metaclass_2.html
> - Roles 0.8, http://pypi.python.org/pypi/roles
> - Django 1.2.3, http://pypi.python.org/pypi/Django/1.2.3

Kind regards,

Arjan

Chokchai Phatharamalai

unread,
Nov 21, 2010, 9:33:46 PM11/21/10
to object-co...@googlegroups.com, Benjamin Scherrey
Dear Arjan,

Thank you very much for your kind response. I'll stick with the revoke approach then. I'll try persisting the account example you gave as it's more complex and practical than the one I tried. I'll keep everyone posted. 

Best regards,
Chokchai Phatharamalai
Hyper-Productivity Seeker, Proteus Technology and
CTO, RC International School



--
You received this message because you are subscribed to the Google Groups "object-composition" group.
To post to this group, send email to object-co...@googlegroups.com.
To unsubscribe from this group, send email to object-composit...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/object-composition?hl=en.



Chokchai Phatharamalai

unread,
Dec 12, 2010, 8:46:47 AM12/12/10
to object-co...@googlegroups.com, Benjamin Scherrey
Dear Arjan,

No more revoke-before-save is needed! Thanks to Bruno, I managed to have a roled Django model saved. :)

The problem was because applying a role to a model class triggers Django's multi-table inheritance, in which case a proxy class must be specified. It can be fixed by replacing 'pass' at line 20 at https://github.com/amolenaar/roles/blob/master/roles/django.py with 2 lines below:
-----------------
    class Meta: 
        proxy = True
-----------------

The 2 lines above tells Django that a role is a proxy class and doesn't need any table of its own. 

I created a simple Carpenter role and apply it at setUp in all test classes in my small financial project and all test pass. Quite happy with the result. :)

Ps.
- the 1st reference contains the post Ben (my boss) raised in Django group, Bruno's suggestions and traces of my experiments.

Best regards,
Chokchai P.

References
--

Arjan Molenaar

unread,
Dec 14, 2010, 8:34:18 AM12/14/10
to object-co...@googlegroups.com, Benjamin Scherrey
Thanks,

I updated the code and pushed it to github.

Reply all
Reply to author
Forward
0 new messages