Why groups are not populated upon `auth_permission`?

Upon Django I made this migration:


# Generated by Django 5.2.17 on 2026-09-19 15:23

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('blog', '0001_initial'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='article',
            options={'permissions': [('create_article', 'Permission for a User to create an Article'), ('update_article', 'Permission for a user to edit an article')]},
        ),
    ]

But upon auth_persmission the permission create_article and update_article are not populated:

id content_type_id codename name
1 1 add_logentry Can add log entry
2 1 change_logentry Can change log entry
3 1 delete_logentry Can delete log entry
4 1 view_logentry Can view log entry
5 2 add_permission Can add permission
6 2 change_permission Can change permission
7 2 delete_permission Can delete permission
8 2 view_permission Can view permission
9 3 add_group Can add group
10 3 change_group Can change group
11 3 delete_group Can delete group
12 3 view_group Can view group
13 4 add_contenttype Can add content type
14 4 change_contenttype Can change content type
15 4 delete_contenttype Can delete content type
16 4 view_contenttype Can view content type
17 5 add_session Can add session
18 5 change_session Can change session
19 5 delete_session Can delete session
20 5 view_session Can view session
21 6 add_category Can add category
22 6 change_category Can change category
23 6 delete_category Can delete category
24 6 view_category Can view category
25 7 add_article Can add article
26 7 change_article Can change article
27 7 delete_article Can delete article
28 7 view_article Can view article
29 8 add_user Can add user
30 8 change_user Can change user
31 8 delete_user Can delete user
32 8 view_user Can view user

Why though??? As far as I understand upon auth_permission the permissions are stored then once I do upon models.py:

class Article(models.Model):
    id=models.AutoField(primary_key=True)
    title=models.CharField(max_length=255)
    content=models.TextField()
    slug=models.SlugField()
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    category = models.ManyToManyField(Category)

    class Meta:
        permissions = [
            ("create_article","Permission for a User to create an Article"),
            ("update_article","Permission for a user to edit an article")
        ]

But the:

python manage.py makemigrations
python manage.py migrate

Does not create the persmission upon db. Do you know why???

I think that the 3 main issues with the current approach:

  • No Database Transactions: In case super().create() succeeded, but the profile creation failed (for example, because of constraint violation in professional_title field), you would have a User object in your database without a profile.
  • Signal vs Serializer Anti-Pattern: Using an implicit logic through signal (post_save creates the profile) and explicit logic through serializer (serializer updates the profile right away) is an anti-pattern. You end up having extra database queries (INSERT + right away UPDATE), which makes debugging harder.
  • WET Code (not DRY): The very same .create() is copied in both CustomUserCreateSerializer and CustomUserCreatePasswordRetypeSerializer.

Thus, in order to solve this problem, you shall do the following two things:

  1. Drop the post_save signal : Since you manage the profile creation manually, drop the post_save signal altogether. Explicit is always better than implicit. Handle it in the serializer (or model manager).

  2. Use a Mixin and transaction.atomic(): Use a mixin in order to not repeat yourself and use an atomic transaction for this creation.

from django.db import transaction
from rest_framework import serializers
from djoser.serializers import UserCreateSerializer, UserCreatePasswordRetypeSerializer
from .models import User, TeacherProfile, StudentProfile
from .serializers import TeacherProfileSerializer, StudentProfileSerializer

class ProfileCreationMixin:
    """Mixin to extract profile data and create profiles atomically."""
    
    def validate(self, attrs):
        role = attrs.get('role')
        if role == User.TEACHER and not attrs.get('teacher_profile'):
            raise serializers.ValidationError({"teacher_profile": "This field is required for teachers."})
        if role == User.STUDENT and not attrs.get('student_profile'):
            raise serializers.ValidationError({"student_profile": "This field is required for students."})
        
        return super().validate(attrs)

    @transaction.atomic
    def create(self, validated_data):
        teacher_profile_data = validated_data.pop("teacher_profile", None)
        student_profile_data = validated_data.pop("student_profile", None)

        user = super().create(validated_data)

        if user.role == User.TEACHER and teacher_profile_data:
            TeacherProfile.objects.create(user=user, **teacher_profile_data)
        
        elif user.role == User.STUDENT and student_profile_data:
            StudentProfile.objects.create(user=user, **student_profile_data)

        return user


# Apply the mixin to your serializers
class CustomUserCreateSerializer(ProfileCreationMixin, UserCreateSerializer):
    teacher_profile = TeacherProfileSerializer(required=False)
    student_profile = StudentProfileSerializer(required=False)

    class Meta(UserCreateSerializer.Meta):
        model = User
        fields = (
            "id", "email", "password", "role", "first_name", "last_name",
            "teacher_profile", "student_profile",
        )


class CustomUserCreatePasswordRetypeSerializer(ProfileCreationMixin, UserCreatePasswordRetypeSerializer):
    teacher_profile = TeacherProfileSerializer(required=False)
    student_profile = StudentProfileSerializer(required=False)

    class Meta(UserCreatePasswordRetypeSerializer.Meta):
        model = User
        fields = (
            "id", "email", "password", "re_password", "role", "first_name", "last_name",
            "teacher_profile", "student_profile",
        )
Вернуться на верх