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'),
]
|