From 9065876fd2e454db99dce5ac537c73cb2624a0fd Mon Sep 17 00:00:00 2001 From: David Wallace Date: Wed, 8 Jan 2025 10:52:48 +0100 Subject: [PATCH] feat(project,handlers): add sync for views and tasks, #966 #1198 Signed-off-by: David Wallace --- rdmo/core/settings.py | 3 +- rdmo/projects/apps.py | 6 +- rdmo/projects/handlers.py | 60 -------- rdmo/projects/handlers/__init__.py | 0 rdmo/projects/handlers/generic_handlers.py | 110 +++++++++++++++ rdmo/projects/handlers/project_tasks.py | 44 ++++++ rdmo/projects/handlers/project_views.py | 45 ++++++ rdmo/projects/handlers/utils.py | 10 ++ rdmo/projects/managers.py | 14 ++ rdmo/projects/tests/helpers.py | 5 + rdmo/projects/tests/test_handlers.py | 61 --------- rdmo/projects/tests/test_handlers_tasks.py | 151 +++++++++++++++++++++ rdmo/projects/tests/test_handlers_views.py | 116 ++++++++++++++++ testing/config/settings/base.py | 3 +- 14 files changed, 503 insertions(+), 125 deletions(-) delete mode 100644 rdmo/projects/handlers.py create mode 100644 rdmo/projects/handlers/__init__.py create mode 100644 rdmo/projects/handlers/generic_handlers.py create mode 100644 rdmo/projects/handlers/project_tasks.py create mode 100644 rdmo/projects/handlers/project_views.py create mode 100644 rdmo/projects/handlers/utils.py create mode 100644 rdmo/projects/tests/helpers.py 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.py b/rdmo/projects/handlers.py deleted file mode 100644 index 6f65709cbe..0000000000 --- a/rdmo/projects/handlers.py +++ /dev/null @@ -1,60 +0,0 @@ -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 - -logger = logging.getLogger(__name__) - - -@receiver(m2m_changed, sender=View.catalogs.through) -def m2m_changed_view_catalog_signal(sender, instance, **kwargs): - catalogs = instance.catalogs.all() - - if catalogs: - catalog_candidates = Catalog.objects.exclude(id__in=[catalog.id for catalog in catalogs]) - - # Remove catalog candidates for all sites - projects = Project.objects.filter(catalog__in=catalog_candidates, views=instance) - for proj in projects: - proj.views.remove(instance) - - -@receiver(m2m_changed, sender=View.sites.through) -def m2m_changed_view_sites_signal(sender, instance, **kwargs): - sites = instance.sites.all() - catalogs = instance.catalogs.all() - - if sites: - site_candidates = Site.objects.exclude(id__in=[site.id for site in sites]) - if not catalogs: - # if no catalogs are selected, update all - catalogs = Catalog.objects.all() - - # Restrict chosen catalogs for chosen sites - projects = Project.objects.filter(site__in=site_candidates, catalog__in=catalogs, views=instance) - for project in projects: - project.views.remove(instance) - - -@receiver(m2m_changed, sender=View.groups.through) -def m2m_changed_view_groups_signal(sender, instance, **kwargs): - groups = instance.groups.all() - catalogs = instance.catalogs.all() - - if groups: - users = User.objects.exclude(groups__in=groups) - memberships = [membership.id for membership in Membership.objects.filter(role='owner', user__in=users)] - if not catalogs: - # if no catalogs are selected, update all - catalogs = Catalog.objects.all() - - # Restrict chosen catalogs for chosen groups - projects = Project.objects.filter(memberships__in=list(memberships), catalog__in=catalogs, views=instance) - for project in projects: - project.views.remove(instance) 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/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 new file mode 100644 index 0000000000..04dc19c05f --- /dev/null +++ b/rdmo/projects/handlers/project_tasks.py @@ -0,0 +1,44 @@ + +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, +) + + +@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): + 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): + 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 new file mode 100644 index 0000000000..458109c735 --- /dev/null +++ b/rdmo/projects/handlers/project_views.py @@ -0,0 +1,45 @@ + +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, +) + + +@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' + ) + + +@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' + ) 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..db04dd17da --- /dev/null +++ b/rdmo/projects/tests/helpers.py @@ -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]) diff --git a/rdmo/projects/tests/test_handlers.py b/rdmo/projects/tests/test_handlers.py deleted file mode 100644 index 83a9c3c3c8..0000000000 --- a/rdmo/projects/tests/test_handlers.py +++ /dev/null @@ -1,61 +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_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.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..cf710fd09a --- /dev/null +++ b/rdmo/projects/tests/test_handlers_tasks.py @@ -0,0 +1,151 @@ + +from django.contrib.auth.models import Group + +from rdmo.projects.models import Project +from rdmo.tasks.models import Task + +from .helpers import assert_other_projects_unchanged + +project_id = 10 +task_id = 1 +group_name = 'view_test' + +def test_project_tasks_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 + project = Project.objects.get(id=project_id) + catalog = project.catalog + other_projects = Project.objects.exclude(catalog=catalog) # All other projects + task = Task.objects.get(id=task_id) # This task does not have catalogs in the fixture + task.catalogs.clear() + initial_project_tasks = project.tasks.values_list('id', flat=True) + + # Save initial state of tasks for other projects + initial_other_project_tasks = { + i.id: list(i.tasks.values_list('id', flat=True)) + for i in other_projects + } + + # Ensure the project does not have the task initially + assert task not in project.tasks.all() + + ## Tests for .add and .remove + # Add the catalog to the task and assert that the project now includes the task + task.catalogs.add(catalog) + assert task in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + # Remove the catalog from the task and assert that the project no longer includes the task + task.catalogs.remove(catalog) + assert task not in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + ## Tests for .set and .clear + # Add the catalog to the task and assert that the project now includes the task + task.catalogs.set([catalog]) + assert task in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + # Remove all catalogs from the task and assert that the project no longer includes the task + task.catalogs.clear() + assert task not in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + # Assert that the initial project tasks are unchanged + assert set(project.tasks.values_list('id', flat=True)) == set(initial_project_tasks) + + +def test_project_tasks_sync_when_adding_or_removing_a_site_to_or_from_a_task(db, settings): + assert settings.PROJECT_TASKS_SYNC + + # Setup: Get an existing project, its associated site, and create a task + project = Project.objects.get(id=project_id) + site = project.site + other_projects = Project.objects.exclude(site=site) # All other projects + task = Task.objects.get(id=task_id) # This task does not have sites in the fixture + task.sites.clear() # Ensure the task starts without any sites + project.tasks.remove(task) + initial_project_tasks = project.tasks.values_list('id', flat=True) + + # Save initial state of tasks for other projects + initial_other_project_tasks = { + i.id: list(i.tasks.values_list('id', flat=True)) + for i in other_projects + } + + # Ensure the project does not have the task initially + assert task not in project.tasks.all() + + ## Tests for .add and .remove + # Add the site to the task and assert that the project now includes the task + task.sites.add(site) + assert task in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + # Remove the site from the task and assert that the project no longer includes the task + task.sites.remove(site) + assert task not in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + ## Tests for .set and .clear + # Add the site to the task and assert that the project now includes the task + task.sites.set([site]) + assert task in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + # Clear all sites from the task and assert that the project no longer includes the task + task.sites.clear() + assert task not in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + # Assert that the initial project tasks are unchanged + assert set(project.tasks.values_list('id', flat=True)) == set(initial_project_tasks) + + +def test_project_tasks_sync_when_adding_or_removing_a_group_to_or_from_a_task(db, settings): + assert settings.PROJECT_TASKS_SYNC + + # Setup: Get an existing project, its associated group, and create a task + project = Project.objects.get(id=project_id) + user = project.owners.first() # Get the first user associated with the project + group = Group.objects.filter(name=group_name).first() # Get a test group + user.groups.add(group) + other_projects = Project.objects.exclude(memberships__user=user) # All other projects + task = Task.objects.get(id=task_id) # This task does not have groups in the fixture + task.groups.clear() # Ensure the task starts without any groups + initial_project_tasks = project.tasks.values_list('id', flat=True) + + # Save initial state of tasks for other projects + initial_other_project_tasks = { + i.id: list(i.tasks.values_list('id', flat=True)) + for i in other_projects + } + + # Ensure the project does not have the task initially + assert task not in project.tasks.all() + + ## Tests for .add and .remove + # Add the group to the task and assert that the project now includes the task + task.groups.add(group) + assert task in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + # Remove the group from the task and assert that the project no longer includes the task + task.groups.remove(group) + assert task not in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + ## Tests for .set and .clear + # Add the group to the task and assert that the project now includes the task + task.groups.set([group]) + assert task in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + # Clear all groups from the task and assert that the project no longer includes the task + task.groups.clear() + assert task not in project.tasks.all() + assert_other_projects_unchanged(other_projects, initial_other_project_tasks) + + # Assert that the initial project tasks are unchanged + assert set(project.tasks.values_list('id', flat=True)) == set(initial_project_tasks) diff --git a/rdmo/projects/tests/test_handlers_views.py b/rdmo/projects/tests/test_handlers_views.py new file mode 100644 index 0000000000..d88c6ca20c --- /dev/null +++ b/rdmo/projects/tests/test_handlers_views.py @@ -0,0 +1,116 @@ + +from django.contrib.auth.models import Group + +from rdmo.projects.models import Project +from rdmo.views.models import View + +project_id = 10 +view_id = 3 +group_name = 'view_test' + +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 + project = Project.objects.get(id=project_id) + catalog = project.catalog + view = View.objects.get(id=view_id) # this view does not have catalogs in fixture + view.catalogs.clear() + initial_project_views = project.views.values_list('id', flat=True) + + # # Initially, the project should not have the view + assert view not in project.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() + + ## 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 that the initial project views are unchanged + assert set(project.views.values_list('id', flat=True)) == set(initial_project_views) + + +def test_project_views_sync_when_adding_or_removing_a_site_to_or_from_a_view(db, settings): + assert settings.PROJECT_VIEWS_SYNC + + # Setup: Get an existing project and its associated site and create a view + project = Project.objects.get(id=project_id) + site = project.site + view = View.objects.get(id=view_id) # This view does not have sites in the fixture + view.sites.clear() # Ensure the view starts without any sites + initial_project_views = project.views.values_list('id', flat=True) + + # Ensure initial state: The project should not have the view + assert view not in project.views.all() + + ## Tests for .add and .remove + # Add the site to the view and assert that the project now includes the view + view.sites.add(site) + assert view in project.views.all() + + # Remove the site from the view and assert that the project should no longer include the view + view.sites.remove(site) + assert view not in project.views.all() + + ## Tests for .set and .clear + # Add the site to the view and assert that the project now includes the view + view.sites.set([site]) + assert view in project.views.all() + + # Clear all sites from the view and assert that the project should no longer include the view + view.sites.clear() + assert view not in project.views.all() + + # Assert that the initial project views are unchanged + assert set(project.views.values_list('id', flat=True)) == set(initial_project_views) + + +def test_project_views_sync_when_adding_or_removing_a_group_to_or_from_a_view(db, settings): + assert settings.PROJECT_VIEWS_SYNC + + # Setup: Get an existing project, its associated group, and create a view + project = Project.objects.get(id=project_id) + # breakpoint() + user = project.owners.first() # Get the first user associated with the project + group = Group.objects.filter(name=group_name).first() # Get the first group the user belongs to + user.groups.add(group) + view = View.objects.get(id=view_id) # This view does not have groups in the fixture + view.groups.clear() # Ensure the view starts without any groups + initial_project_views = project.views.values_list('id', flat=True) + + # Ensure initial state: The project should not have the view + assert view not in project.views.all() + + ## Tests for .add and .remove + # Add the group to the view and assert that the project now includes the view + view.groups.add(group) + assert view in project.views.all() + + # Remove the group from the view and assert that the project should no longer include the view + view.groups.remove(group) + assert view not in project.views.all() + + ## Tests for .set and .clear + # Add the group to the view and assert that the project now includes the view + view.groups.set([group]) + assert view in project.views.all() + + # Clear all groups from the view and assert that the project should no longer include the view + view.groups.clear() + assert view not in project.views.all() + + # Assert that the initial project views are unchanged + assert set(project.views.values_list('id', flat=True)) == set(initial_project_views) diff --git a/testing/config/settings/base.py b/testing/config/settings/base.py index 3517e314a4..4133ff8ec4 100644 --- a/testing/config/settings/base.py +++ b/testing/config/settings/base.py @@ -69,7 +69,8 @@ PROJECT_SEND_INVITE = True -PROJECT_REMOVE_VIEWS = True +PROJECT_VIEWS_SYNC = True +PROJECT_TASKS_SYNC = True PROJECT_SNAPSHOT_EXPORTS = [ ('xml', _('RDMO XML'), 'rdmo.projects.exports.RDMOXMLExport'),