From aa42691724fcf50317f1f8e31706e58ca7d34b13 Mon Sep 17 00:00:00 2001 From: David Wallace Date: Tue, 7 Jan 2025 17:59:03 +0100 Subject: [PATCH] feat(project, handlers): add generic signal receiver handler fuctions Signed-off-by: David Wallace --- rdmo/projects/handlers/generic_handlers.py | 110 +++++++++++++++++++++ rdmo/projects/handlers/project_tasks.py | 97 +++++++----------- rdmo/projects/handlers/project_views.py | 94 ++++++------------ rdmo/projects/handlers/utils.py | 10 ++ rdmo/projects/managers.py | 14 +++ rdmo/projects/tests/helpers.py | 0 6 files changed, 199 insertions(+), 126 deletions(-) create mode 100644 rdmo/projects/handlers/generic_handlers.py create mode 100644 rdmo/projects/handlers/utils.py create mode 100644 rdmo/projects/tests/helpers.py diff --git a/rdmo/projects/handlers/generic_handlers.py b/rdmo/projects/handlers/generic_handlers.py new file mode 100644 index 0000000000..1c061dcece --- /dev/null +++ b/rdmo/projects/handlers/generic_handlers.py @@ -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) diff --git a/rdmo/projects/handlers/project_tasks.py b/rdmo/projects/handlers/project_tasks.py index 098f7b0bb5..061c544287 100644 --- a/rdmo/projects/handlers/project_tasks.py +++ b/rdmo/projects/handlers/project_tasks.py @@ -1,78 +1,53 @@ import logging -from django.contrib.auth.models import User -from django.contrib.sites.models import Site from django.db.models.signals import m2m_changed from django.dispatch import receiver -from rdmo.projects.models import Membership, Project -from rdmo.questions.models import Catalog 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, **kwargs): - - task = instance - # catalogs that were changed - catalogs = model.objects.filter(pk__in=kwargs['pk_set']) - if action in ('post_remove', 'post_clear'): - # Remove the task from projects whose catalog is no longer linked to this task - projects_to_change = Project.objects.filter(catalog__in=catalogs, tasks=task) - for project in projects_to_change: - project.tasks.remove(task) - - elif action == 'post_add': - # Add the task to projects whose catalog is now linked to this task - projects_to_change = Project.objects.filter(catalog__in=task.catalogs.all()).exclude(tasks=task) - for project in projects_to_change: - project.tasks.add(task) +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, **kwargs): - - task = instance - sites = model.objects.filter(pk__in=kwargs['pk_set']) - catalogs = task.catalogs.all() or Catalog.objects.all() # If no catalogs, consider all - - if action in ('post_remove', 'post_clear'): - # Remove the task from projects whose site is no longer linked to this task - site_candidates = Site.objects.exclude(id__in=sites.values_list('id', flat=True)) - projects_to_change = Project.objects.filter(site__in=site_candidates, catalog__in=catalogs, tasks=task) - for project in projects_to_change: - project.tasks.remove(task) - - elif action == 'post_add': - # Add the task to projects whose site is now linked to this task - site_candidates = sites - projects_to_change = Project.objects.filter(site__in=site_candidates, catalog__in=catalogs).exclude(tasks=task) - for project in projects_to_change: - project.tasks.add(task) +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=None, **kwargs): - - task = instance - groups = task.groups.all() - catalogs = task.catalogs.all() or Catalog.objects.all() # If no catalogs, consider all - - if action in ('post_remove', 'post_clear'): - # Remove the task from projects whose group is no longer linked to this task - users = User.objects.exclude(groups__in=groups) - memberships = Membership.objects.filter(role='owner', user__in=users).values_list('id', flat=True) - projects_to_change = Project.objects.filter(memberships__in=memberships, catalog__in=catalogs, tasks=task) - for project in projects_to_change: - project.tasks.remove(task) - - elif action == 'post_add': - # Add the task to projects whose group is now linked to this task - users = User.objects.filter(groups__in=groups) - memberships = Membership.objects.filter(role='owner', user__in=users).values_list('id', flat=True) - projects_to_change = Project.objects.filter( - memberships__in=memberships, catalog__in=catalogs).exclude(tasks=task) - for project in projects_to_change: - project.tasks.add(task) +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' + ) diff --git a/rdmo/projects/handlers/project_views.py b/rdmo/projects/handlers/project_views.py index e6c7c154ec..a06a5bafa7 100644 --- a/rdmo/projects/handlers/project_views.py +++ b/rdmo/projects/handlers/project_views.py @@ -1,84 +1,48 @@ import logging -from django.contrib.auth.models import User -from django.contrib.sites.models import Site from django.db.models.signals import m2m_changed from django.dispatch import receiver -from rdmo.projects.models import Membership, Project -from rdmo.questions.models import Catalog 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): - view = instance - - if action == 'post_remove': - # catalogs that were changed - catalogs = model.objects.filter(pk__in=pk_set) - # Remove the view from projects whose catalog is no longer linked to this view - projects_to_change = Project.objects.filter(catalog__in=catalogs, views=view) - for project in projects_to_change: - project.views.remove(view) + m2m_catalogs_changed_projects_sync_signal_handler( + action=action, + related_model=model, + pk_set=pk_set, + instance=instance, + project_field='views', + ) - elif action == 'post_clear': - # Remove the view from all projects that were using this view - for project in Project.objects.filter(views=view): - project.views.remove(view) - - elif action == 'post_add': - # Add the view to projects whose catalog is now linked to this view - for project in Project.objects.filter(catalog__in=view.catalogs.all()): - project.views.add(view) @receiver(m2m_changed, sender=View.sites.through) -def m2m_changed_view_sites_signal(sender, instance, action, model, **kwargs): - # sites = instance.sites.all() - view = instance - sites = model.objects.filter(pk__in=kwargs['pk_set']) - catalogs = view.catalogs.all() or Catalog.objects.all() # If no catalogs, consider all - - if action in ('post_remove', 'post_clear'): - # Remove the view from projects whose site is no longer linked to this view - site_candidates = Site.objects.exclude(id__in=sites.values_list('id', flat=True)) - projects_to_change = Project.objects.filter(site__in=site_candidates, catalog__in=catalogs, views=view) - for project in projects_to_change: - project.views.remove(view) - - elif action == 'post_add': - # Add the view to projects whose site is now linked to this view - site_candidates = sites - projects_to_change = Project.objects.filter(site__in=site_candidates, catalog__in=catalogs).exclude(views=view) - for project in projects_to_change: - project.views.add(view) +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=None, **kwargs): - """ - Synchronize Projects when a View's groups are updated. - """ - view = instance - groups = view.groups.all() - catalogs = view.catalogs.all() or Catalog.objects.all() # If no catalogs, consider all - - if action in ('post_remove', 'post_clear'): - # Remove the view from projects whose group is no longer linked to this view - users = User.objects.exclude(groups__in=groups) - memberships = Membership.objects.filter(role='owner', user__in=users).values_list('id', flat=True) - projects_to_change = Project.objects.filter(memberships__in=memberships, catalog__in=catalogs, views=view) - for project in projects_to_change: - project.views.remove(view) - - elif action == 'post_add': - # Add the view to projects whose group is now linked to this view - users = User.objects.filter(groups__in=groups) - memberships = Membership.objects.filter(role='owner', user__in=users).values_list('id', flat=True) - projects_to_change = Project.objects.filter( - memberships__in=memberships, catalog__in=catalogs).exclude(views=view) - for project in projects_to_change: - project.views.add(view) +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 + ) diff --git a/rdmo/projects/handlers/utils.py b/rdmo/projects/handlers/utils.py new file mode 100644 index 0000000000..3175010938 --- /dev/null +++ b/rdmo/projects/handlers/utils.py @@ -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) diff --git a/rdmo/projects/managers.py b/rdmo/projects/managers.py index a6d49e4ff6..369b592f46 100644 --- a/rdmo/projects/managers.py +++ b/rdmo/projects/managers.py @@ -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): @@ -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): diff --git a/rdmo/projects/tests/helpers.py b/rdmo/projects/tests/helpers.py new file mode 100644 index 0000000000..e69de29bb2