From 798703b4d82cbc945aad9db63644aef1d58778ad Mon Sep 17 00:00:00 2001 From: David Wallace Date: Mon, 6 Jan 2025 15:41:47 +0100 Subject: [PATCH] feat(project, handlers): refactor project singal handlers for views and add for tasks Signed-off-by: David Wallace --- rdmo/core/settings.py | 3 +- rdmo/projects/apps.py | 6 +- rdmo/projects/handlers/__init__.py | 0 rdmo/projects/handlers/project_tasks.py | 78 ++++++++++++++++ .../project_views.py} | 17 ++-- rdmo/projects/tests/test_handlers.py | 91 ------------------- rdmo/projects/tests/test_handlers_tasks.py | 30 ++++++ rdmo/projects/tests/test_handlers_views.py | 41 +++++++++ 8 files changed, 165 insertions(+), 101 deletions(-) create mode 100644 rdmo/projects/handlers/__init__.py create mode 100644 rdmo/projects/handlers/project_tasks.py rename rdmo/projects/{handlers.py => handlers/project_views.py} (88%) delete mode 100644 rdmo/projects/tests/test_handlers.py create mode 100644 rdmo/projects/tests/test_handlers_tasks.py create mode 100644 rdmo/projects/tests/test_handlers_views.py diff --git a/rdmo/core/settings.py b/rdmo/core/settings.py index c6610be4de..53293abf63 100644 --- a/rdmo/core/settings.py +++ b/rdmo/core/settings.py @@ -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 = [] diff --git a/rdmo/projects/apps.py b/rdmo/projects/apps.py index 178bb4ccd3..d66f40735d 100644 --- a/rdmo/projects/apps.py +++ b/rdmo/projects/apps.py @@ -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 diff --git a/rdmo/projects/handlers/__init__.py b/rdmo/projects/handlers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rdmo/projects/handlers/project_tasks.py b/rdmo/projects/handlers/project_tasks.py new file mode 100644 index 0000000000..098f7b0bb5 --- /dev/null +++ b/rdmo/projects/handlers/project_tasks.py @@ -0,0 +1,78 @@ +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 + +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) + + +@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) + + +@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) diff --git a/rdmo/projects/handlers.py b/rdmo/projects/handlers/project_views.py similarity index 88% rename from rdmo/projects/handlers.py rename to rdmo/projects/handlers/project_views.py index 8b30b0992e..e6c7c154ec 100644 --- a/rdmo/projects/handlers.py +++ b/rdmo/projects/handlers/project_views.py @@ -13,22 +13,25 @@ @receiver(m2m_changed, sender=View.catalogs.through) -def m2m_changed_view_catalog_signal(sender, instance, action, model, **kwargs): - +def m2m_changed_view_catalog_signal(sender, instance, action, model, pk_set, **kwargs): view = instance - # catalogs that were changed - catalogs = model.objects.filter(pk__in=kwargs['pk_set']) - if action in ('post_remove', 'post_clear'): + 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) + 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 - projects_to_change = Project.objects.filter(catalog__in=view.catalogs.all()).exclude(views=view) - for project in projects_to_change: + for project in Project.objects.filter(catalog__in=view.catalogs.all()): project.views.add(view) diff --git a/rdmo/projects/tests/test_handlers.py b/rdmo/projects/tests/test_handlers.py deleted file mode 100644 index 1cb389d866..0000000000 --- a/rdmo/projects/tests/test_handlers.py +++ /dev/null @@ -1,91 +0,0 @@ -import itertools - -import pytest - -from django.contrib.auth.models import Group -from django.contrib.sites.models import Site - -from rdmo.projects.models import Project -from rdmo.questions.models import Catalog -from rdmo.views.models import View - -view_id = 3 -project_id = 10 -# project(id=10) starts with views([1,2,3]) and catalog(1) -# view(3) starts with sites([1]) -view_update_tests = [ - # tuples of: - # view_id, sites, catalogs, groups, project_id, project_exists - (3, [], [], [], 10, True), - (3, [2], [], [], 10, False), - (3, [1, 2, 3], [], [], 10, True), - (3, [], [2], [], 10, False), - (3, [2], [2], [], 10, False), - (3, [1, 2, 3], [2], [], 10, False), - (3, [], [1, 2], [], 10, True), - (3, [2], [1, 2], [], 10, False), - (3, [1, 2, 3], [1, 2], [], 10, True), - - (3, [], [], [1], 10, False), - (3, [2], [], [1], 10, False), - (3, [1, 2, 3], [], [1], 10, False), - (3, [], [2], [1], 10, False), - (3, [2], [2], [1], 10, False), - (3, [1, 2, 3], [2], [1], 10, False), - (3, [], [1, 2], [1], 10, False), - (3, [2], [1, 2], [1], 10, False), - (3, [1, 2, 3], [1, 2], [1], 10, False), - - (3, [], [], [1, 2, 3, 4], 10, False), - (3, [2], [], [1, 2, 3, 4], 10, False), - (3, [1, 2, 3], [], [1, 2, 3, 4], 10, False), - (3, [], [2], [1, 2, 3, 4], 10, False), - (3, [2], [2], [1, 2, 3, 4], 10, False), - (3, [1, 2, 3], [2], [1, 2, 3, 4], 10, False), - (3, [], [1, 2], [1, 2, 3, 4], 10, False), - (3, [2], [1, 2], [1, 2, 3, 4], 10, False), - (3, [1, 2, 3], [1, 2], [1, 2, 3, 4], 10, False) -] - - -@pytest.mark.django_db() -def test_project_views_sync_when_adding_or_removing_a_catalog_to_or_from_a_view(): - - # Setup: Create a catalog, a view, and a project using the catalog - catalog = Catalog.objects.first() - view = view = View.objects.get(pk=view_id) - project = Project.objects.create(title="Test Project", catalog=catalog) - - # Initially, the project should not have the view - assert view not in project.views.all() - - # Add the catalog to the view - view.catalogs.add(catalog) - - # After adding the catalog, the project should now include the view - assert view in project.views.all() - - # Remove the catalog from the view - view.catalogs.remove(catalog) - - # After removing the catalog, the project should no longer include the view - assert view not in project.views.all() - - -@pytest.mark.parametrize('view_id,sites,catalogs,groups,project_id,project_exists', view_update_tests) -def test_update_projects(db, view_id, sites, catalogs, groups, project_id, project_exists): - view = View.objects.get(pk=view_id) - - view.sites.set(Site.objects.filter(pk__in=sites)) - view.catalogs.set(Catalog.objects.filter(pk__in=catalogs)) - view.groups.set(Group.objects.filter(pk__in=groups)) - - assert sorted(itertools.chain.from_iterable(view.sites.all().values_list('pk'))) == sites - assert sorted(itertools.chain.from_iterable(view.catalogs.all().values_list('pk'))) == catalogs - assert sorted(itertools.chain.from_iterable(view.groups.all().values_list('pk'))) == groups - - if not project_exists: - with pytest.raises(Project.DoesNotExist): - Project.objects.filter(views=view).get(pk=project_id) - else: - assert Project.objects.filter(views=view).get(pk=project_id) diff --git a/rdmo/projects/tests/test_handlers_tasks.py b/rdmo/projects/tests/test_handlers_tasks.py new file mode 100644 index 0000000000..c381ff08cf --- /dev/null +++ b/rdmo/projects/tests/test_handlers_tasks.py @@ -0,0 +1,30 @@ + + +from rdmo.projects.models import Project +from rdmo.questions.models import Catalog +from rdmo.tasks.models import Task + +task_id = 1 + + +def test_project_views_sync_when_adding_or_removing_a_catalog_to_or_from_a_task(db, settings): + assert settings.PROJECT_TASKS_SYNC + + # Setup: Create a catalog, a task, and a project using the catalog + catalog = Catalog.objects.first() + task = Task.objects.get(pk=task_id) + task.catalogs.set([]) + project = Project.objects.create(title="Test Project", catalog=catalog) + + # Initially, the project should not have the task + assert task not in project.tasks.all() + + # Add the catalog to the task + task.catalogs.add(catalog) + # After adding the catalog, the project should now include the task + assert task in project.tasks.all() + + # Remove the catalog from the task + task.catalogs.remove(catalog) + # After removing the catalog, the project should no longer include the task + assert task not in project.tasks.all() diff --git a/rdmo/projects/tests/test_handlers_views.py b/rdmo/projects/tests/test_handlers_views.py new file mode 100644 index 0000000000..6b872040f4 --- /dev/null +++ b/rdmo/projects/tests/test_handlers_views.py @@ -0,0 +1,41 @@ + + + +from rdmo.projects.models import Project +from rdmo.questions.models import Catalog +from rdmo.views.models import View + + +def test_project_views_sync_when_adding_or_removing_a_catalog_to_or_from_a_view(db, settings): + assert settings.PROJECT_VIEWS_SYNC + + # Setup: Create a catalog, a view, and a project using the catalog + catalog = Catalog.objects.first() + view = View.objects.get(pk=3) + # view.catalogs.clear() + project = Project.objects.create(title="Test Project", catalog=catalog) + pr10 = Project.objects.get(pk=10) + + # # Initially, the project should not have the view + # assert view not in project.views.all() + # assert view not in pr10.views.all() + + ## Tests for .add and .remove + # Add the catalog to the view and assert that the project now includes the view + view.catalogs.add(catalog) + assert view in project.views.all() + + # Remove the catalog from the view and assert that the project should no longer include the view + view.catalogs.remove(catalog) + assert view not in project.views.all() + assert view not in pr10.views.all() + + ## Tests for .set and .clear + # Add the catalog to the view and assert that the project now includes the view + view.catalogs.set([catalog]) + assert view in project.views.all() + + # Remove the catalog from the view and assert that the project should no longer include the view + view.catalogs.clear() + assert view not in project.views.all() + assert view not in pr10.views.all()