Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: signal handlers sync project views and tasks #1218

Draft
wants to merge 4 commits into
base: 2.3.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion rdmo/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,8 @@

PROJECT_SEND_INVITE = True

PROJECT_REMOVE_VIEWS = True
PROJECT_VIEWS_SYNC = True
PROJECT_TASKS_SYNC = True

PROJECT_CREATE_RESTRICTED = False
PROJECT_CREATE_GROUPS = []
Expand Down
6 changes: 4 additions & 2 deletions rdmo/projects/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ class ProjectsConfig(AppConfig):
def ready(self):
from . import rules # noqa: F401

if settings.PROJECT_REMOVE_VIEWS:
from . import handlers # noqa: F401
if settings.PROJECT_VIEWS_SYNC:
from .handlers import project_views # noqa: F401
if settings.PROJECT_TASKS_SYNC:
from .handlers import project_tasks # noqa: F401
60 changes: 0 additions & 60 deletions rdmo/projects/handlers.py

This file was deleted.

Empty file.
110 changes: 110 additions & 0 deletions rdmo/projects/handlers/generic_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from django.contrib.auth.models import User

from rdmo.projects.models import Membership, Project

from .utils import add_instance_to_projects, remove_instance_from_projects


def m2m_catalogs_changed_projects_sync_signal_handler(action, related_model, pk_set, instance, project_field):
"""
Update project relationships for m2m_changed signals.

Args:
action (str): The m2m_changed action (post_add, post_remove, post_clear).
related_model (Model): The related model (e.g., Catalog).
pk_set (set): The set of primary keys for the related model instances.
instance (Model): The instance being updated (e.g., View or Task).
project_field (str): The field on Project to update (e.g., 'views', 'tasks').
"""
if action == 'post_remove' and pk_set:
related_instances = related_model.objects.filter(pk__in=pk_set)
projects_to_change = Project.objects.filter_catalogs(catalogs=related_instances).filter(
**{project_field: instance}
)
remove_instance_from_projects(projects_to_change, project_field, instance)

elif action == 'post_clear':
projects_to_change = Project.objects.filter(**{project_field: instance})
remove_instance_from_projects(projects_to_change, project_field, instance)

elif action == 'post_add' and pk_set:
related_instances = related_model.objects.filter(pk__in=pk_set)
projects_to_change = Project.objects.filter_catalogs(catalogs=related_instances).exclude(
**{project_field: instance}
)
add_instance_to_projects(projects_to_change, project_field, instance)


def m2m_sites_changed_projects_sync_signal_handler(action, model, pk_set, instance, project_field):
"""
Synchronize Project relationships for m2m_changed signals triggered by site updates.

Args:
action (str): The m2m_changed action (post_add, post_remove, post_clear).
model (Model): The related model (e.g., Site).
pk_set (set): The set of primary keys for the related model instances.
instance (Model): The instance being updated (e.g., View or Task).
project_field (str): The field on Project to update (e.g., 'views', 'tasks').
"""
if action == 'post_remove' and pk_set:
related_sites = model.objects.filter(pk__in=pk_set)
catalogs = instance.catalogs.all()

projects_to_change = Project.objects.filter_catalogs(catalogs=catalogs).filter(
site__in=related_sites,
**{project_field: instance}
)
remove_instance_from_projects(projects_to_change, project_field, instance)

elif action == 'post_clear':
projects_to_change = Project.objects.filter_catalogs().filter(**{project_field: instance})
remove_instance_from_projects(projects_to_change, project_field, instance)

elif action == 'post_add' and pk_set:
related_sites = model.objects.filter(pk__in=pk_set)
catalogs = instance.catalogs.all()

projects_to_change = Project.objects.filter_catalogs(catalogs=catalogs).filter(
site__in=related_sites
).exclude(**{project_field: instance})
add_instance_to_projects(projects_to_change, project_field, instance)


def m2m_groups_changed_projects_sync_signal_handler(action, model, pk_set, instance, project_field):
"""
Synchronize Project relationships for m2m_changed signals triggered by group updates.

Args:
action (str): The m2m_changed action (post_add, post_remove, post_clear).
model (Model): The related model (e.g., Group).
pk_set (set): The set of primary keys for the related model instances.
instance (Model): The instance being updated (e.g., View or Task).
project_field (str): The field on Project to update (e.g., 'views', 'tasks').
"""
if action == 'post_remove' and pk_set:
related_groups = model.objects.filter(pk__in=pk_set)
users = User.objects.filter(groups__in=related_groups)
memberships = Membership.objects.filter(role='owner', user__in=users).values_list('id', flat=True)
catalogs = instance.catalogs.all()

projects_to_change = Project.objects.filter_catalogs(catalogs=catalogs).filter(
memberships__in=memberships,
**{project_field: instance}
)
remove_instance_from_projects(projects_to_change, project_field, instance)

elif action == 'post_clear':
# Remove all linked projects regardless of catalogs
projects_to_change = Project.objects.filter_catalogs().filter(**{project_field: instance})
remove_instance_from_projects(projects_to_change, project_field, instance)

elif action == 'post_add' and pk_set:
related_groups = model.objects.filter(pk__in=pk_set)
users = User.objects.filter(groups__in=related_groups)
memberships = Membership.objects.filter(role='owner', user__in=users).values_list('id', flat=True)
catalogs = instance.catalogs.all()

projects_to_change = Project.objects.filter_catalogs(catalogs=catalogs).filter(
memberships__in=memberships
).exclude(**{project_field: instance})
add_instance_to_projects(projects_to_change, project_field, instance)
53 changes: 53 additions & 0 deletions rdmo/projects/handlers/project_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import logging

from django.db.models.signals import m2m_changed
from django.dispatch import receiver

from rdmo.tasks.models import Task

from .generic_handlers import (
m2m_catalogs_changed_projects_sync_signal_handler,
m2m_groups_changed_projects_sync_signal_handler,
m2m_sites_changed_projects_sync_signal_handler,
)

logger = logging.getLogger(__name__)


@receiver(m2m_changed, sender=Task.catalogs.through)
def m2m_changed_task_catalog_signal(sender, instance, action, model, pk_set, **kwargs):
m2m_catalogs_changed_projects_sync_signal_handler(
action=action,
related_model=model,
pk_set=pk_set,
instance=instance,
project_field='tasks',
)


@receiver(m2m_changed, sender=Task.sites.through)
def m2m_changed_task_sites_signal(sender, instance, action, model, pk_set, **kwargs):
"""
Synchronize Project relationships when a Task's sites are updated.
"""
m2m_sites_changed_projects_sync_signal_handler(
action=action,
model=model,
pk_set=pk_set,
instance=instance,
project_field='tasks'
)


@receiver(m2m_changed, sender=Task.groups.through)
def m2m_changed_task_groups_signal(sender, instance, action, model, pk_set, **kwargs):
"""
Synchronize Project relationships when a Task's groups are updated.
"""
m2m_groups_changed_projects_sync_signal_handler(
action=action,
model=model,
pk_set=pk_set,
instance=instance,
project_field='tasks'
)
48 changes: 48 additions & 0 deletions rdmo/projects/handlers/project_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging

from django.db.models.signals import m2m_changed
from django.dispatch import receiver

from rdmo.views.models import View

from .generic_handlers import (
m2m_catalogs_changed_projects_sync_signal_handler,
m2m_groups_changed_projects_sync_signal_handler,
m2m_sites_changed_projects_sync_signal_handler,
)

logger = logging.getLogger(__name__)


@receiver(m2m_changed, sender=View.catalogs.through)
def m2m_changed_view_catalog_signal(sender, instance, action, model, pk_set, **kwargs):
m2m_catalogs_changed_projects_sync_signal_handler(
action=action,
related_model=model,
pk_set=pk_set,
instance=instance,
project_field='views',
)



@receiver(m2m_changed, sender=View.sites.through)
def m2m_changed_view_sites_signal(sender, instance, action, model, pk_set, **kwargs):
m2m_sites_changed_projects_sync_signal_handler(
action=action,
model=model,
pk_set=pk_set,
instance=instance,
project_field='views' # Field to update on Project
)


@receiver(m2m_changed, sender=View.groups.through)
def m2m_changed_view_groups_signal(sender, instance, action, model, pk_set, **kwargs):
m2m_groups_changed_projects_sync_signal_handler(
action=action,
model=model,
pk_set=pk_set,
instance=instance,
project_field='views' # Field to update on Project
)
10 changes: 10 additions & 0 deletions rdmo/projects/handlers/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@


def remove_instance_from_projects(projects, project_field, instance):
for project in projects:
getattr(project, project_field).remove(instance)


def add_instance_to_projects(projects, project_field, instance):
for project in projects:
getattr(project, project_field).add(instance)
14 changes: 14 additions & 0 deletions rdmo/projects/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ def filter_visibility(self, user):
visibility_filter = Q(visibility__isnull=False) & sites_filter & groups_filter
return self.filter(Q(user=user) | visibility_filter)

def filter_catalogs(self, catalogs=None, exclude_catalogs=None, exclude_null=True):
catalogs_filter = Q()
if exclude_null:
catalogs_filter &= Q(catalog__isnull=False)
if catalogs:
catalogs_filter &= Q(catalog__in=catalogs)
if exclude_catalogs:
catalogs_filter &= ~Q(catalog__in=exclude_catalogs)
return self.filter(catalogs_filter)


class MembershipQuerySet(models.QuerySet):

Expand Down Expand Up @@ -167,6 +177,10 @@ def filter_user(self, user):
def filter_visibility(self, user):
return self.get_queryset().filter_visibility(user)

def filter_catalogs(self, catalogs=None, exclude_catalogs=None, exclude_null=True):
return self.get_queryset().filter_catalogs(catalogs=catalogs, exclude_catalogs=exclude_catalogs,
exclude_null=exclude_null)


class MembershipManager(CurrentSiteManagerMixin, models.Manager):

Expand Down
5 changes: 5 additions & 0 deletions rdmo/projects/tests/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@


def assert_other_projects_unchanged(other_projects, initial_tasks_state):
for other_project in other_projects:
assert set(other_project.tasks.values_list('id', flat=True)) == set(initial_tasks_state[other_project.id])
Loading
Loading