11

I know I can create user groups and assign permission to them from the admin area in a django project. I can also create a group and assign permission to it by importing Group and Permission model from django's auth module.

What I want to know if there is any way I can create group and assign permission to them when I set up the project. So, if I have types of users, Admin, Developer, Tester and Project Manager. They are basically user groups which would have different permission level. I did not customize the User model and only can differentiate by the groups they are assigned to. So is there a way to create these groups and assign required permission to them like when permissions are created for admin when I run python manage.py migrate?

Sayantan Das
  • 1,619
  • 4
  • 24
  • 43

3 Answers3

16

You can define a post_migrate signal to create required User and Group model instances if they don't exist already.

When you create an application in using python manage.py startapp <app_name>, it creates an AppConfig class in apps.py file.

You can specify which signal to call in AppConfig class definition. Say the signal is called populate_models. In that case, modify AppConfig to look like following:

from django.apps import AppConfig
from django.db.models.signals import post_migrate

class AppConfig(AppConfig):
    name = 'app'

    def ready(self):
        from .signals import populate_models
        post_migrate.connect(populate_models, sender=self)

And in signals.py define the populate_models function.

def populate_models(sender, **kwargs):
    from django.contrib.auth.models import User
    from django.contrib.auth.models import group
    # create groups
    # assign permissions to groups
    # create users
Rolando Cruz
  • 2,834
  • 1
  • 16
  • 24
narendra-choudhary
  • 4,582
  • 4
  • 38
  • 58
  • So this signal is emitted after the initial migration or every migration? Do I need to check if the groups and permissions already exist and then add it? – Sayantan Das Mar 12 '17 at 06:54
  • Yes, signal is emitted after every `migrate` call. You will have to check if some `User` or `Group` already exists. You can use `get_or_create` shortcut. – narendra-choudhary Mar 12 '17 at 06:56
  • Or alternatively you can check if `User.objects.count()` is zero. – narendra-choudhary Mar 12 '17 at 06:58
  • 2
    There is no guarantee that this `post_migrate` handler will run after the handler defined by `django.contrib.auth` that creates the permissions. Hence this code could run and there are no permissions to assign to the groups. – Penguin Brian Sep 06 '17 at 02:59
  • @PenguinBrian do you know of a workaround for this? (I suppose "run the migration twice" could work but...) – ukrutt Nov 01 '17 at 23:17
  • Unfortunately, no, I don't have any real solution. Otherwise, I would have added a solution here. The method we implemented was to add a button that is manually invoked on to the admin site. – Penguin Brian Nov 03 '17 at 00:38
  • 1
    @PenguinBrian are you sure there is no guarantee? This solution worked for me. At first I thought it wasn't working but it turns out the problem was my function was running in every post_migrate handler for every app (I was using the `@receiver` decorator). Once I properly connected passing the sender, the problem was solved. If Django decides the order in which the handlers are called in a deterministic way, this should always work. Do you know it to be otherwise? Is the order non-deterministic? – Ariel Aug 17 '18 at 13:27
  • @Ariel @PenguinBrian from [the signal docs](https://docs.djangoproject.com/en/3.2/topics/signals/#listening-to-signals): the receiver functions are called in the order they were registered. And if you register your receiver on your app's `ready()` function (like how the snippet above and django's auth register the receiver), based on [the app docs](https://docs.djangoproject.com/en/3.2/ref/applications/#how-applications-are-loaded): the app's `ready()` stage is processed in the order of `INSTALLED_APPS`. – hashlash Aug 20 '21 at 11:33
5

According to @narenda-choudhary and @Rolando Cruz answer,

Here is the code for adding all existing permissions of an app under a Group. For exemple if you have App1 and App2, and you want to automatically create 2 groups named app1 and app2 containing permissions to the models of each app (respectively), try this :

For App1 on apps.py :

from django.apps import AppConfig
from django.db.models.signals import post_migrate

class App1Config(AppConfig):
    name = 'app1'

    def ready(self):
        from .signals import populate_models
        post_migrate.connect(populate_models, sender=self)

Create a signals.py file that contains :

def populate_models(sender, **kwargs):
    from django.apps import apps
    from .apps import App1Config
    from django.contrib.auth.models import Group, Permission
    from django.contrib.contenttypes.models import ContentType

    group_app, created = Group.objects.get_or_create(name=App1Config.name)

    models = apps.all_models[App1Config.name]
    for model in models:
        content_type = ContentType.objects.get(
            app_label=App1Config.name,
            model=model
        )
        permissions = Permission.objects.filter(content_type=content_type)
        group_app.permissions.add(*permissions)

Do the same for App2

Then assign users to their groups.

For usage :

Create a file called permissions.py on each app and add :

from .apps import App1Config

def is_in_group_app1(user):
    return user.groups.filter(name=App1Config.name).exists() 

In views.py use :

from django.contrib.auth.decorators import login_required, user_passes_test
from .permissions import is_in_group_app1

@login_required(login_url='where_to_redirect')
@user_passes_test(is_in_group_app1) 
def myview(request):
    # Do your processing

For CBV :

@method_decorator(user_passes_test(is_in_group_app1), name='dispatch')
class LogListView(ListView):
    """ Displays all logs saved on App1 """
    model= Logger.objects.order_by('-date_created')

Create a folder named templatestag and subfolder app1 and a has_perms.py file :

from django import template
from app1.permissions import is_in_group_app1
register = template.Library()

@register.filter
def has_perms(user):
    return is_in_group_app1(user)

In your template :

{% load has_perms %}

{% if request.user|has_perms %}
    <li class="nav-item">
        <a href="{% url 'app1:log' %}" class="nav-link">
            <i class="icon-history"></i>
            <span data-i18n="nav.dash.main">App1 Log</span>
        </a>
    </li>
{% endif %}

It took me a while to find all this process, so if it can help others :

Enjoy ;) !

HamzDiou
  • 588
  • 9
  • 15
1

Note: scroll down to see my code implementation.

Django automatically populates the database at least on the following:

Looking at the source code (django.contrib.contenttypes, django.contrib.auth, django.contrib.sites), I found the following pattern:

  • Connect the receiver function to post_migrate signal inside the AppConfig.

    Example from django.contrib.sites.apps:

    class SitesConfig(AppConfig):
        ...
    
        def ready(self):
            post_migrate.connect(create_default_site, sender=self)
            ...
    
  • Define the receiver function in the management module (instead of inside the signal module, which is usually used for defining the signal itself), and get the model class using apps.get_model() (instead of directly import the model class which could be inconsistent with the current state of migration).

    Example from django.contrib.sites.management:

    def create_default_site(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs):
        try:
            Site = apps.get_model('sites', 'Site')
        except LookupError:
            return
    
        if not router.allow_migrate_model(using, Site):
            return
    
        if not Site.objects.using(using).exists():
            ...
    

    The most important parameter here is apps for getting a model in the current migration state. If you use multiple databases, you'll also need the using parameter to check if the model is allowed to be migrated and to be used in your query's .using(). For other parameter explanations, check the post_migrate signal docs.

Talk is cheap. Show me the code!

  • I created an assign_group_permissions() function, which returns a receiver function that populates some permissions for a given group.

    someproject/accounts/management.py:

    from django import apps as global_apps
    from django.db.models import Q
    
    
    def assign_group_permissions(group_name, permissions):
        def receiver(*args, apps=global_apps, **kwargs):
            try:
                Group = apps.get_model('auth', 'Group')
                Permission = apps.get_model('auth', 'Permission')
            except LookupError:
                return
    
            perm_q = Q()
            for perm in permissions:
                app_label, codename = perm.split('.')
                perm_q |= Q(content_type__app_label=app_label) & Q(codename=codename)
    
            group, _ = Group.objects.get_or_create(name=group_name)
            group.permissions.add(
                *Permission.objects.filter(perm_q)
            )
    
        return receiver
    
  • Call the function inside each of the corresponding AppConfig. The example below shows how to assign view, add, change, and delete permissions of the Announcement model, which lives in someproject.announcements app to event_admin group.

    someproject/announcements/apps.py:

    from django.apps import AppConfig
    from django.db.models.signals import post_migrate
    
    from someproject.accounts.management import assign_group_permissions
    
    
    class AnnouncementConfig(AppConfig):
        name = 'someproject.announcements'
    
        def ready(self):
            post_migrate.connect(assign_group_permissions(
                'event_admin',
                [
                    'announcements.add_announcement',
                    'announcements.change_announcement',
                    'announcements.delete_announcement',
                    'announcements.view_announcement',
                ],
            ))
    
hashlash
  • 897
  • 8
  • 19