Dynamic Meta attributes for Django Models?
Asked Answered
I

3

11

I am trying to add a dynamic Meta attribute to all of my Django models using model inheritance, but I can't get it to work. I have a permission that I want to add to all my models like this:

class ModelA(models.Model):
    class Meta:
        permisssions =(('view_modela','Can view Model A'),)   

class ModelB(models.Model):
    class Meta:
        permisssions =(('view_modelb','Can view Model B'),)

I tried creating an abstract base class like this:

class CustomModel(models.Model):
    def __init__(self, *args, **kwargs):
        self._meta.permissions.append(('view_'+self._meta.module_name, u'Can view %s' % self._meta.verbose_name))
        super(CustomModel,self).__init__(*args, **kwargs)

class ModelA(CustomModel):
    ....

class ModelB(CustomModel):
    ...

but it's not working. Is this the right approach? Because Django uses introspection to construct the Model classes, I'm not sure if adding permissions during the __init__() of the class will even work. With my current implementation every time I access a model instance it appends another tuple of the permissions.

Inrush answered 7/4, 2009 at 14:7 Comment(0)
L
27

Your instinct is right that this won't work. In Django, permissions are stored in the database, which means that:

  • they need to be available at the class level when syncdb is run in order to populate the auth_permission table (and your approach requires an instance, which won't be made during syncdb)
  • even if you did add it to _meta.permissions in __init__, the User object wouldn't pick it up in any permission check calls because those consult the permissions table in the DB (and a cache of that table, at that).

Your goal can't be accomplished using inheritance. What you actually need here is a Python metaclass.

This metaclass re-writes your ModelA and ModelB class definitions dynamically before they are defined, thus it doesn't require a ModelA instance, and is available to syncdb. Since Django's models also use metaclasses to build the Meta object in the first place, the only requirement is that your metaclass must inherit from the same metaclass as Django's models.

Here's some sample code (Python 2):

from django.db.models.base import ModelBase

class CustomModelMetaClass(ModelBase):

    def __new__(cls, name, bases, attrs):
        klas = super(CustomModelMetaClass, cls).__new__(cls, name, bases, attrs)
        klas._meta.permissions.append(
            (
                'view_{0.module_name}'.format(klas._meta),
                u'Can view {0.verbose_name}'.format(klas._meta))
        )

        return klas

class ModelA(models.Model):

    __metaclass__ = CustomModelMetaClass

    test = models.CharField(max_length=5)

Python 3:

from django.db.models.base import ModelBase

class CustomModelMetaClass(ModelBase):

    def __new__(cls, name, bases, attrs):
        klas = super().__new__(cls, name, bases, attrs)
        klas._meta.permissions.append(
            (
                'view_{0.module_name}'.format(klas._meta),
                'Can view {0.verbose_name}'.format(klas._meta))
        )

        return klas

class ModelA(models.Model, metaclass=CustomModelMetaClass):

    test = models.CharField(max_length=5)

Note that permissions in this case will be written only on migrate. If you need to change permissions dynamically at run time base on the user, you'll want to provide your own authentication backend.

Liturgy answered 7/4, 2009 at 23:15 Comment(5)
+1 for an example of writing a custom metaclass in a django-related question. this stuff is hard to find!Vermicelli
I know this is what upvotes are for, and this is an old thread, but... damn, Jarret. The encyclopedic level of knowledge of Django and Python needed to come up with this answer and then explain simply and clearly is mind-boggling. Thank you for making the Internet a more useful place.Stalagmite
I don't know who you are, @General_Mayhem, but you just made my year. Thanks for making nice comments on such an old post!Liturgy
I'm a beginning Django dev who was trying (and has now succeeded) to solve the same problem as OP, but who didn't even know that Python had metaclasses, let alone exactly how to apply them in this situation, and was getting very frustrated with how many of his problems with Django wind up being absolutely impossible to Google because every single page about Django uses all of its keywords.Stalagmite
Note that for Python 3 you need class ModelA(models.Model, metaclass=CustomModelMetaClass) (instead of __metaclass__).Incongruous
E
1

Try to use a custom manager:

#create a custom manager
class DynTableNameManager(models.Manager):
    #overwrite all() (example)
    #provide table_name
    def all(self, table_name):
        from django.db import connection
        cursor = connection.cursor()
        cursor.execute("""
            SELECT id, name
            FROM %s
            """ % table_name)
        result_list = []
        for row in cursor.fetchall():
            p = self.model(id=row[0], name=row[1])
            result_list.append(p)
        return result_list

#cerate a dummy table
class DummyTable(models.Model):
    name = models.CharField ( max_length = 200 )
    objects = DynTableNameManager()

use like this:

f = DummyTable.objects.all('my_table_name')
Edomite answered 25/2, 2010 at 12:40 Comment(2)
i forgot to mention: this only works if your table structure the same for each table.Edomite
cannot use filter, thus too long for query if multiple tables with bigger size.Outwards
P
0

In addition to Jarret Hardie solution:

In Django 4.2 you cannot add permissions to a metaclass unless you add a permissions declaration to the model Meta (Django migrations will ignore your custom permissions). So you need either define permissions in the model Meta or add it in your metaclass before creating ModelBase instance.

You can also add your permissions without accessing the _meta attribute at all. Just add permissions to attrs and pass them to the __new__ method. Here is my example of how to fix the migrations issue and add permissions without access to _meta:

class CustomModelMetaClass(ModelBase):
    def __new__(cls, name, bases, attrs):

        # check the `Meta` class existing
        if not attrs.get('Meta'):
            # creating a new `Meta` class inline
            # (read the `type` documentation for more information)
            attrs['Meta'] = type('Meta', tuple(), {})

        meta = attrs['Meta']

        # add permissions only into not abstract classes
        if not getattr(meta, 'abstract', False):

            if not hasattr(meta, 'permissions'):
                setattr(meta, 'permissions', [])

            meta.permissions.append(
                # just example of the permission
                # (you still can get `verbose_name` and etc. from the `meta`)
                (f'view_{name}_self', f'Can view self {name}'),
            )

        return super().__new__(cls, name, bases, attrs)

P.S.: Python type documentation

Pollination answered 4/7 at 8:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.