diff --git a/rdmo/core/settings.py b/rdmo/core/settings.py index 7f803d29a7..200467f4a8 100644 --- a/rdmo/core/settings.py +++ b/rdmo/core/settings.py @@ -208,7 +208,9 @@ 'PROJECT_IMPORTS', 'PROJECT_IMPORTS_LIST', 'PROJECT_SEND_ISSUE', - 'NESTED_PROJECTS' + 'NESTED_PROJECTS', + 'PROJECT_VIEWS_SYNC', + 'PROJECT_TASKS_SYNC' ] SETTINGS_API = [ @@ -331,7 +333,8 @@ PROJECT_SEND_INVITE = True -PROJECT_REMOVE_VIEWS = True +PROJECT_VIEWS_SYNC = False +PROJECT_TASKS_SYNC = False PROJECT_CREATE_RESTRICTED = False PROJECT_CREATE_GROUPS = [] diff --git a/rdmo/projects/apps.py b/rdmo/projects/apps.py index 178bb4ccd3..07dfc6a64d 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 m2m_changed_views, project_save_views # noqa: F401 + if settings.PROJECT_TASKS_SYNC: + from .handlers import m2m_changed_tasks, project_save_tasks # noqa: F401 diff --git a/rdmo/projects/forms.py b/rdmo/projects/forms.py index 3d2d1a220a..9616c38aca 100644 --- a/rdmo/projects/forms.py +++ b/rdmo/projects/forms.py @@ -160,12 +160,21 @@ class Meta: 'catalog': forms.RadioSelect() } + def save(self, *args, **kwargs): + # if the catalog is the same, do nothing + if self.instance.catalog.id == self.cleaned_data.get('catalog'): + return self.instance + return super().save(*args, **kwargs) + class ProjectUpdateTasksForm(forms.ModelForm): use_required_attribute = False def __init__(self, *args, **kwargs): + if settings.PROJECT_TASKS_SYNC: + raise ValidationError(_("Editing tasks is disabled.")) + tasks = kwargs.pop('tasks') super().__init__(*args, **kwargs) self.fields['tasks'].queryset = tasks @@ -180,12 +189,20 @@ class Meta: 'tasks': forms.CheckboxSelectMultiple() } + def save(self, *args, **kwargs): + if settings.PROJECT_TASKS_SYNC: + raise ValidationError(_("Editing tasks is disabled.")) + super().save(*args, **kwargs) + class ProjectUpdateViewsForm(forms.ModelForm): use_required_attribute = False def __init__(self, *args, **kwargs): + if settings.PROJECT_VIEWS_SYNC: + raise ValidationError(_("Editing views is disabled.")) + views = kwargs.pop('views') super().__init__(*args, **kwargs) self.fields['views'].queryset = views @@ -200,6 +217,11 @@ class Meta: 'views': forms.CheckboxSelectMultiple() } + def save(self, *args, **kwargs): + if settings.PROJECT_VIEWS_SYNC: + raise ValidationError(_("Editing views is disabled.")) + super().save(*args, **kwargs) + class ProjectUpdateParentForm(forms.ModelForm): 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..b6a03736ad --- /dev/null +++ b/rdmo/projects/handlers/generic_handlers.py @@ -0,0 +1,110 @@ +from django.contrib.auth.models import Group, User +from django.contrib.sites.models import Site + +from rdmo.projects.models import Membership, Project +from rdmo.questions.models import Catalog + + +def m2m_catalogs_changed_projects_sync_signal_handler(instance, action, pk_set, project_field): + + if action == 'post_remove' and pk_set: + projects_to_change = ( + Project.objects + .filter_catalogs(catalogs=Catalog.objects.filter(pk__in=pk_set)) + .filter(**{project_field: instance}) + ) + for project in projects_to_change: # remove instance from project + getattr(project, project_field).remove(instance) + + elif action == 'post_clear': + projects_to_change = Project.objects.filter(**{project_field: instance}) + for project in projects_to_change: # remove instance from project + getattr(project, project_field).remove(instance) + + elif action == 'post_add' and pk_set: + projects_to_change = ( + Project.objects + .filter_catalogs(catalogs=Catalog.objects.filter(pk__in=pk_set)) + .exclude(**{project_field: instance}) + ) + for project in projects_to_change: # add instance to project + getattr(project, project_field).add(instance) + + +def m2m_sites_changed_projects_sync_signal_handler(instance, action, pk_set, project_field): + + if action == 'post_remove' and pk_set: + projects_to_change = ( + Project.objects + .filter_catalogs(catalogs=instance.catalogs.all()) + .filter(site__in=Site.objects.filter(pk__in=pk_set)) + .filter(**{project_field: instance}) + ) + for project in projects_to_change: # remove instance from project + getattr(project, project_field).remove(instance) + + elif action == 'post_clear': + projects_to_change = ( + Project.objects + .filter_catalogs() + .filter(**{project_field: instance}) + ) + for project in projects_to_change: # remove instance from project + getattr(project, project_field).remove(instance) + + elif action == 'post_add' and pk_set: + projects_to_change = ( + Project.objects + .filter_catalogs(catalogs=instance.catalogs.all()) + .filter(site__in=Site.objects.filter(pk__in=pk_set)) + .exclude(**{project_field: instance}) + ) + for project in projects_to_change: # add instance to project + getattr(project, project_field).add(instance) + + +def m2m_groups_changed_projects_sync_signal_handler(instance, action, pk_set, project_field): + + if action == 'post_remove' and pk_set: + related_groups = Group.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) + ) + projects_to_change = ( + Project.objects + .filter_catalogs(catalogs=instance.catalogs.all()) + .filter(memberships__in=memberships) + .filter(**{project_field: instance}) + ) + for project in projects_to_change: # remove instance from project + getattr(project, project_field).remove(instance) + + elif action == 'post_clear': + # Remove all linked projects regardless of catalogs + projects_to_change = ( + Project.objects + .filter_catalogs() + .filter(**{project_field: instance}) + ) + for project in projects_to_change: # remove instance from project + getattr(project, project_field).remove(instance) + + elif action == 'post_add' and pk_set: + related_groups = Group.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) + ) + projects_to_change = ( + Project.objects + .filter_catalogs(catalogs=instance.catalogs.all()) + .filter(memberships__in=memberships) + .exclude(**{project_field: instance}) + ) + for project in projects_to_change: # add instance to project + getattr(project, project_field).add(instance) diff --git a/rdmo/projects/handlers/m2m_changed_tasks.py b/rdmo/projects/handlers/m2m_changed_tasks.py new file mode 100644 index 0000000000..df097b79f3 --- /dev/null +++ b/rdmo/projects/handlers/m2m_changed_tasks.py @@ -0,0 +1,25 @@ +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, pk_set, **kwargs): + m2m_catalogs_changed_projects_sync_signal_handler(instance, action, pk_set, 'tasks') + + +@receiver(m2m_changed, sender=Task.sites.through) +def m2m_changed_task_sites_signal(sender, instance, action, pk_set, **kwargs): + m2m_sites_changed_projects_sync_signal_handler(instance, action, pk_set, 'tasks') + + +@receiver(m2m_changed, sender=Task.groups.through) +def m2m_changed_task_groups_signal(sender, instance, action, pk_set, **kwargs): + m2m_groups_changed_projects_sync_signal_handler(instance, action, pk_set, 'tasks') diff --git a/rdmo/projects/handlers/m2m_changed_views.py b/rdmo/projects/handlers/m2m_changed_views.py new file mode 100644 index 0000000000..dd7a91cd78 --- /dev/null +++ b/rdmo/projects/handlers/m2m_changed_views.py @@ -0,0 +1,25 @@ +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, pk_set, **kwargs): + m2m_catalogs_changed_projects_sync_signal_handler(instance, action, pk_set, 'views') + + +@receiver(m2m_changed, sender=View.sites.through) +def m2m_changed_view_sites_signal(sender, instance, action, pk_set, **kwargs): + m2m_sites_changed_projects_sync_signal_handler(instance, action, pk_set, 'views') + + +@receiver(m2m_changed, sender=View.groups.through) +def m2m_changed_view_groups_signal(sender, instance, action, pk_set, **kwargs): + m2m_groups_changed_projects_sync_signal_handler(instance, action, pk_set, 'views') diff --git a/rdmo/projects/handlers/project_save_tasks.py b/rdmo/projects/handlers/project_save_tasks.py new file mode 100644 index 0000000000..9da6600013 --- /dev/null +++ b/rdmo/projects/handlers/project_save_tasks.py @@ -0,0 +1,36 @@ +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver + +from rdmo.projects.models import Project +from rdmo.tasks.models import Task + + +@receiver(pre_save, sender=Project) +def pre_save_project_sync_tasks_from_catalog(sender, instance, raw, update_fields, **kwargs): + instance._catalog_has_changed_sync_tasks = False + + if raw or (update_fields and 'catalog' not in update_fields): + return + + if instance.id is not None: + # Fetch the original catalog from the database + if sender.objects.get(id=instance.id).catalog == instance.catalog: + # Do nothing if the catalog has not changed + return + + # Defer synchronization of views + instance._catalog_has_changed_sync_tasks = True + + +@receiver(post_save, sender=Project) +def post_save_project_sync_tasks_from_catalog(sender, instance, created, raw, update_fields, **kwargs): + if raw or (update_fields and 'catalog' not in update_fields): + return + + if instance._catalog_has_changed_sync_tasks or (created and not instance.tasks.exists) : + instance.tasks.set( + Task.objects + .filter_for_project(instance) + .filter(available=True) + .values_list('id', flat=True) + ) diff --git a/rdmo/projects/handlers/project_save_views.py b/rdmo/projects/handlers/project_save_views.py new file mode 100644 index 0000000000..a8f9284098 --- /dev/null +++ b/rdmo/projects/handlers/project_save_views.py @@ -0,0 +1,36 @@ +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver + +from rdmo.projects.models import Project +from rdmo.views.models import View + + +@receiver(pre_save, sender=Project) +def pre_save_project_sync_views_from_catalog(sender, instance, raw, update_fields, **kwargs): + instance._catalog_has_changed_sync_views = False + + if raw or (update_fields and 'catalog' not in update_fields): + return + + if instance.id is not None: + # Fetch the original catalog from the database + if sender.objects.get(id=instance.id).catalog == instance.catalog: + # Do nothing if the catalog has not changed + return + + # Defer synchronization of views + instance._catalog_has_changed_sync_views = True + + +@receiver(post_save, sender=Project) +def post_save_project_sync_views_from_catalog(sender, instance, created, raw, update_fields, **kwargs): + if raw or (update_fields and 'catalog' not in update_fields): + return + + if instance._catalog_has_changed_sync_views or (created and not instance.views.exists): + instance.views.set( + View.objects + .filter_for_project(instance) + .filter(available=True) + .values_list('id', flat=True) + ) diff --git a/rdmo/projects/managers.py b/rdmo/projects/managers.py index 16b4f223e7..5b86fe99b6 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): @@ -193,6 +203,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/models/project.py b/rdmo/projects/models/project.py index b992c0dc9d..22559b7be3 100644 --- a/rdmo/projects/models/project.py +++ b/rdmo/projects/models/project.py @@ -123,6 +123,14 @@ def authors(self): def guests(self): return self.get_members('guest') + @cached_property + def groups_str(self): + return ', '.join(str(i) for i in self.groups if i is not None) + + @property + def groups(self): + return {group for user in self.get_members('owner') for group in user.groups.all()} + @property def file_size(self): queryset = self.values.filter(snapshot=None).exclude(models.Q(file='') | models.Q(file=None)) diff --git a/rdmo/projects/serializers/v1/__init__.py b/rdmo/projects/serializers/v1/__init__.py index 0ca08bddbc..459c23bf38 100644 --- a/rdmo/projects/serializers/v1/__init__.py +++ b/rdmo/projects/serializers/v1/__init__.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from rest_framework.exceptions import ValidationError from rdmo.domain.models import Attribute from rdmo.questions.models import Catalog @@ -97,6 +98,12 @@ class Meta: ProjectParentValidator() ] + def validate_views(self, value): + """Block updates to views if syncing is enabled.""" + if settings.PROJECT_VIEWS_SYNC and value: + raise ValidationError(_('Editing views is disabled.')) + return value + class ProjectCopySerializer(ProjectSerializer): diff --git a/rdmo/projects/templates/projects/project_detail_issues.html b/rdmo/projects/templates/projects/project_detail_issues.html index 0e51a4257f..9bf77532e2 100644 --- a/rdmo/projects/templates/projects/project_detail_issues.html +++ b/rdmo/projects/templates/projects/project_detail_issues.html @@ -21,7 +21,7 @@

{% trans 'Tasks' %}

{% trans 'Time frame' %} {% trans 'Status' %} - {% if can_change_project %} + {% if can_change_project and not settings.PROJECT_TASKS_SYNC %} @@ -67,7 +67,7 @@

{% trans 'Tasks' %}

{% else %} - {% if can_change_project %} + {% if can_change_project and not settings.PROJECT_TASKS_SYNC %}

diff --git a/rdmo/projects/templates/projects/project_detail_sidebar.html b/rdmo/projects/templates/projects/project_detail_sidebar.html index 542f807730..fc51cee15a 100644 --- a/rdmo/projects/templates/projects/project_detail_sidebar.html +++ b/rdmo/projects/templates/projects/project_detail_sidebar.html @@ -51,12 +51,12 @@

{% trans 'Options' %}

{% trans 'Update parent project' %} {% endif %} - {% if settings.PROJECT_ISSUES and tasks_available %} + {% if settings.PROJECT_ISSUES and tasks_available and not settings.PROJECT_TASKS_SYNC %}
  • {% trans 'Update project tasks' %}
  • {% endif %} - {% if settings.PROJECT_VIEWS and views_available %} + {% if settings.PROJECT_VIEWS and views_available and not settings.PROJECT_VIEWS_SYNC %}
  • {% trans 'Update project views' %}
  • diff --git a/rdmo/projects/templates/projects/project_detail_views.html b/rdmo/projects/templates/projects/project_detail_views.html index ae3ce20fa7..09f9915cf5 100644 --- a/rdmo/projects/templates/projects/project_detail_views.html +++ b/rdmo/projects/templates/projects/project_detail_views.html @@ -19,7 +19,7 @@

    {% trans 'Views' %}

    {% trans 'View' %} {% trans 'Description' %} - {% if can_change_project %} + {% if can_change_project and not settings.PROJECT_VIEWS_SYNC %} @@ -45,7 +45,7 @@

    {% trans 'Views' %}

    {% else %} - {% if can_change_project %} + {% if can_change_project and not settings.PROJECT_VIEWS_SYNC %}

    diff --git a/rdmo/projects/tests/helpers.py b/rdmo/projects/tests/helpers.py new file mode 100644 index 0000000000..22c271ca53 --- /dev/null +++ b/rdmo/projects/tests/helpers.py @@ -0,0 +1,50 @@ +from collections import defaultdict + +import pytest + +from django.apps import apps + +from rdmo.questions.models import Catalog +from rdmo.views.models import View + + +@pytest.fixture +def enable_project_views_sync(settings): # noqa:PT004 + settings.PROJECT_VIEWS_SYNC = True + apps.get_app_config('projects').ready() + +@pytest.fixture +def enable_project_tasks_sync(settings): # noqa:PT004 + settings.PROJECT_TASKS_SYNC = True + apps.get_app_config('projects').ready() + +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]) + + + +def get_catalog_view_mapping(): + """ + Generate a mapping of catalogs to their associated views. + Includes all catalogs, even those with no views, and adds `sites` and `groups` for each view. + """ + # Initialize an empty dictionary for the catalog-to-views mapping + catalog_views_mapping = defaultdict(list) + + # Populate the mapping for all catalogs + for catalog in Catalog.objects.all(): + catalog_views_mapping[catalog.id] = [] + + # Iterate through all views and enrich the mapping + for view in View.objects.prefetch_related('sites', 'groups'): + if view.catalogs.exists(): # Only include views with valid catalogs + for catalog in view.catalogs.all(): + catalog_views_mapping[catalog.id].append({ + 'id': view.id, + 'sites': list(view.sites.values_list('id', flat=True)), + 'groups': list(view.groups.values_list('id', flat=True)) + }) + + # Convert defaultdict to a regular dictionary + return dict(catalog_views_mapping) 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_m2m_tasks.py b/rdmo/projects/tests/test_handlers_m2m_tasks.py new file mode 100644 index 0000000000..11df71cae0 --- /dev/null +++ b/rdmo/projects/tests/test_handlers_m2m_tasks.py @@ -0,0 +1,160 @@ + +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, + enable_project_tasks_sync, # noqa: F401 +) + +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, enable_project_tasks_sync # noqa:F811 + ): + 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, enable_project_tasks_sync # noqa:F811 + ): + 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, enable_project_tasks_sync # noqa:F811 + ): + 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_m2m_views.py b/rdmo/projects/tests/test_handlers_m2m_views.py new file mode 100644 index 0000000000..effeec830b --- /dev/null +++ b/rdmo/projects/tests/test_handlers_m2m_views.py @@ -0,0 +1,122 @@ +from django.contrib.auth.models import Group + +from rdmo.projects.models import Project +from rdmo.views.models import View + +from .helpers import enable_project_views_sync # noqa: F401 + +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, enable_project_views_sync # noqa:F811 + ): + 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, enable_project_views_sync # noqa:F811 + ): + 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, enable_project_views_sync # noqa:F811 + ): + 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/rdmo/projects/tests/test_handlers_project_save.py b/rdmo/projects/tests/test_handlers_project_save.py new file mode 100644 index 0000000000..cb25cbd911 --- /dev/null +++ b/rdmo/projects/tests/test_handlers_project_save.py @@ -0,0 +1,38 @@ + +from rdmo.projects.models import Project +from rdmo.questions.models import Catalog +from rdmo.views.models import View + +from .helpers import ( + enable_project_views_sync, # noqa: F401 + get_catalog_view_mapping, +) + +project_id = 10 + + +def test_project_views_sync_when_changing_the_catalog_on_a_project( + db, settings, enable_project_views_sync # noqa:F811 +): + assert settings.PROJECT_VIEWS_SYNC + + # Setup: Create a catalog, a view, and a project using the catalog + project = Project.objects.get(id=project_id) + initial_project_views = set(project.views.values_list('id', flat=True)) + assert initial_project_views == {1,2,3} # from the fixture + + catalog_view_mapping = get_catalog_view_mapping() + for catalog_id, view_ids in catalog_view_mapping.items(): + if project.catalog_id == catalog_id: + continue # catalog will not change + project.catalog = Catalog.objects.get(id=catalog_id) + project.save() + + # TODO this filter_available_views_for_project method needs to tested explicitly + available_views = set( + View.objects + .filter_for_project(project) + .filter_availability(project.owners.first()) + .values_list('id', flat=True) + ) + assert set(project.views.values_list('id', flat=True)) == available_views diff --git a/rdmo/projects/tests/test_view_project.py b/rdmo/projects/tests/test_view_project.py index 5dad90e1fd..b224a1509a 100644 --- a/rdmo/projects/tests/test_view_project.py +++ b/rdmo/projects/tests/test_view_project.py @@ -10,6 +10,7 @@ from ..forms import CatalogChoiceField from ..models import Project +from .helpers import enable_project_tasks_sync, enable_project_views_sync # noqa: F401 users = ( ('owner', 'owner'), @@ -451,7 +452,7 @@ def test_project_update_catalog_post(db, client, username, password, project_id) @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) -def test_project_update_tasks_get(db, client, username, password, project_id): +def test_project_update_tasks_get(db, client, settings, username, password, project_id): client.login(username=username, password=password) url = reverse('project_update_tasks', args=[project_id]) @@ -466,9 +467,21 @@ def test_project_update_tasks_get(db, client, username, password, project_id): assert response.status_code == 302 +def test_project_update_tasks_get_not_allowed(db, client, settings, enable_project_tasks_sync): # noqa:F811 + assert settings.PROJECT_TASKS_SYNC + client.login(username='owner', password='owner') + + url = reverse('project_update_tasks', args=[project_id]) + data = { + 'tasks': [1] + } + response = client.get(url, data) + + assert response.status_code == 404 + @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) -def test_project_update_tasks_post(db, client, username, password, project_id): +def test_project_update_tasks_post(db, client, settings, username, password, project_id): client.login(username=username, password=password) project = Project.objects.get(pk=project_id) @@ -490,9 +503,22 @@ def test_project_update_tasks_post(db, client, username, password, project_id): assert list(Project.objects.get(pk=project_id).tasks.values('id')) == list(project.tasks.values('id')) +def test_project_update_tasks_post_not_allowed(db, client, settings, enable_project_tasks_sync): # noqa:F811 + assert settings.PROJECT_TASKS_SYNC + client.login(username='owner', password='owner') + + url = reverse('project_update_tasks', args=[project_id]) + data = { + 'tasks': [1] + } + response = client.post(url, data) + + assert response.status_code == 404 + + @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) -def test_project_update_views_get(db, client, username, password, project_id): +def test_project_update_views_get(db, client, settings, username, password, project_id): client.login(username=username, password=password) url = reverse('project_update_views', args=[project_id]) @@ -507,9 +533,22 @@ def test_project_update_views_get(db, client, username, password, project_id): assert response.status_code == 302 +def test_project_update_views_get_not_allowed(db, client, settings, enable_project_views_sync): # noqa:F811 + assert settings.PROJECT_VIEWS_SYNC + client.login(username='owner', password='owner') + + url = reverse('project_update_views', args=[project_id]) + data = { + 'tasks': [1] + } + response = client.get(url, data) + + assert response.status_code == 404 + + @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) -def test_project_update_views_post(db, client, username, password, project_id): +def test_project_update_views_post(db, client, settings, username, password, project_id): client.login(username=username, password=password) project = Project.objects.get(pk=project_id) @@ -531,6 +570,18 @@ def test_project_update_views_post(db, client, username, password, project_id): assert list(Project.objects.get(pk=project_id).views.values('id')) == list(project.views.values('id')) +def test_project_update_views_post_not_allowed(db, client, settings, enable_project_views_sync): # noqa:F811 + assert settings.PROJECT_VIEWS_SYNC + client.login(username='owner', password='owner') + + url = reverse('project_update_views', args=[project_id]) + data = { + 'tasks': [1] + } + response = client.post(url, data) + + assert response.status_code == 404 + @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) def test_project_update_parent_get(db, client, username, password, project_id): diff --git a/rdmo/projects/tests/test_viewset_project.py b/rdmo/projects/tests/test_viewset_project.py index 76048202a3..ce1d6232a6 100644 --- a/rdmo/projects/tests/test_viewset_project.py +++ b/rdmo/projects/tests/test_viewset_project.py @@ -4,6 +4,7 @@ from django.urls import reverse from ..models import Membership, Project, Snapshot, Value +from .helpers import enable_project_views_sync # noqa: F401 users = ( ('owner', 'owner'), @@ -429,6 +430,20 @@ def test_update_parent(db, client, username, password, project_id): assert Project.objects.get(pk=project_id).parent == project.parent +def test_update_project_views_not_allowed(db, client, settings, enable_project_views_sync): # noqa:F811 + assert settings.PROJECT_VIEWS_SYNC + + client.login(username='owner', password='owner') + url = reverse(urlnames['detail'], args=[project_id]) + data = { + 'views': [1] + } + response = client.put(url, data, content_type='application/json') + + assert response.status_code == 400 + assert 'Editing views is disabled' in ' '.join(response.json()['views']) + + @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) def test_delete(db, client, username, password, project_id): diff --git a/rdmo/projects/views/project.py b/rdmo/projects/views/project.py index dffaddb8c3..4b48e5ecac 100644 --- a/rdmo/projects/views/project.py +++ b/rdmo/projects/views/project.py @@ -63,14 +63,29 @@ def get_context_data(self, **kwargs): context['catalogs'] = Catalog.objects.filter_current_site() \ .filter_group(self.request.user) \ .filter_availability(self.request.user) - context['tasks_available'] = Task.objects.filter_current_site() \ - .filter_catalog(self.object.catalog) \ - .filter_group(self.request.user) \ - .filter_availability(self.request.user).exists() - context['views_available'] = View.objects.filter_current_site() \ - .filter_catalog(self.object.catalog) \ - .filter_group(self.request.user) \ - .filter_availability(self.request.user).exists() + + if settings.PROJECT_TASKS_SYNC: + # tasks should be synced, the user can not change them + context['tasks_available'] = project.tasks.exists() + else: + context['tasks_available'] = ( + Task.objects + .filter_for_project(project) + .filter_availability(self.request.user) + .exists() + ) + + if settings.PROJECT_VIEWS_SYNC: + # views should be synced, the user can not change them + context['views_available'] = project.views.exists() + else: + context['views_available'] = ( + View.objects + .filter_for_project(project) + .filter_availability(self.request.user) + .exists() + ) + ancestors_import = [] for instance in ancestors.exclude(id=project.id): if self.request.user.has_perm('projects.view_project_object', instance): diff --git a/rdmo/projects/views/project_create.py b/rdmo/projects/views/project_create.py index c909e9a719..fb74dcdd16 100644 --- a/rdmo/projects/views/project_create.py +++ b/rdmo/projects/views/project_create.py @@ -1,5 +1,6 @@ import logging +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.sites.shortcuts import get_current_site from django.urls import reverse_lazy @@ -44,26 +45,22 @@ def form_valid(self, form): # save the project response = super().form_valid(form) - # add all tasks to project - tasks = Task.objects.filter_current_site() \ - .filter_catalog(self.object.catalog) \ - .filter_group(self.request.user) \ - .filter_availability(self.request.user) - for task in tasks: - form.instance.tasks.add(task) - - # add all views to project - views = View.objects.filter_current_site() \ - .filter_catalog(self.object.catalog) \ - .filter_group(self.request.user) \ - .filter_availability(self.request.user) - for view in views: - form.instance.views.add(view) - # add current user as owner membership = Membership(project=form.instance, user=self.request.user, role='owner') membership.save() + # add all tasks to project + if not settings.PROJECT_TASKS_SYNC: + tasks = Task.objects.filter_for_project(form.instance).filter_availability(self.request.user) + for task in tasks: + form.instance.tasks.add(task) + + # add all views to project + if not settings.PROJECT_VIEWS_SYNC: + views = View.objects.filter_for_project(form.instance).filter_availability(self.request.user) + for view in views: + form.instance.views.add(view) + return response diff --git a/rdmo/projects/views/project_update.py b/rdmo/projects/views/project_update.py index b1889f7d8e..dfedc7aa6e 100644 --- a/rdmo/projects/views/project_update.py +++ b/rdmo/projects/views/project_update.py @@ -1,5 +1,7 @@ import logging +from django.conf import settings +from django.http import Http404 from django.views.generic import UpdateView from rdmo.core.views import ObjectPermissionMixin, RedirectViewMixin @@ -81,18 +83,22 @@ class ProjectUpdateTasksView(ObjectPermissionMixin, RedirectViewMixin, UpdateVie form_class = ProjectUpdateTasksForm permission_required = 'projects.change_project_object' - def get_form_kwargs(self): - tasks = Task.objects.filter_current_site() \ - .filter_catalog(self.object.catalog) \ - .filter_group(self.request.user) \ - .filter_availability(self.request.user) + def dispatch(self, request, *args, **kwargs): + if settings.PROJECT_TASKS_SYNC: + raise Http404 + return super().dispatch(request, *args, **kwargs) + def get_form_kwargs(self): + tasks = Task.objects.filter_for_project(self.object).filter_availability(self.request.user) form_kwargs = super().get_form_kwargs() form_kwargs.update({ 'tasks': tasks }) return form_kwargs + def get_success_url(self): + return self.get_object().get_absolute_url() + class ProjectUpdateViewsView(ObjectPermissionMixin, RedirectViewMixin, UpdateView): model = Project @@ -100,18 +106,22 @@ class ProjectUpdateViewsView(ObjectPermissionMixin, RedirectViewMixin, UpdateVie form_class = ProjectUpdateViewsForm permission_required = 'projects.change_project_object' - def get_form_kwargs(self): - views = View.objects.filter_current_site() \ - .filter_catalog(self.object.catalog) \ - .filter_group(self.request.user) \ - .filter_availability(self.request.user) + def dispatch(self, request, *args, **kwargs): + if settings.PROJECT_VIEWS_SYNC: + raise Http404 + return super().dispatch(request, *args, **kwargs) + def get_form_kwargs(self): + views = View.objects.filter_for_project(self.object).filter_availability(self.request.user) form_kwargs = super().get_form_kwargs() form_kwargs.update({ 'views': views }) return form_kwargs + def get_success_url(self): + return self.get_object().get_absolute_url() + class ProjectUpdateParentView(ObjectPermissionMixin, RedirectViewMixin, UpdateView): model = Project diff --git a/rdmo/projects/viewsets.py b/rdmo/projects/viewsets.py index 142d0e31bf..317b53a6e2 100644 --- a/rdmo/projects/viewsets.py +++ b/rdmo/projects/viewsets.py @@ -371,26 +371,24 @@ def imports(self, request): def perform_create(self, serializer): project = serializer.save(site=get_current_site(self.request)) + # add current user as owner + membership = Membership(project=project, user=self.request.user, role='owner') + membership.save() + + # add all tasks to project - tasks = Task.objects.filter_current_site() \ - .filter_catalog(project.catalog) \ - .filter_group(self.request.user) \ - .filter_availability(self.request.user) - for task in tasks: - project.tasks.add(task) + if self.request.data.get('tasks') is None: + if not settings.PROJECT_TASKS_SYNC: + tasks = Task.objects.filter_for_project(project).filter_availability(self.request.user) + for task in tasks: + project.tasks.add(task) if self.request.data.get('views') is None: # add all views to project - views = View.objects.filter_current_site() \ - .filter_catalog(project.catalog) \ - .filter_group(self.request.user) \ - .filter_availability(self.request.user) - for view in views: - project.views.add(view) - - # add current user as owner - membership = Membership(project=project, user=self.request.user, role='owner') - membership.save() + if not settings.PROJECT_VIEWS_SYNC: + views = View.objects.filter_for_project(project).filter_availability(self.request.user) + for view in views: + project.views.add(view) class ProjectNestedViewSetMixin(NestedViewSetMixin): diff --git a/rdmo/tasks/managers.py b/rdmo/tasks/managers.py index 0251eab9f0..f3ee3ca8ae 100644 --- a/rdmo/tasks/managers.py +++ b/rdmo/tasks/managers.py @@ -1,4 +1,4 @@ -from django.db import models +from django.db.models import Manager, Q, QuerySet from rdmo.core.managers import ( AvailabilityManagerMixin, @@ -10,16 +10,30 @@ ) -class TaskQuestionSet(CurrentSiteQuerySetMixin, GroupsQuerySetMixin, AvailabilityQuerySetMixin, models.QuerySet): +class TaskQuestionSet(CurrentSiteQuerySetMixin, GroupsQuerySetMixin, AvailabilityQuerySetMixin, QuerySet): def filter_catalog(self, catalog): - return self.filter(models.Q(catalogs=None) | models.Q(catalogs=catalog)) + return self.filter(Q(catalogs=None) | Q(catalogs=catalog)) + def filter_for_project_site(self, project): + return self.filter(Q(sites=None) | Q(sites=project.site)) -class TaskManager(CurrentSiteManagerMixin, GroupsManagerMixin, AvailabilityManagerMixin, models.Manager): + def filter_for_project_group(self, project): + return self.filter(Q(groups=None) | Q(groups__in=project.groups)) - def get_queryset(self): +class TaskManager(CurrentSiteManagerMixin, GroupsManagerMixin, AvailabilityManagerMixin, Manager): + + def get_queryset(self) -> TaskQuestionSet: return TaskQuestionSet(self.model, using=self._db) def filter_catalog(self, catalog): return self.get_queryset().filter_catalog(catalog) + + def filter_for_project(self, project): + return ( + self + .get_queryset() + .filter_for_project_site(project) + .filter_catalog(project.catalog) + .filter_for_project_group(project) + ) diff --git a/rdmo/views/managers.py b/rdmo/views/managers.py index 4c584e4839..3886836df4 100644 --- a/rdmo/views/managers.py +++ b/rdmo/views/managers.py @@ -1,4 +1,4 @@ -from django.db import models +from django.db.models import Manager, Q, QuerySet from rdmo.core.managers import ( AvailabilityManagerMixin, @@ -10,16 +10,30 @@ ) -class ViewQuestionSet(CurrentSiteQuerySetMixin, GroupsQuerySetMixin, AvailabilityQuerySetMixin, models.QuerySet): +class ViewQuestionSet(CurrentSiteQuerySetMixin, GroupsQuerySetMixin, AvailabilityQuerySetMixin, QuerySet): def filter_catalog(self, catalog): - return self.filter(models.Q(catalogs=None) | models.Q(catalogs=catalog)) + return self.filter(Q(catalogs=None) | Q(catalogs=catalog)) + def filter_for_project_site(self, project): + return self.filter(Q(sites=None) | Q(sites=project.site)) -class ViewManager(CurrentSiteManagerMixin, GroupsManagerMixin, AvailabilityManagerMixin, models.Manager): + def filter_for_project_group(self, project): + return self.filter(Q(groups=None) | Q(groups__in=project.groups)) - def get_queryset(self): +class ViewManager(CurrentSiteManagerMixin, GroupsManagerMixin, AvailabilityManagerMixin, Manager): + + def get_queryset(self) -> ViewQuestionSet: return ViewQuestionSet(self.model, using=self._db) def filter_catalog(self, catalog): return self.get_queryset().filter_catalog(catalog) + + def filter_for_project(self, project): + return ( + self + .get_queryset() + .filter_for_project_site(project) + .filter_catalog(project.catalog) + .filter_for_project_group(project) + ) diff --git a/testing/config/settings/base.py b/testing/config/settings/base.py index dad68a9871..666edfb018 100644 --- a/testing/config/settings/base.py +++ b/testing/config/settings/base.py @@ -69,8 +69,6 @@ PROJECT_SEND_INVITE = True -PROJECT_REMOVE_VIEWS = True - PROJECT_SNAPSHOT_EXPORTS = [ ('xml', _('RDMO XML'), 'rdmo.projects.exports.RDMOXMLExport'), ]