2

I would like to add a can_view Meta permission to every model in my Django app.

Pretty much I want to add this to every class in models.py

class Meta:
    permissions = [ ( "can_view", "Can view {something}".format( something = self.verbose_name ) ]

I'm not even sure if self.verbose_name would even work like that in this case....

Is this possible?

Bonus question: Once I add a permission inside the model's Meta then I can call it with has_perm right? Like

if request.user.has_perm( 'polls.can_view' ) :
    # Show a list of polls.
else :
    # Say "Insufficient permissions" or something.    
jpic
  • 32,891
  • 5
  • 112
  • 113
hobbes3
  • 28,078
  • 24
  • 87
  • 116
  • 1
    I was wondering myself but in the end I have only a handful of models that have a detailview so I didn't bother trying to enable the view permission on all models. But I understand that on a project with many detailview it can be tedious to do them one by one. On the other hand I did make a base view class to inherit from that checks for permission: https://gist.github.com/97b3a0f17aa6e4d35da8 so that might help. If you don't use django-guardian then you can still adapt it easily. Also: don't forget to tag your questions with the language (I added the python tag) – jpic Mar 30 '12 at 08:46
  • @jpic Ya, I was wondering why Django doesn't have use `can_view` as one of their default permissions. Well I only have like 5 models that would need these permissions. Maybe I could override Django's `models.Model` class? It's probably not worth it. By the way, thanks for helping me out with Django the past 2 months. You're like my private mentor here in SO :-). Do you regularly check my questions or something lol? – hobbes3 Mar 30 '12 at 08:53

3 Answers3

6

Permission is also a just normal Django model instance. You can create it like any other model.

So you need something like

from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

for content_type in ContentType.objects.all():
     Permission.objects.create(content_type=content_type, codename='view_%s' % content_type.model, name='Can view %s' % content_type.name)

You need to do it once, so post_syncdb signal looks like a good place for that.

2023 Edit

post_syncdb is now post_migrate, and works slightly differently. Here is an example of using post_migrate in app my_app to put the permission "override_<model_name>" on all of your models.

This config should 'just work' if you have my_app in your INSTALLED_APPS, or you could alternatively replace 'my_app' with 'rest.apps.MyAppConfig', although I don't know the implications of doing that. I also changed create() to get_or_create() because the signal will re-run this code after every migration.

# In my_app/apps.py
from django.apps import AppConfig
from django.db.models.signals import post_migrate


def create_override_permissions(sender, **kwargs):
    # Imported here so django is finished setting up
    # Otherwise you will get AppRegistryNotReady
    from django.contrib.auth.models import Permission
    from django.contrib.contenttypes.models import ContentType
    for content_type in ContentType.objects.all():
        Permission.objects.get_or_create(content_type=content_type,
                                  codename='override_%s' % content_type.model,
                                  name='Can override disabled buttons for %s' % content_type.name)


class MyAppConfig(AppConfig):
    name = 'my_app'

    def ready(self):
        post_migrate.connect(create_override_permissions, sender=self)
Bradleo
  • 167
  • 9
DrTyrsa
  • 31,014
  • 7
  • 86
  • 86
  • Where should I put this code and where should I put the `post_syncdb` signal code? – hobbes3 Apr 01 '12 at 23:02
  • 3
    For future reference for others: "You can put signal handling and registration code anywhere you like. However, you'll need to make sure that the module it's in gets imported early on so that the signal handling gets registered before any signals need to be sent. This makes your app's __models.py__ a good place to put registration of signal handlers." – hobbes3 Apr 02 '12 at 10:28
  • 3
    @hobbes3 I prefer to put it in file named `signals.py` and to import it in the end of `models.py` – DrTyrsa Apr 02 '12 at 11:14
  • By the way, in case anyone is interested, `post_syncdb` is called for *every* app installed in `setttings.py`. So you need to place a line like `if Permission.objects.filter( content_type_id = content_type.pk, codename__contains = 'view_' ) is None :` to check to make sure that the permissions aren't applied more than once per `content_type`. – hobbes3 Apr 02 '12 at 19:53
  • @hobbes3 [Read about](https://docs.djangoproject.com/en/dev/ref/signals/#post-syncdb) `sender` arg. – DrTyrsa Apr 03 '12 at 06:26
  • @DrTyrsa Ya, I found out the hard way with a bunch of `print` statement originally (now I use `logging` :D), then I found out in the documentation as well. By the way, the correct if statement is actually `if not Permission.objects.filter( content_type_id = content_type.pk, codename__contains = 'view_' ) : `, since `filter()` returns an empty array not `None` if it can't find matches. – hobbes3 Apr 03 '12 at 06:41
  • @hobbes3 You don't need this, you just need to connect the signal to the specific sender. And don't use `filter` in such situations, use [exists](https://docs.djangoproject.com/en/dev/ref/models/querysets/#exists). – DrTyrsa Apr 03 '12 at 06:57
  • Actually I think [`get_or_create`](https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.get_or_create) is better in this case, since it checks *and* create if necessary. I just realized every model but the models in the app I created gets the `view_` permissions. In other words, the permissions only gets applied to the built-in Django models like `comment`, `group`, `logentry`, `permission`, `site`, `user`, etc. – hobbes3 Apr 03 '12 at 07:19
  • The post_syncdb signal has been renamed to post_migrate. Working docs link here: https://docs.djangoproject.com/en/dev/ref/signals/#post-migrate – Bradleo Jul 17 '23 at 20:32
2

The most simplest and obvious approach would be a custom base model that is a parent of all your models. This way you (or another programmer) will ever wonder where the heck can_view is coming from.

class CustomBaseModel(models.Model):
    class Meta:
        abstract = True
        permissions = [ ( "can_view", "Can view {something}".format( something = self.verbose_name ) ]

class SomeModel(CustomBaseModel):
    # ...

However, this requires you to change all your models (this is easily done with a little search & replace) and it won't change Djangos builtin models (like User).

Martin Thurau
  • 7,564
  • 7
  • 43
  • 80
  • This will cause `NameError: name 'self' is not defined` – DrTyrsa Mar 30 '12 at 08:56
  • How *would* I add `can_view` to `User` as well? I guess this is more of a Python question. I know how to inherit and extend classes, but that would give it a different name. How can I *replace* and extend these built-in Django classes and keep the same name (without modifying the source code)? Kind of like customizing Django admin page by overriding their templates with yours. – hobbes3 Mar 30 '12 at 08:57
  • Or maybe I should apply the `can_view` to `UserProfile`? Well either way, my question was more from a Python perspective, not Django, so I would still like to know a solution to my hypothetical question. – hobbes3 Mar 30 '12 at 09:00
  • As @DrTyrsa pointed out, although this looks simple, Django doesn't support this easily. See https://stackoverflow.com/questions/2964947/django-inherit-permissions-from-abstract-models?rq=4 Handling this better has been an open ticket in Django for 14 years https://code.djangoproject.com/ticket/10686 https://github.com/django/django/pull/15936 – Bradleo Jul 17 '23 at 16:04
0

As someone else suggested in another answer, an abstract class is an obvious approach. However, the code given in that answer throws errors.

To be frank, doing this with an abstract class (as of summer 2023) is clunky and needs work arounds. Testing this is also hard, because I don't think Django removes any permissions from the auth_permission table when undoing migrations (migrate my_app 0022 where 0022 was the prefix of a previous migration)

What does NOT work:

# BROKEN, DOES NOT WORK
class MyAppBaseModel(models.Model):
    class Meta:
        abstract = True
        permissions = (("see_details_%(class)s", "Can see details of %(class)s"),)

class ChildModel(MyAppBaseModel):
    class Meta:
        # Ideally would result in model getting default permissions plus "see_details_childmodel" and "foobar_childmodel"
        permissions =  (("foobar_childmodel", "Can foobar childmodel"),)

What DOES work (source):

  • fairly short
  • takes advantage of how Django does meta-class inheritance (needs explanation with comments)
  • Adds to the default permissions (from what I could see)
  • ALL OBJECTS GET THE SAME PERMISSION NAME

Code:

class AbstractBaseModel(models.Model):
    class Meta:
        abstract = True
        permissions = (("see_details_generic","See details (Generic)"),) # <-- Trailing comma


class SomeClass(AbstractBaseModel):
    name = models.CharField(max_length=255,verbose_name="Name")

    class Meta(AbstractBaseModel.Meta): # <--- Meta Inheritance
        # Results in child getting default permissions plus "see_details_generic" (NOT view_childmodel)
        pass
        # Still trying to figure out how to get this to work
        # As written, if use the below instead of pass, it removes see_details_generic
        # permissions =  (("foobar_childmodel", "Can foobar childmodel"),)

P.S. An improvement to Django to help make the broken example work has been kicked around for 14 years. Hoping someone could finish it up on Github.

Bradleo
  • 167
  • 9