Search code examples
pythondjangocontinuous-integrationpytestpytest-django

Django post_save is not executed on CI/CD


I recently increased the default permissions for all my Django Views. In order for the users to have some default permissions I add new users to a default Group that has mainly viewing permissions. This is done via a post_save signal.

MODELS_VIEW = [
    "time entry",
    "time entry version",
    "project",
    "client",
    "user",
]
MODELS_CHANGE = ["time entry version", "user"]
MODELS_CREATE = ["time entry version"]
MODELS_DELETE = ["time entry version"]


def _add_permissions_to_group(group, perm_type: str, models: List[str]) -> None:
    """Add the permission of the `perm_type` to the given models."""
    from django.contrib.auth.models import Permission

    for model in models:
        perm_name = f"Can {perm_type} {model}"
        permission = Permission.objects.get(name=perm_name)
        group.permissions.add(permission)


def _add_view_permissions_to_group(group) -> None:
    _add_permissions_to_group(group, perm_type="view", models=MODELS_VIEW)


def _add_create_permissions_to_group(group) -> None:
    _add_permissions_to_group(group, perm_type="add", models=MODELS_CREATE)


def _add_change_permissions_to_group(group) -> None:
    _add_permissions_to_group(group, perm_type="change", models=MODELS_CHANGE)


def _add_delete_permissions_to_group(group) -> None:
    _add_permissions_to_group(group, perm_type="delete", models=MODELS_DELETE)


def _get_default_user_group_with_permissions():
    """Get or create the default UserGroup and add all standard permissions."""
    from django.contrib.auth.models import Group

    logger.debug("Getting default group for user")
    group, _ = Group.objects.get_or_create(name="default")
    # Add the standard permissions for users
    _add_view_permissions_to_group(group)
    _add_create_permissions_to_group(group)
    _add_change_permissions_to_group(group)
    _add_delete_permissions_to_group(group)

    return group


class UsersConfig(AppConfig):
    """Config for Users"""

    default_auto_field = "django.db.models.BigAutoField"
    name = "users"

    def ready(self):
        """Add the user to the default UserGroup."""
        logger.debug("User-config: App ready")

        def _add_to_default_group(sender, **kwargs):
            """Add the user to the default group on creation."""
            if kwargs["created"]:
                group = _get_default_user_group_with_permissions()
                user = kwargs["instance"]
                logger.debug(f"Adding user {user} to group {group}")
                user.groups.add(group)

        post_save.connect(_add_to_default_group, sender=settings.AUTH_USER_MODEL)

I have a fixture in my tests that creates a user and the following test using that fixture:

@pytest.fixture()
def user_max(django_user_model):
    """Create a normal user called max."""
    logger.debug("Creating user max")
    user = django_user_model.objects.create_user(
        username="max",
        email="[email protected]",
        password="password",
    )
    logger.debug(f"Permissions after creating: {user.get_all_permissions()}")
    return user


def test_default_permissions(django_user_model):
    """Test default permissions."""
    
    print("Group default permissions: ", Group.objects.get(name="default").permissions.all())
    print(
        "Permissions of user_max: ",
        django_user_model.objects.get(id=django_user_model.objects.get(username="max").id).get_all_permissions(),
    )
    assert django_user_model.objects.get(username="max").id).get_all_permissions().count() > 5

When running the test I receive the debug messages from ready()-function, _get_default_user_group_with_permissions()-function as well as the test:

2023-04-03 11:13:05,685 - conftest - DEBUG - Creating user max
2023-04-03 11:13:05,900 - users.apps - DEBUG - Getting default group for user
2023-04-03 11:13:05,978 - users.apps - DEBUG - Adding user max to group default
2023-04-03 11:13:05,995 - conftest - DEBUG - Permissions after creating: {'projects.view_task', 'projects.view_project', 'bookings.delete_timeentryversion', 'projects.view_projecttype', 'bookings.view_timeentry', 'bookings.view_timeentryversion', 'clients.view_client', 'users.view_resource', 'projects.view_taskgroup', 'users.change_user', 'users.view_team', 'bookings.change_timeentryversion', 'bookings.add_timeentryversion', 'users.view_user'}

Now to my issue: When running the same code on Gitlab CI/CD this test errors because no default-Group is created and there are neither debug messages from the ready()-function nor the _get_default_user_group_with_permissions()-function:

2023-04-03 10:31:07,963 - conftest - DEBUG - Creating user max
2023-04-03 10:31:08,293 - conftest - DEBUG - Permissions after creating: set()

How come that the code from ready() is not executed in the CI/CD Pipeline?


Solution

  • I finally found the issue. According to the docs:

    Note also that Django stores signal handlers as weak references by default, so if your handler is a local function, it may be garbage collected. To prevent this, pass weak=False when you call the signal’s connect().

    Apparently garbage collection on the CI/CD pipeline was removing the reference with the default value weak=True. It worked when setting weak=False in the post_save:

    post_save.connect(_add_to_default_group, sender=settings.AUTH_USER_MODEL, weak=False)