From 9b9fdcedc8fb7d69e7738256ae7d652c506a0ca0 Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Thu, 15 Aug 2024 18:10:08 +0200 Subject: [PATCH 01/10] Add visibility to projects --- rdmo/core/settings.py | 3 +++ rdmo/projects/constants.py | 7 +++++++ rdmo/projects/forms.py | 13 +++++++++++- rdmo/projects/managers.py | 4 +++- .../migrations/0061_project_visibility.py | 18 ++++++++++++++++ rdmo/projects/models/project.py | 21 +++++++++++++++++++ rdmo/projects/rules.py | 19 ++++++++++------- .../projects/project_detail_header.html | 10 +++++++++ .../project_detail_header_visibility.html | 13 ++++++++++++ .../projects/project_detail_sidebar.html | 5 +++++ rdmo/projects/urls/__init__.py | 3 +++ rdmo/projects/views/__init__.py | 1 + rdmo/projects/views/project_update.py | 8 +++++++ 13 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 rdmo/projects/migrations/0061_project_visibility.py create mode 100644 rdmo/projects/templates/projects/project_detail_header_visibility.html diff --git a/rdmo/core/settings.py b/rdmo/core/settings.py index b19fc15358..c6610be4de 100644 --- a/rdmo/core/settings.py +++ b/rdmo/core/settings.py @@ -200,6 +200,7 @@ 'MULTISITE', 'GROUPS', 'EXPORT_FORMATS', + 'PROJECT_VISIBILITY', 'PROJECT_ISSUES', 'PROJECT_VIEWS', 'PROJECT_EXPORTS', @@ -296,6 +297,8 @@ PROJECT_TABLE_PAGE_SIZE = 20 +PROJECT_VISIBILITY = True + PROJECT_ISSUES = True PROJECT_ISSUE_PROVIDERS = [] diff --git a/rdmo/projects/constants.py b/rdmo/projects/constants.py index 77add3ea45..acec835398 100644 --- a/rdmo/projects/constants.py +++ b/rdmo/projects/constants.py @@ -10,3 +10,10 @@ (ROLE_AUTHOR, _('Author')), (ROLE_GUEST, _('Guest')), ) + +VISIBILITY_PRIVATE = 'private' +VISIBILITY_INTERNAL = 'internal' +VISIBILITY_CHOICES = ( + (VISIBILITY_PRIVATE, _('Private')), + (VISIBILITY_INTERNAL, _('Internal')) +) diff --git a/rdmo/projects/forms.py b/rdmo/projects/forms.py index 11adc0f1b9..05bbcab3fd 100644 --- a/rdmo/projects/forms.py +++ b/rdmo/projects/forms.py @@ -80,6 +80,8 @@ class Meta: fields = ['title', 'description', 'catalog'] if settings.NESTED_PROJECTS: fields += ['parent'] + if settings.PROJECT_VISIBILITY: + fields += ['visibility'] field_classes = { 'catalog': CatalogChoiceField @@ -95,7 +97,16 @@ class ProjectUpdateInformationForm(forms.ModelForm): class Meta: model = Project - fields = ('title', 'description') + fields = ('title', 'description', 'visibility') + + +class ProjectUpdateVisibilityForm(forms.ModelForm): + + use_required_attribute = False + + class Meta: + model = Project + fields = ('visibility', ) class ProjectUpdateCatalogForm(forms.ModelForm): diff --git a/rdmo/projects/managers.py b/rdmo/projects/managers.py index 72e7c21a0b..988303ed92 100644 --- a/rdmo/projects/managers.py +++ b/rdmo/projects/managers.py @@ -8,6 +8,8 @@ from rdmo.accounts.utils import is_site_manager from rdmo.core.managers import CurrentSiteManagerMixin +from .constants import VISIBILITY_INTERNAL + class ProjectQuerySet(TreeQuerySet): @@ -21,7 +23,7 @@ def filter_user(self, user): elif is_site_manager(user): return self.filter_current_site() else: - queryset = self.filter(user=user) + queryset = self.filter(Q(user=user) | models.Q(visibility=VISIBILITY_INTERNAL)) for instance in queryset: queryset |= instance.get_descendants() return queryset.distinct() diff --git a/rdmo/projects/migrations/0061_project_visibility.py b/rdmo/projects/migrations/0061_project_visibility.py new file mode 100644 index 0000000000..a956d40027 --- /dev/null +++ b/rdmo/projects/migrations/0061_project_visibility.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-08-15 15:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0060_alter_issue_options'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='visibility', + field=models.CharField(choices=[('private', 'Private'), ('internal', 'Internal')], default='private', help_text='The visibility for this project.', max_length=8, verbose_name='visibility'), + ), + ] diff --git a/rdmo/projects/models/project.py b/rdmo/projects/models/project.py index b992c0dc9d..4276e49b28 100644 --- a/rdmo/projects/models/project.py +++ b/rdmo/projects/models/project.py @@ -14,6 +14,7 @@ from rdmo.tasks.models import Task from rdmo.views.models import View +from ..constants import VISIBILITY_CHOICES, VISIBILITY_INTERNAL, VISIBILITY_PRIVATE from ..managers import ProjectManager @@ -72,6 +73,11 @@ class Project(MPTTModel, Model): verbose_name=_('Progress count'), help_text=_('The number of values for the progress bar.') ) + visibility = models.CharField( + max_length=8, choices=VISIBILITY_CHOICES, default=VISIBILITY_PRIVATE, + verbose_name=_('visibility'), + help_text=_('The visibility for this project.') + ) class Meta: ordering = ('tree_id', 'level', 'title') @@ -128,6 +134,21 @@ def file_size(self): queryset = self.values.filter(snapshot=None).exclude(models.Q(file='') | models.Q(file=None)) return sum([value.file.size for value in queryset]) + @property + def is_private(self): + return self.visibility == VISIBILITY_PRIVATE + + @property + def is_internal(self): + return self.visibility == VISIBILITY_INTERNAL + + @property + def get_visibility_help(self): + if self.is_private: + return _('Project access must be granted explicitly to each user.') + elif self.is_internal: + return _('The project can be accessed by any logged in user.') + def get_members(self, role): try: # membership_list is created by the Prefetch call in the viewset diff --git a/rdmo/projects/rules.py b/rdmo/projects/rules.py index c0b7901bab..2bb3c968ca 100644 --- a/rdmo/projects/rules.py +++ b/rdmo/projects/rules.py @@ -46,6 +46,11 @@ def is_project_guest(user, project): return user in project.guests or (project.parent and is_project_guest(user, project.parent)) +@rules.predicate +def is_internal_project(user, project): + return user.is_authenticated and project.is_internal + + @rules.predicate def is_site_manager(user, project): if user.is_authenticated: @@ -67,7 +72,7 @@ def is_site_manager_for_current_site(user, request): rules.add_rule('projects.can_view_all_projects', is_site_manager_for_current_site | is_superuser) rules.add_perm('projects.add_project', can_add_project) -rules.add_perm('projects.view_project_object', is_project_member | is_site_manager) +rules.add_perm('projects.view_project_object', is_project_member | is_internal_project | is_site_manager) rules.add_perm('projects.change_project_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.change_project_progress_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) # noqa: E501 rules.add_perm('projects.delete_project_object', is_project_owner | is_site_manager) @@ -75,7 +80,7 @@ def is_site_manager_for_current_site(user, request): rules.add_perm('projects.export_project_object', is_project_owner | is_project_manager | is_site_manager) rules.add_perm('projects.import_project_object', is_project_owner | is_project_manager | is_site_manager) -rules.add_perm('projects.view_membership_object', is_project_member | is_site_manager) +rules.add_perm('projects.view_membership_object', is_project_member | is_internal_project | is_site_manager) rules.add_perm('projects.add_membership_object', is_project_owner | is_site_manager) rules.add_perm('projects.change_membership_object', is_project_owner | is_site_manager) rules.add_perm('projects.delete_membership_object', is_project_owner | is_site_manager) @@ -85,28 +90,28 @@ def is_site_manager_for_current_site(user, request): rules.add_perm('projects.change_invite_object', is_project_owner | is_site_manager) rules.add_perm('projects.delete_invite_object', is_project_owner | is_site_manager) -rules.add_perm('projects.view_integration_object', is_project_member | is_site_manager) +rules.add_perm('projects.view_integration_object', is_project_member | is_internal_project | is_site_manager) rules.add_perm('projects.add_integration_object', is_project_owner | is_project_manager | is_site_manager) rules.add_perm('projects.change_integration_object', is_project_owner | is_project_manager | is_site_manager) rules.add_perm('projects.delete_integration_object', is_project_owner | is_project_manager | is_site_manager) -rules.add_perm('projects.view_issue_object', is_project_member | is_site_manager) +rules.add_perm('projects.view_issue_object', is_project_member | is_internal_project | is_site_manager) rules.add_perm('projects.add_issue_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.change_issue_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) # noqa: E501 rules.add_perm('projects.delete_issue_object', is_project_manager | is_project_owner | is_site_manager) -rules.add_perm('projects.view_snapshot_object', is_project_member | is_site_manager) +rules.add_perm('projects.view_snapshot_object', is_project_member | is_internal_project | is_site_manager) rules.add_perm('projects.add_snapshot_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.change_snapshot_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.rollback_snapshot_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.export_snapshot_object', is_project_owner | is_project_manager | is_site_manager) -rules.add_perm('projects.view_value_object', is_project_member | is_site_manager) +rules.add_perm('projects.view_value_object', is_project_member | is_internal_project | is_site_manager) rules.add_perm('projects.add_value_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.change_value_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) # noqa: E501 rules.add_perm('projects.delete_value_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) # noqa: E501 -rules.add_perm('projects.view_page_object', is_project_member | is_site_manager) +rules.add_perm('projects.view_page_object', is_project_member | is_internal_project | is_site_manager) # TODO: use one of the permissions above rules.add_perm('projects.is_project_owner', is_project_owner) diff --git a/rdmo/projects/templates/projects/project_detail_header.html b/rdmo/projects/templates/projects/project_detail_header.html index 6302201657..c93e2f6bfe 100644 --- a/rdmo/projects/templates/projects/project_detail_header.html +++ b/rdmo/projects/templates/projects/project_detail_header.html @@ -26,6 +26,16 @@

{{ project.title }}

{% include 'projects/project_detail_header_catalog.html' %} + {% if settings.PROJECT_VISIBILITY %} + + + {% trans 'Visibility' %} + + + {% include 'projects/project_detail_header_visibility.html' %} + + + {% endif %} {% if project.is_child_node or project.get_descendant_count %} {% drilldown_tree_for_node project as project_tree all_descendants %} diff --git a/rdmo/projects/templates/projects/project_detail_header_visibility.html b/rdmo/projects/templates/projects/project_detail_header_visibility.html new file mode 100644 index 0000000000..15e20eeda4 --- /dev/null +++ b/rdmo/projects/templates/projects/project_detail_header_visibility.html @@ -0,0 +1,13 @@ +{% load i18n %} +{% load core_tags %} + +{% if can_change_project %} +

+ + + +

+{% endif %} + +{{ project.get_visibility_display }} +{{ project.get_visibility_help }} diff --git a/rdmo/projects/templates/projects/project_detail_sidebar.html b/rdmo/projects/templates/projects/project_detail_sidebar.html index 3943bcccd2..19666138d9 100644 --- a/rdmo/projects/templates/projects/project_detail_sidebar.html +++ b/rdmo/projects/templates/projects/project_detail_sidebar.html @@ -48,6 +48,11 @@

{% trans 'Options' %}

  • {% trans 'Update project information' %}
  • + {% if settings.PROJECT_VISIBILITY %} +
  • + {% trans 'Update project visibility' %} +
  • + {% endif %}
  • {% trans 'Update project catalog' %}
  • diff --git a/rdmo/projects/urls/__init__.py b/rdmo/projects/urls/__init__.py index 7fa4a27705..ce8d1a95a6 100644 --- a/rdmo/projects/urls/__init__.py +++ b/rdmo/projects/urls/__init__.py @@ -34,6 +34,7 @@ ProjectUpdateTasksView, ProjectUpdateView, ProjectUpdateViewsView, + ProjectUpdateVisibilityView, ProjectViewExportView, ProjectViewView, SnapshotCreateView, @@ -64,6 +65,8 @@ ProjectUpdateView.as_view(), name='project_update'), re_path(r'^(?P[0-9]+)/update/information/$', ProjectUpdateInformationView.as_view(), name='project_update_information'), + re_path(r'^(?P[0-9]+)/update/visibility/$', + ProjectUpdateVisibilityView.as_view(), name='project_update_visibility'), re_path(r'^(?P[0-9]+)/update/catalog/$', ProjectUpdateCatalogView.as_view(), name='project_update_catalog'), re_path(r'^(?P[0-9]+)/update/parent/$', diff --git a/rdmo/projects/views/__init__.py b/rdmo/projects/views/__init__.py index 7cee69be9f..af072ff51e 100644 --- a/rdmo/projects/views/__init__.py +++ b/rdmo/projects/views/__init__.py @@ -25,6 +25,7 @@ ProjectUpdateTasksView, ProjectUpdateView, ProjectUpdateViewsView, + ProjectUpdateVisibilityView, ) from .project_view import ProjectViewExportView, ProjectViewView from .snapshot import SnapshotCreateView, SnapshotExportView, SnapshotRollbackView, SnapshotUpdateView diff --git a/rdmo/projects/views/project_update.py b/rdmo/projects/views/project_update.py index b6e79be2cd..f6203e493f 100644 --- a/rdmo/projects/views/project_update.py +++ b/rdmo/projects/views/project_update.py @@ -14,6 +14,7 @@ ProjectUpdateParentForm, ProjectUpdateTasksForm, ProjectUpdateViewsForm, + ProjectUpdateVisibilityForm, ) from ..mixins import ProjectImportMixin from ..models import Project @@ -48,6 +49,13 @@ class ProjectUpdateInformationView(ObjectPermissionMixin, RedirectViewMixin, Upd permission_required = 'projects.change_project_object' +class ProjectUpdateVisibilityView(ObjectPermissionMixin, RedirectViewMixin, UpdateView): + model = Project + queryset = Project.objects.all() + form_class = ProjectUpdateVisibilityForm + permission_required = 'projects.change_project_object' + + class ProjectUpdateCatalogView(ObjectPermissionMixin, RedirectViewMixin, UpdateView): model = Project queryset = Project.objects.all() From a081febbfc1d9733a8be569044a5146f38fbb37e Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Fri, 16 Aug 2024 13:04:36 +0200 Subject: [PATCH 02/10] Fix ProjectUpdateInformationForm --- rdmo/projects/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rdmo/projects/forms.py b/rdmo/projects/forms.py index 05bbcab3fd..c45008d9e3 100644 --- a/rdmo/projects/forms.py +++ b/rdmo/projects/forms.py @@ -97,7 +97,7 @@ class ProjectUpdateInformationForm(forms.ModelForm): class Meta: model = Project - fields = ('title', 'description', 'visibility') + fields = ('title', 'description') class ProjectUpdateVisibilityForm(forms.ModelForm): From 9f05c62488848c42ee45895aec196be008bc6609 Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Fri, 16 Aug 2024 13:37:59 +0200 Subject: [PATCH 03/10] Refactor project tests for internal projects --- rdmo/projects/tests/test_view_integration.py | 9 - rdmo/projects/tests/test_view_issue.py | 259 ++++++++---------- rdmo/projects/tests/test_view_project.py | 99 +++++-- .../projects/tests/test_view_project_leave.py | 5 +- rdmo/projects/tests/test_view_snapshot.py | 27 +- rdmo/projects/tests/test_viewset_issue.py | 15 +- .../projects/tests/test_viewset_membership.py | 15 +- rdmo/projects/tests/test_viewset_project.py | 32 +-- .../tests/test_viewset_project_integration.py | 24 +- .../tests/test_viewset_project_issue.py | 62 ++--- .../tests/test_viewset_project_membership.py | 55 ++-- .../tests/test_viewset_project_page.py | 15 +- .../tests/test_viewset_project_progress.py | 23 +- .../tests/test_viewset_project_snapshot.py | 77 +++--- .../tests/test_viewset_project_value.py | 87 +++--- rdmo/projects/tests/test_viewset_snapshot.py | 25 +- rdmo/projects/tests/test_viewset_value.py | 35 ++- 17 files changed, 447 insertions(+), 417 deletions(-) diff --git a/rdmo/projects/tests/test_view_integration.py b/rdmo/projects/tests/test_view_integration.py index cb20690f8d..711b7790ee 100644 --- a/rdmo/projects/tests/test_view_integration.py +++ b/rdmo/projects/tests/test_view_integration.py @@ -14,15 +14,6 @@ ('anonymous', None), ) -view_integration_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] -} - add_integration_permission_map = change_integration_permission_map = delete_integration_permission_map = { 'owner': [1, 2, 3, 4, 5], 'manager': [1, 3, 5], diff --git a/rdmo/projects/tests/test_view_issue.py b/rdmo/projects/tests/test_view_issue.py index 8780c7114f..cf4f6d745e 100644 --- a/rdmo/projects/tests/test_view_issue.py +++ b/rdmo/projects/tests/test_view_issue.py @@ -21,137 +21,113 @@ ) view_issue_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } -change_issue_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] -} - -delete_issue_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] +change_issue_permission_map = delete_issue_permission_map = { + 'owner': [1, 2, 3, 4, 12], + 'manager': [1, 3], + 'author': [1, 3], + 'api': [1, 2, 3, 4, 12], + 'site': [1, 2, 3, 4, 12], } - -projects = [1, 2, 3, 4, 5] -issues = [1, 2, 3, 4] +issues = [1, 2, 3, 4, 9] integration_pk = 1 issue_status = ('open', 'in_progress', 'closed') @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('issue_id', issues) -def test_issue(db, client, username, password, project_id, issue_id): +def test_issue(db, client, username, password, issue_id): client.login(username=username, password=password) - issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() + issue = Issue.objects.get(id=issue_id) - url = reverse('issue', args=[project_id, issue_id]) + url = reverse('issue', args=[issue.project_id, issue_id]) response = client.get(url) - if issue: - if project_id in view_issue_permission_map.get(username, []): - assert response.status_code == 200 - elif password: - assert response.status_code == 403 - else: - assert response.status_code == 302 + if issue.project_id in view_issue_permission_map.get(username, []): + assert response.status_code == 200 + elif password: + assert response.status_code == 403 else: - assert response.status_code == 404 + assert response.status_code == 302 @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('issue_id', issues) -def test_issue_update_get(db, client, username, password, project_id, issue_id): +def test_issue_update_get(db, client, username, password, issue_id): client.login(username=username, password=password) - issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() + issue = Issue.objects.get(id=issue_id) - url = reverse('issue_update', args=[project_id, issue_id]) + url = reverse('issue_update', args=[issue.project_id, issue_id]) response = client.get(url) - if issue: - if project_id in change_issue_permission_map.get(username, []): - assert response.status_code == 200 - elif password: - assert response.status_code == 403 - else: - assert response.status_code == 302 + if issue.project_id in change_issue_permission_map.get(username, []): + assert response.status_code == 200 + elif password: + assert response.status_code == 403 else: - assert response.status_code == 404 + assert response.status_code == 302 @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('issue_id', issues) @pytest.mark.parametrize('status', issue_status) -def test_issue_update_post(db, client, username, password, project_id, issue_id, status): +def test_issue_update_post(db, client, username, password, issue_id, status): client.login(username=username, password=password) - issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() + issue = Issue.objects.get(id=issue_id) - url = reverse('issue_update', args=[project_id, issue_id]) + url = reverse('issue_update', args=[issue.project_id, issue_id]) data = { 'status': status } response = client.post(url, data) - if issue: - if project_id in change_issue_permission_map.get(username, []): - assert response.status_code == 302 - assert Issue.objects.get(id=issue_id).status == status + if issue.project_id in change_issue_permission_map.get(username, []): + assert response.status_code == 302 + assert Issue.objects.get(id=issue_id).status == status + else: + if password: + assert response.status_code == 403 else: - if password: - assert response.status_code == 403 - else: - assert response.status_code == 302 + assert response.status_code == 302 + + assert Issue.objects.get(id=issue_id).status == issue.status - assert Issue.objects.get(id=issue_id).status == issue.status - else: - assert response.status_code == 404 @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('issue_id', issues) -@pytest.mark.parametrize('project_id', projects) -def test_issue_send_get(db, client, username, password, project_id, issue_id): +def test_issue_send_get(db, client, username, password, issue_id): client.login(username=username, password=password) - issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() + issue = Issue.objects.get(id=issue_id) - url = reverse('issue_update', args=[project_id, issue_id]) + url = reverse('issue_update', args=[issue.project_id, issue_id]) response = client.get(url) - if issue: - if project_id in change_issue_permission_map.get(username, []): - assert response.status_code == 200 - elif password: - assert response.status_code == 403 - else: - assert response.status_code == 302 + if issue.project_id in change_issue_permission_map.get(username, []): + assert response.status_code == 200 + elif password: + assert response.status_code == 403 else: - assert response.status_code == 404 + assert response.status_code == 302 @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('issue_id', issues) -@pytest.mark.parametrize('project_id', projects) -def test_issue_send_post_email(db, client, username, password, project_id, issue_id): +def test_issue_send_post_email(db, client, username, password, issue_id): client.login(username=username, password=password) - issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() + issue = Issue.objects.get(id=issue_id) - url = reverse('issue_send', args=[project_id, issue_id]) + url = reverse('issue_send', args=[issue.project_id, issue_id]) data = { 'subject': 'Subject', 'message': 'Message', @@ -159,12 +135,57 @@ def test_issue_send_post_email(db, client, username, password, project_id, issue } response = client.post(url, data) - if issue: - if project_id in change_issue_permission_map.get(username, []): + if issue.project_id in change_issue_permission_map.get(username, []): + assert response.status_code == 302 + assert len(mail.outbox) == 1 + assert mail.outbox[0].subject == '[example.com] Subject' + assert mail.outbox[0].body == 'Message' + else: + if password: + assert response.status_code == 403 + else: + assert response.status_code == 302 + + assert len(mail.outbox) == 0 + + +@pytest.mark.parametrize('username,password', users) +@pytest.mark.parametrize('issue_id', issues) +def test_issue_send_post_attachments(db, client, files, username, password, issue_id): + client.login(username=username, password=password) + issue = Issue.objects.get(id=issue_id) + + view = issue.project.views.first() + file = issue.project.values.filter(snapshot=None, value_type=VALUE_TYPE_FILE).first() + + if file and view: + url = reverse('issue_send', args=[issue.project_id, issue_id]) + data = { + 'subject': 'Subject', + 'message': 'Message', + 'recipients': 'email@example.com', + 'attachments_answers': 'project_answers', + 'attachments_views': str(view.id), + 'attachments_files': str(file.id), + 'attachments_snapshot': '', + 'attachments_format': 'html' + } + response = client.post(url, data) + + if issue.project_id in change_issue_permission_map.get(username, []): assert response.status_code == 302 assert len(mail.outbox) == 1 assert mail.outbox[0].subject == '[example.com] Subject' assert mail.outbox[0].body == 'Message' + + attachments = mail.outbox[0].attachments + assert len(attachments) == 3 + assert attachments[0][0] == 'Test.html' + assert attachments[0][2] == 'text/html; charset=utf-8' + assert attachments[1][0] == 'Test.html' + assert attachments[1][2] == 'text/html; charset=utf-8' + assert attachments[2][0] == 'test.txt' + assert attachments[2][2] == 'text/plain' else: if password: assert response.status_code == 403 @@ -172,69 +193,18 @@ def test_issue_send_post_email(db, client, username, password, project_id, issue assert response.status_code == 302 assert len(mail.outbox) == 0 - else: - assert response.status_code == 404 - - -@pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('issue_id', issues) -@pytest.mark.parametrize('project_id', projects) -def test_issue_send_post_attachments(db, client, files, username, password, project_id, issue_id): - client.login(username=username, password=password) - issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() - - if issue: - view = issue.project.views.first() - file = issue.project.values.filter(snapshot=None, value_type=VALUE_TYPE_FILE).first() - - if file and view: - url = reverse('issue_send', args=[project_id, issue_id]) - data = { - 'subject': 'Subject', - 'message': 'Message', - 'recipients': 'email@example.com', - 'attachments_answers': 'project_answers', - 'attachments_views': str(view.id), - 'attachments_files': str(file.id), - 'attachments_snapshot': '', - 'attachments_format': 'html' - } - response = client.post(url, data) - - if project_id in change_issue_permission_map.get(username, []): - assert response.status_code == 302 - assert len(mail.outbox) == 1 - assert mail.outbox[0].subject == '[example.com] Subject' - assert mail.outbox[0].body == 'Message' - - attachments = mail.outbox[0].attachments - assert len(attachments) == 3 - assert attachments[0][0] == 'Test.html' - assert attachments[0][2] == 'text/html; charset=utf-8' - assert attachments[1][0] == 'Test.html' - assert attachments[1][2] == 'text/html; charset=utf-8' - assert attachments[2][0] == 'test.txt' - assert attachments[2][2] == 'text/plain' - else: - if password: - assert response.status_code == 403 - else: - assert response.status_code == 302 - - assert len(mail.outbox) == 0 @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('issue_id', issues) -@pytest.mark.parametrize('project_id', projects) -def test_issue_send_post_integration(db, client, mocker, username, password, project_id, issue_id): +def test_issue_send_post_integration(db, client, mocker, username, password, issue_id): mocked_send_issue = Mock(return_value=HttpResponseRedirect(redirect_to='https://example.com/login/oauth/authorize')) mocker.patch('rdmo.projects.providers.SimpleIssueProvider.send_issue', mocked_send_issue) client.login(username=username, password=password) - issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() + issue = Issue.objects.get(id=issue_id) - url = reverse('issue_send', args=[project_id, issue_id]) + url = reverse('issue_send', args=[issue.project_id, issue_id]) data = { 'subject': 'Subject', 'message': 'Message', @@ -242,18 +212,15 @@ def test_issue_send_post_integration(db, client, mocker, username, password, pro } response = client.post(url, data) - if issue: - if project_id in change_issue_permission_map.get(username, []): - if integration_pk in Project.objects.get(pk=project_id).integrations.values_list('id', flat=True): - assert response.status_code == 302 - assert response.url.startswith('https://example.com') - else: - assert response.status_code == 200 + if issue.project_id in change_issue_permission_map.get(username, []): + if integration_pk in Project.objects.get(pk=issue.project_id).integrations.values_list('id', flat=True): + assert response.status_code == 302 + assert response.url.startswith('https://example.com') else: - if password: - assert response.status_code == 403 - else: - assert response.status_code == 302 - assert not response.url.startswith('https://example.com') + assert response.status_code == 200 else: - assert response.status_code == 404 + if password: + assert response.status_code == 403 + else: + assert response.status_code == 302 + assert not response.url.startswith('https://example.com') diff --git a/rdmo/projects/tests/test_view_project.py b/rdmo/projects/tests/test_view_project.py index 8a1465c765..e51768bc7d 100644 --- a/rdmo/projects/tests/test_view_project.py +++ b/rdmo/projects/tests/test_view_project.py @@ -17,43 +17,47 @@ ('author', 'author'), ('guest', 'guest'), ('user', 'user'), - ('site', 'site'), - ('anonymous', None), ('editor', 'editor'), ('reviewer', 'reviewer'), ('api', 'api'), + ('site', 'site'), + ('anonymous', None) ) view_project_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], - 'manager': [1, 3, 5, 7], - 'author': [1, 3, 5, 8], - 'guest': [1, 3, 5, 9], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + 'owner': [1, 2, 3, 4, 5, 10, 12], + 'manager': [1, 3, 5, 7, 12], + 'author': [1, 3, 5, 8, 12], + 'guest': [1, 3, 5, 9, 12], + 'user': [12], + 'editor': [12], + 'reviewer': [12], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] } change_project_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], + 'owner': [1, 2, 3, 4, 5, 10, 12], 'manager': [1, 3, 5, 7], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] } delete_project_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + 'owner': [1, 2, 3, 4, 5, 10, 12], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], } export_project_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], + 'owner': [1, 2, 3, 4, 5, 10, 12], 'manager': [1, 3, 5, 7], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], } -projects = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +projects = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] +projects_internal = [12] export_formats = ('rtf', 'odt', 'docx', 'html', 'markdown', 'tex', 'pdf') @@ -209,7 +213,8 @@ def test_project_create_post(db, client, username, password): data = { 'title': 'A new project', 'description': 'Some description', - 'catalog': catalog_id + 'catalog': catalog_id, + 'visibility': 'private' } response = client.post(url, data) @@ -235,7 +240,8 @@ def test_project_create_post_restricted(db, client, settings): data = { 'title': 'A new project', 'description': 'Some description', - 'catalog': catalog_id + 'catalog': catalog_id, + 'visibility': 'private' } response = client.post(url, data) @@ -251,7 +257,8 @@ def test_project_create_post_forbidden(db, client, settings): data = { 'title': 'A new project', 'description': 'Some description', - 'catalog': catalog_id + 'catalog': catalog_id, + 'visibility': 'private' } response = client.post(url, data) @@ -268,7 +275,8 @@ def test_project_create_parent_post(db, client, username, password): 'title': 'A new project', 'description': 'Some description', 'catalog': catalog_id, - 'parent': project_id + 'parent': project_id, + 'visibility': 'private' } response = client.post(url, data) @@ -310,7 +318,8 @@ def test_project_update_post(db, client, username, password, project_id): data = { 'title': 'New title', 'description': project.description, - 'catalog': project.catalog.pk + 'catalog': project.catalog.pk, + 'visibility': 'private' } response = client.post(url, data) @@ -337,7 +346,8 @@ def test_project_update_post_parent(db, client, username, password, project_id): 'title': project.title, 'description': project.description, 'catalog': project.catalog.pk, - 'parent': parent_id + 'parent': parent_id, + 'visibility': 'private' } response = client.post(url, data) @@ -403,6 +413,47 @@ def test_project_update_information_post(db, client, username, password, project assert Project.objects.get(pk=project_id).title == project.title +@pytest.mark.parametrize('username,password', users) +@pytest.mark.parametrize('project_id', projects) +def test_project_update_visibility_get(db, client, username, password, project_id): + client.login(username=username, password=password) + + url = reverse('project_update_visibility', args=[project_id]) + response = client.get(url) + + if project_id in change_project_permission_map.get(username, []): + assert response.status_code == 200 + else: + if password: + assert response.status_code == 403 + else: + assert response.status_code == 302 + + +@pytest.mark.parametrize('username,password', users) +@pytest.mark.parametrize('project_id', projects) +def test_project_update_visibility_post(db, client, username, password, project_id): + client.login(username=username, password=password) + project = Project.objects.get(pk=project_id) + + url = reverse('project_update_visibility', args=[project_id]) + data = { + 'visibility': 'internal' + } + response = client.post(url, data) + + if project_id in change_project_permission_map.get(username, []): + assert response.status_code == 302 + assert Project.objects.get(pk=project_id).visibility == 'internal' + else: + if password: + assert response.status_code == 403 + else: + assert response.status_code == 302 + + assert Project.objects.get(pk=project_id).visibility == project.visibility + + @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) def test_project_update_catalog_get(db, client, username, password, project_id): diff --git a/rdmo/projects/tests/test_view_project_leave.py b/rdmo/projects/tests/test_view_project_leave.py index fa0fd55bbd..b8ef2c07ca 100644 --- a/rdmo/projects/tests/test_view_project_leave.py +++ b/rdmo/projects/tests/test_view_project_leave.py @@ -15,14 +15,15 @@ ) leave_project_permission_map = { - 'owner': [1, 2, 3, 4, 5], + 'owner': [1, 2, 3, 4, 5, 12], 'manager': [1, 3], 'author': [1, 3], 'guest': [1, 3] } -projects = [1, 2, 3, 4, 5] +projects = [1, 2, 3, 4, 5, 12] +internal_projects = [12] @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) diff --git a/rdmo/projects/tests/test_view_snapshot.py b/rdmo/projects/tests/test_view_snapshot.py index 92a057bf73..dedb234929 100644 --- a/rdmo/projects/tests/test_view_snapshot.py +++ b/rdmo/projects/tests/test_view_snapshot.py @@ -14,35 +14,26 @@ ('manager', 'manager'), ('author', 'author'), ('guest', 'guest'), - ('user', 'user'), + ('api', 'api'), ('site', 'site'), ('anonymous', None), ) -view_snapshot_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] -} - add_snapshot_permission_map = change_snapshot_permission_map = rollback_snapshot_permission_map = { - 'owner': [1, 2, 3, 4, 5], + 'owner': [1, 2, 3, 4, 5, 12], 'manager': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } export_snapshot_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'manager': [1, 3, 5, 12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } -projects = [1, 2, 3, 4, 5] +projects = [1, 2, 3, 4, 5, 12] snapshots = [1, 3, 7, 4, 5, 6] snapshots_project = 1 diff --git a/rdmo/projects/tests/test_viewset_issue.py b/rdmo/projects/tests/test_viewset_issue.py index 96f354478f..715821ad7b 100644 --- a/rdmo/projects/tests/test_viewset_issue.py +++ b/rdmo/projects/tests/test_viewset_issue.py @@ -16,12 +16,12 @@ ) view_issue_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5, 10], - 'site': [1, 2, 3, 4, 5, 10] + 'owner': [1, 2, 3, 4, 5, 10, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'api': [1, 2, 3, 4, 5, 10, 12], + 'site': [1, 2, 3, 4, 5, 10, 12] } urlnames = { @@ -31,6 +31,7 @@ projects = [1, 2, 3, 4, 5, 10] issues = [1, 2, 3, 4] +issues_internal = [8, 9] site_id = 1 project_id = 1 @@ -51,7 +52,7 @@ def test_list(db, client, username, password): assert isinstance(response.json(), list) if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert sorted([item['id'] for item in response.json()]) == issues_internal else: values_list = Issue.objects.filter(project__in=view_issue_permission_map.get(username, [])) \ .order_by('id').values_list('id', flat=True) diff --git a/rdmo/projects/tests/test_viewset_membership.py b/rdmo/projects/tests/test_viewset_membership.py index 6bad007774..c099c4294b 100644 --- a/rdmo/projects/tests/test_viewset_membership.py +++ b/rdmo/projects/tests/test_viewset_membership.py @@ -16,12 +16,12 @@ ) view_membership_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], - 'manager': [1, 3, 5, 7], - 'author': [1, 3, 5, 8], - 'guest': [1, 3, 5, 9], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + 'owner': [1, 2, 3, 4, 5, 10, 12], + 'manager': [1, 3, 5, 7, 12], + 'author': [1, 3, 5, 8, 12], + 'guest': [1, 3, 5, 9, 12], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] } urlnames = { @@ -31,6 +31,7 @@ projects = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] memberships = [1, 2, 3, 4] +memberships_internal = [16] membership_roles = ('owner', 'manager', 'author', 'guest') @@ -46,7 +47,7 @@ def test_list(db, client, username, password): assert isinstance(response.json(), list) if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert sorted([item['id'] for item in response.json()]) == memberships_internal else: values_list = Membership.objects.filter(project__in=view_membership_permission_map.get(username, [])) \ .order_by('id').values_list('id', flat=True) diff --git a/rdmo/projects/tests/test_viewset_project.py b/rdmo/projects/tests/test_viewset_project.py index 9f2d97fe8a..d23a268182 100644 --- a/rdmo/projects/tests/test_viewset_project.py +++ b/rdmo/projects/tests/test_viewset_project.py @@ -17,25 +17,26 @@ ) view_project_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], - 'manager': [1, 3, 5, 7], - 'author': [1, 3, 5, 8], - 'guest': [1, 3, 5, 9], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + 'owner': [1, 2, 3, 4, 5, 10, 12], + 'manager': [1, 3, 5, 7, 12], + 'author': [1, 3, 5, 8, 12], + 'guest': [1, 3, 5, 9, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] } change_project_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], + 'owner': [1, 2, 3, 4, 5, 10, 12], 'manager': [1, 3, 5, 7], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] } delete_project_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + 'owner': [1, 2, 3, 4, 5, 10, 12], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] } urlnames = { @@ -50,7 +51,8 @@ 'imports': 'v1-projects:project-imports' } -projects = [1, 2, 3, 4, 5] +projects = [1, 2, 3, 4, 5, 12] +projects_internal = [12] conditions = [1] catalog_id = 1 @@ -80,8 +82,7 @@ def test_list(db, client, username, password): assert isinstance(response_data, dict) if username == 'user': - assert response_data['count'] == 0 - assert response_data['results'] == [] + assert sorted([item['id'] for item in response.json()]) == projects_internal else: values_list = Project.objects.filter(id__in=view_project_permission_map.get(username, [])) \ .values_list('id', flat=True) @@ -461,7 +462,6 @@ def test_navigation(db, client, username, password, project_id): client.login(username=username, password=password) url = reverse(urlnames['navigation'], args=[project_id]) - print(url) response = client.get(url) if project_id in view_project_permission_map.get(username, []): diff --git a/rdmo/projects/tests/test_viewset_project_integration.py b/rdmo/projects/tests/test_viewset_project_integration.py index d1271081cc..ca373a127c 100644 --- a/rdmo/projects/tests/test_viewset_project_integration.py +++ b/rdmo/projects/tests/test_viewset_project_integration.py @@ -18,19 +18,20 @@ ) view_integration_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } add_integration_permission_map = change_integration_permission_map = delete_integration_permission_map = { - 'owner': [1, 2, 3, 4, 5], + 'owner': [1, 2, 3, 4, 5, 12], 'manager': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } urlnames = { @@ -38,8 +39,9 @@ 'detail': 'v1-projects:project-integration-detail' } -projects = [1, 2, 3, 4, 5] +projects = [1, 2, 3, 4, 5, 12] integrations = [1, 2] +integrations_internal = [] @pytest.mark.parametrize('username,password', users) @@ -54,7 +56,7 @@ def test_list(db, client, username, password, project_id): assert response.status_code == 200 if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert sorted([item['id'] for item in response.json()]) == integrations_internal else: values_list = Integration.objects.filter(project_id=project_id) \ .order_by('id').values_list('id', flat=True) diff --git a/rdmo/projects/tests/test_viewset_project_issue.py b/rdmo/projects/tests/test_viewset_project_issue.py index 16bd036b41..62b9e21e85 100644 --- a/rdmo/projects/tests/test_viewset_project_issue.py +++ b/rdmo/projects/tests/test_viewset_project_issue.py @@ -16,27 +16,28 @@ ) view_issue_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } add_issue_permission_map = delete_issue_permission_map = { - 'owner': [1, 2, 3, 4, 5], + 'owner': [1, 2, 3, 4, 5, 12], 'manager': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } change_issue_permission_map = { - 'owner': [1, 2, 3, 4, 5], + 'owner': [1, 2, 3, 4, 5, 12], 'manager': [1, 3, 5], 'author': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } urlnames = { @@ -44,8 +45,9 @@ 'detail': 'v1-projects:project-issue-detail' } -projects = [1, 2, 3, 4, 5] -issues = [1, 2, 3, 4] +projects = [1, 2, 3, 4, 5, 12] +issues = [1, 2, 3, 4, 9] +issues_internal = [8, 9] issue_status = ('open', 'in_progress', 'closed') @@ -62,7 +64,7 @@ def test_list(db, client, username, password, project_id): assert response.status_code == 200 if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert sorted([item['id'] for item in response.json()]) == issues_internal else: values_list = Issue.objects.filter(project_id=project_id) \ .order_by('id').values_list('id', flat=True) @@ -72,16 +74,15 @@ def test_list(db, client, username, password, project_id): @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('issue_id', issues) -def test_detail(db, client, username, password, project_id, issue_id): +def test_detail(db, client, username, password, issue_id): client.login(username=username, password=password) - issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() + issue = Issue.objects.get(id=issue_id) - url = reverse(urlnames['detail'], args=[project_id, issue_id]) + url = reverse(urlnames['detail'], args=[issue.project_id, issue_id]) response = client.get(url) - if issue and project_id in view_issue_permission_map.get(username, []): + if issue.project_id in view_issue_permission_map.get(username, []): assert response.status_code == 200 assert isinstance(response.json(), dict) assert response.json().get('id') == issue_id @@ -106,40 +107,39 @@ def test_create(db, client, username, password, project_id): @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('issue_id', issues) @pytest.mark.parametrize('status', issue_status) -def test_update(db, client, username, password, project_id, issue_id, status): +def test_update(db, client, username, password, issue_id, status): client.login(username=username, password=password) - issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() + issue = Issue.objects.get(id=issue_id) - url = reverse(urlnames['detail'], args=[project_id, issue_id]) + url = reverse(urlnames['detail'], args=[issue.project_id, issue_id]) data = { 'status': status } response = client.put(url, data, content_type='application/json') - if issue and project_id in change_issue_permission_map.get(username, []): + if issue.project_id in change_issue_permission_map.get(username, []): assert response.status_code == 200 assert response.json().get('status') == status - elif issue and project_id in view_issue_permission_map.get(username, []): + elif issue.project_id in view_issue_permission_map.get(username, []): assert response.status_code == 403 else: assert response.status_code == 404 @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('issue_id', issues) -def test_delete(db, client, username, password, project_id, issue_id): +def test_delete(db, client, username, password, issue_id): client.login(username=username, password=password) + issue = Issue.objects.get(id=issue_id) - url = reverse(urlnames['detail'], args=[project_id, issue_id]) + url = reverse(urlnames['detail'], args=[issue.project_id, issue_id]) response = client.delete(url) - if project_id in delete_issue_permission_map.get(username, []): + if issue.project_id in delete_issue_permission_map.get(username, []): assert response.status_code == 405 - elif project_id in view_issue_permission_map.get(username, []): + elif issue.project_id in view_issue_permission_map.get(username, []): assert response.status_code == 405 else: assert response.status_code == 404 diff --git a/rdmo/projects/tests/test_viewset_project_membership.py b/rdmo/projects/tests/test_viewset_project_membership.py index d935bff060..65ef0029ab 100644 --- a/rdmo/projects/tests/test_viewset_project_membership.py +++ b/rdmo/projects/tests/test_viewset_project_membership.py @@ -17,18 +17,19 @@ ) view_membership_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } add_membership_permission_map = change_membership_permission_map = delete_membership_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } urlnames = { @@ -36,8 +37,9 @@ 'detail': 'v1-projects:project-membership-detail' } -projects = [1, 2, 3, 4, 5] +projects = [1, 2, 3, 4, 5, 12] memberships = [1, 2, 3, 4] +memberships_internal = [16] membership_roles = ('owner', 'manager', 'author', 'guest') @@ -53,7 +55,7 @@ def test_list(db, client, username, password, project_id): assert response.status_code == 200 if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert sorted([item['id'] for item in response.json()]) == memberships_internal else: values_list = Membership.objects.filter(project_id=project_id) \ .order_by('id').values_list('id', flat=True) @@ -63,16 +65,15 @@ def test_list(db, client, username, password, project_id): @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('membership_id', memberships) -def test_detail(db, client, username, password, project_id, membership_id): +def test_detail(db, client, username, password, membership_id): client.login(username=username, password=password) - membership = Membership.objects.filter(project_id=project_id, id=membership_id).first() + membership = Membership.objects.get(id=membership_id) - url = reverse(urlnames['detail'], args=[project_id, membership_id]) + url = reverse(urlnames['detail'], args=[membership.project_id, membership_id]) response = client.get(url) - if membership and project_id in view_membership_permission_map.get(username, []): + if membership.project_id in view_membership_permission_map.get(username, []): assert response.status_code == 200 assert isinstance(response.json(), dict) assert response.json().get('id') == membership_id @@ -104,40 +105,38 @@ def test_create(db, client, username, password, project_id, membership_role): @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('membership_id', memberships) @pytest.mark.parametrize('membership_role', membership_roles) -def test_update(db, client, username, password, project_id, membership_id, membership_role): +def test_update(db, client, username, password, membership_id, membership_role): client.login(username=username, password=password) - membership = Membership.objects.filter(project_id=project_id, id=membership_id).first() + membership = Membership.objects.get(id=membership_id) - url = reverse(urlnames['detail'], args=[project_id, membership_id]) + url = reverse(urlnames['detail'], args=[membership.project_id, membership_id]) data = { 'role': membership_role } response = client.put(url, data, content_type='application/json') - if membership and project_id in change_membership_permission_map.get(username, []): + if membership.project_id in change_membership_permission_map.get(username, []): assert response.status_code == 200 - elif membership and project_id in view_membership_permission_map.get(username, []): + elif membership.project_id in view_membership_permission_map.get(username, []): assert response.status_code == 403 else: assert response.status_code == 404 @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('membership_id', memberships) -def test_delete(db, client, username, password, project_id, membership_id): +def test_delete(db, client, username, password, membership_id): client.login(username=username, password=password) - membership = Membership.objects.filter(project_id=project_id, id=membership_id).first() + membership = Membership.objects.get(id=membership_id) - url = reverse(urlnames['detail'], args=[project_id, membership_id]) + url = reverse(urlnames['detail'], args=[membership.project_id, membership_id]) response = client.delete(url) - if membership and project_id in delete_membership_permission_map.get(username, []): + if membership.project_id in change_membership_permission_map.get(username, []): assert response.status_code == 204 - elif membership and project_id in view_membership_permission_map.get(username, []): + elif membership.project_id in view_membership_permission_map.get(username, []): assert response.status_code == 403 else: assert response.status_code == 404 diff --git a/rdmo/projects/tests/test_viewset_project_page.py b/rdmo/projects/tests/test_viewset_project_page.py index 228ee2e3d3..b6c562692c 100644 --- a/rdmo/projects/tests/test_viewset_project_page.py +++ b/rdmo/projects/tests/test_viewset_project_page.py @@ -14,12 +14,13 @@ ) view_questionset_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } urlnames = { @@ -27,7 +28,7 @@ 'detail': 'v1-projects:project-page-detail' } -projects = [1, 2, 3, 4, 5] +projects = [1, 2, 3, 4, 5, 12] pages = [1] diff --git a/rdmo/projects/tests/test_viewset_project_progress.py b/rdmo/projects/tests/test_viewset_project_progress.py index 5d7529261f..f02c5366cd 100644 --- a/rdmo/projects/tests/test_viewset_project_progress.py +++ b/rdmo/projects/tests/test_viewset_project_progress.py @@ -16,27 +16,28 @@ ) view_progress_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], - 'manager': [1, 3, 5, 7], - 'author': [1, 3, 5, 8], - 'guest': [1, 3, 5, 9], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + 'owner': [1, 2, 3, 4, 5, 10, 12], + 'manager': [1, 3, 5, 7, 12], + 'author': [1, 3, 5, 8, 12], + 'guest': [1, 3, 5, 9, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] } change_progress_permission_map = { - 'owner': [1, 2, 3, 4, 5, 10], + 'owner': [1, 2, 3, 4, 5, 10, 12], 'manager': [1, 3, 5, 7], - 'author': [1, 3, 5, 8], - 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + 'author': [1, 3, 5, 7], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] } urlnames = { 'progress': 'v1-projects:project-progress' } -projects = [1, 2, 3, 4, 5] +projects = [1, 2, 3, 4, 5, 12] @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) diff --git a/rdmo/projects/tests/test_viewset_project_snapshot.py b/rdmo/projects/tests/test_viewset_project_snapshot.py index 54a91c3f90..3ea22dda3e 100644 --- a/rdmo/projects/tests/test_viewset_project_snapshot.py +++ b/rdmo/projects/tests/test_viewset_project_snapshot.py @@ -21,19 +21,20 @@ ) view_snapshot_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } add_snapshot_permission_map = change_snapshot_permission_map = delete_snapshot_permission_map = { - 'owner': [1, 2, 3, 4, 5], + 'owner': [1, 2, 3, 4, 5, 12], 'manager': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } urlnames = { @@ -41,9 +42,9 @@ 'detail': 'v1-projects:project-snapshot-detail' } -projects = [1, 2, 3, 4, 5] +projects = [1, 2, 3, 4, 5, 12] snapshots = [1, 3, 7, 4, 5, 6] - +snapshots_internal = [8] @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) @@ -58,7 +59,7 @@ def test_list(db, client, username, password, project_id): assert isinstance(response.json(), list) if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert sorted([item['id'] for item in response.json()]) == snapshots_internal else: values_list = Snapshot.objects.filter(project_id=project_id) \ .order_by('id').values_list('id', flat=True) @@ -69,16 +70,15 @@ def test_list(db, client, username, password, project_id): @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('snapshot_id', snapshots) -def test_detail(db, client, username, password, project_id, snapshot_id): +def test_detail(db, client, username, password, snapshot_id): client.login(username=username, password=password) - snapshot = Snapshot.objects.filter(project_id=project_id, id=snapshot_id).filter() + snapshot = Snapshot.objects.get(id=snapshot_id) - url = reverse(urlnames['detail'], args=[project_id, snapshot_id]) + url = reverse(urlnames['detail'], args=[snapshot.project_id, snapshot_id]) response = client.get(url) - if snapshot and project_id in view_snapshot_permission_map.get(username, []): + if snapshot.project_id in view_snapshot_permission_map.get(username, []): assert response.status_code == 200 assert isinstance(response.json(), dict) assert response.json().get('id') == snapshot_id @@ -123,59 +123,56 @@ def test_create(db, client, files, username, password, project_id): @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('snapshot_id', snapshots) -def test_update(db, client, files, username, password, project_id, snapshot_id): +def test_update(db, client, files, username, password, snapshot_id): client.login(username=username, password=password) - project = Project.objects.get(id=project_id) - snapshot = Snapshot.objects.filter(project_id=project_id, id=snapshot_id).first() + snapshot = Snapshot.objects.get(id=snapshot_id) - snapshot_count = project.snapshots.count() - values_count = project.values.count() - values_files = [value.file.name for value in project.values.filter(value_type=VALUE_TYPE_FILE)] + snapshot_count = snapshot.project.snapshots.count() + values_count = snapshot.project.values.count() + values_files = [value.file.name for value in snapshot.project.values.filter(value_type=VALUE_TYPE_FILE)] - url = reverse(urlnames['detail'], args=[project_id, snapshot_id]) + url = reverse(urlnames['detail'], args=[snapshot.project_id, snapshot_id]) data = { 'title': 'A new title', 'description': 'A new description' } response = client.put(url, data, content_type='application/json') - if snapshot and project_id in change_snapshot_permission_map.get(username, []): + if snapshot.project_id in change_snapshot_permission_map.get(username, []): assert response.status_code == 200 assert isinstance(response.json(), dict) - assert response.json().get('id') in project.snapshots.values_list('id', flat=True) - elif snapshot and project_id in view_snapshot_permission_map.get(username, []): + assert response.json().get('id') in snapshot.project.snapshots.values_list('id', flat=True) + elif snapshot.project_id in view_snapshot_permission_map.get(username, []): assert response.status_code == 403 else: assert response.status_code == 404 - assert project.snapshots.count() == snapshot_count - assert project.values.count() == values_count + assert snapshot.project.snapshots.count() == snapshot_count + assert snapshot.project.values.count() == values_count for file_value in values_files: assert Path(settings.MEDIA_ROOT).joinpath(file_value).exists() @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('snapshot_id', snapshots) -def test_delete(db, client, files, username, password, project_id, snapshot_id): +def test_delete(db, client, files, username, password, snapshot_id): client.login(username=username, password=password) - project = Project.objects.get(id=project_id) + snapshot = Snapshot.objects.get(id=snapshot_id) - snapshot_count = project.snapshots.count() - values_count = project.values.count() - values_files = [value.file.name for value in project.values.filter(value_type=VALUE_TYPE_FILE)] + snapshot_count = snapshot.project.snapshots.count() + values_count = snapshot.project.values.count() + values_files = [value.file.name for value in snapshot.project.values.filter(value_type=VALUE_TYPE_FILE)] - url = reverse(urlnames['detail'], args=[project_id, snapshot_id]) + url = reverse(urlnames['detail'], args=[snapshot.project_id, snapshot_id]) response = client.delete(url) - if project_id in view_snapshot_permission_map.get(username, []): + if snapshot.project_id in view_snapshot_permission_map.get(username, []): assert response.status_code == 405 else: assert response.status_code == 404 - assert project.snapshots.count() == snapshot_count - assert project.values.count() == values_count + assert snapshot.project.snapshots.count() == snapshot_count + assert snapshot.project.values.count() == values_count for file_value in values_files: assert Path(settings.MEDIA_ROOT).joinpath(file_value).exists() diff --git a/rdmo/projects/tests/test_viewset_project_value.py b/rdmo/projects/tests/test_viewset_project_value.py index e221b82214..aaa1fb2693 100644 --- a/rdmo/projects/tests/test_viewset_project_value.py +++ b/rdmo/projects/tests/test_viewset_project_value.py @@ -21,20 +21,21 @@ ) view_value_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } add_value_permission_map = change_value_permission_map = delete_value_permission_map = { - 'owner': [1, 2, 3, 4, 5], + 'owner': [1, 2, 3, 4, 5, 12], 'manager': [1, 3, 5], 'author': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } urlnames = { @@ -44,8 +45,16 @@ 'file': 'v1-projects:project-value-file' } -projects = [1, 2, 3, 4, 5] -values = [1, 2, 3, 4, 5, 6, 7, 238, 242, 247, 248, 249] +projects = [1, 2, 3, 4, 5, 12] +values = [ + 1, 2, 3, 4, 5, 6, 7, 238, # from Test <1> + 242, # from Parent <2> + 247, # from Child1 <3> + 248, # from Child2 <4> + 249, # from Child11 <5> + 456 # from Internal <12> +] +values_internal = [456] attribute_id = 1 option_id = 1 @@ -80,7 +89,7 @@ def test_list(db, client, username, password, project_id): assert isinstance(response.json(), list) if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert sorted([item['id'] for item in response.json()]) == values_internal else: values_list = Value.objects.filter(project_id=project_id) \ .filter(snapshot_id=None) \ @@ -92,16 +101,15 @@ def test_list(db, client, username, password, project_id): @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('value_id', values) -def test_detail(db, client, username, password, project_id, value_id): +def test_detail(db, client, username, password, value_id): client.login(username=username, password=password) - value = Value.objects.filter(project_id=project_id, id=value_id).filter() + value = Value.objects.get(id=value_id) - url = reverse(urlnames['detail'], args=[project_id, value_id]) + url = reverse(urlnames['detail'], args=[value.project_id, value_id]) response = client.get(url) - if value and project_id in view_value_permission_map.get(username, []): + if value.project_id in view_value_permission_map.get(username, []): assert response.status_code == 200 assert isinstance(response.json(), dict) assert response.json().get('id') == value_id @@ -193,13 +201,12 @@ def test_create_external(db, client, username, password, project_id, value_type, @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('value_id', values) -def test_update(db, client, username, password, project_id, value_id): +def test_update(db, client, username, password, value_id): client.login(username=username, password=password) - value = Value.objects.filter(project_id=project_id, id=value_id).first() + value = Value.objects.get(id=value_id) - url = reverse(urlnames['detail'], args=[project_id, value_id]) + url = reverse(urlnames['detail'], args=[value.project_id, value_id]) data = { 'attribute': attribute_id, 'set_index': 0, @@ -210,30 +217,30 @@ def test_update(db, client, username, password, project_id, value_id): } response = client.put(url, data, content_type='application/json') - if value and project_id in change_value_permission_map.get(username, []): + if value.project_id in change_value_permission_map.get(username, []): assert response.status_code == 200 assert isinstance(response.json(), dict) - assert response.json().get('id') in Value.objects.filter(project_id=project_id).values_list('id', flat=True) - elif value and project_id in view_value_permission_map.get(username, []): + assert response.json().get('id') in Value.objects.filter(project_id=value.project_id) \ + .values_list('id', flat=True) + elif value.project_id in view_value_permission_map.get(username, []): assert response.status_code == 403 else: assert response.status_code == 404 @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('value_id', values) -def test_delete(db, client, username, password, project_id, value_id): +def test_delete(db, client, username, password, value_id): client.login(username=username, password=password) - value = Value.objects.filter(project_id=project_id, id=value_id).first() + value = Value.objects.get(id=value_id) - url = reverse(urlnames['detail'], args=[project_id, value_id]) + url = reverse(urlnames['detail'], args=[value.project_id, value_id]) response = client.delete(url) - if value and project_id in delete_value_permission_map.get(username, []): + if value.project_id in delete_value_permission_map.get(username, []): assert response.status_code == 204 assert not Value.objects.filter(pk=value_id).exists() - elif value and project_id in view_value_permission_map.get(username, []): + elif value.project_id in view_value_permission_map.get(username, []): assert response.status_code == 403 assert Value.objects.filter(pk=value_id).exists() else: @@ -267,16 +274,15 @@ def test_set(db, client, username, password, project_id, value_id, set_values_co @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('value_id', values) -def test_file_get(db, client, files, username, password, project_id, value_id): +def test_file_get(db, client, files, username, password, value_id): client.login(username=username, password=password) - value = Value.objects.filter(project_id=project_id, id=value_id).first() + value = Value.objects.get(id=value_id) - url = reverse(urlnames['file'], args=[project_id, value_id]) + url = reverse(urlnames['file'], args=[value.project_id, value_id]) response = client.get(url) - if value and value.value_type == VALUE_TYPE_FILE and project_id in view_value_permission_map.get(username, []): + if value.value_type == VALUE_TYPE_FILE and value.project_id in view_value_permission_map.get(username, []): assert response.status_code == 200 assert response['Content-Type'] == value.file_type assert response['Content-Disposition'] == f'attachment; filename={value.file_name}' @@ -286,22 +292,21 @@ def test_file_get(db, client, files, username, password, project_id, value_id): @pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('value_id', values) -def test_file_put(db, client, files, username, password, project_id, value_id): +def test_file_put(db, client, files, username, password, value_id): client.login(username=username, password=password) - value = Value.objects.filter(project_id=project_id, id=value_id).first() + value = Value.objects.get(id=value_id) - url = reverse(urlnames['file'], args=[project_id, value_id]) + url = reverse(urlnames['file'], args=[value.project_id, value_id]) file_path = Path(settings.MEDIA_ROOT) / 'test_file.txt' with file_path.open() as fp: response = client.post(url, {'name': 'test_file.txt', 'file': fp}) - if value and project_id in change_value_permission_map.get(username, []): + if value.project_id in change_value_permission_map.get(username, []): assert response.status_code == 200 assert response.json().get('file_name') == 'test_file.txt' - elif value and project_id in view_value_permission_map.get(username, []): + elif value.project_id in view_value_permission_map.get(username, []): assert response.status_code == 403 else: assert response.status_code == 404 diff --git a/rdmo/projects/tests/test_viewset_snapshot.py b/rdmo/projects/tests/test_viewset_snapshot.py index 40fedc1bda..49f8b38450 100644 --- a/rdmo/projects/tests/test_viewset_snapshot.py +++ b/rdmo/projects/tests/test_viewset_snapshot.py @@ -16,12 +16,13 @@ ) view_snapshot_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } urlnames = { @@ -29,7 +30,15 @@ 'detail': 'v1-projects:snapshot-detail' } -snapshots = [1, 3, 7, 4, 5, 6] +snapshots = [ + 1, 7, # from Test <1> + 3, # from Parent <2> + 4, # from Child1 <3> + 5, # from Child2 <4> + 6, # from Child11 <5> + 8 # from Internal <12> +] +snapshots_internal = [8] @pytest.mark.parametrize('username,password', users) @@ -44,7 +53,7 @@ def test_list(db, client, username, password): assert isinstance(response.json(), list) if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert sorted([item['id'] for item in response.json()]) == snapshots_internal else: values_list = Snapshot.objects.filter(project__in=view_snapshot_permission_map.get(username, [])) \ .order_by('id').values_list('id', flat=True) diff --git a/rdmo/projects/tests/test_viewset_value.py b/rdmo/projects/tests/test_viewset_value.py index a2b3dfbf80..6a26af33ec 100644 --- a/rdmo/projects/tests/test_viewset_value.py +++ b/rdmo/projects/tests/test_viewset_value.py @@ -18,12 +18,13 @@ ) view_value_permission_map = { - 'owner': [1, 2, 3, 4, 5], - 'manager': [1, 3, 5], - 'author': [1, 3, 5], - 'guest': [1, 3, 5], - 'api': [1, 2, 3, 4, 5], - 'site': [1, 2, 3, 4, 5] + 'owner': [1, 2, 3, 4, 5, 12], + 'manager': [1, 3, 5, 12], + 'author': [1, 3, 5, 12], + 'guest': [1, 3, 5, 12], + 'user': [12], + 'api': [1, 2, 3, 4, 5, 12], + 'site': [1, 2, 3, 4, 5, 12] } urlnames = { @@ -32,9 +33,18 @@ 'file': 'v1-projects:value-file' } -values = [1, 2, 3, 4, 5, 6, 7, 238, 242, 243, 244, 245] -snapshots = [1, 3, 7, 4, 5, 6] - +values = [ + 1, 2, 3, 4, 5, 6, 7, 238, # from Test <1> + 242, 243, # from Parent <2> + 247, # from Child1 <3> + 248, # from Child2 <4> + 249, # from Child11 <5> + 456, 457 # from Internal <12> +] +values_internal = [456] +values_snapshot_internal = [457] +snapshots = [1, 3, 7, 4, 5, 6, 8] +snapshots_internal = [8] @pytest.mark.parametrize('username,password', users) def test_list(db, client, username, password): @@ -48,7 +58,7 @@ def test_list(db, client, username, password): assert isinstance(response.json(), list) if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert sorted([item['id'] for item in response.json()]) == values_internal else: values_list = Value.objects.filter(project__in=view_value_permission_map.get(username, [])) \ .filter(snapshot_id=None) \ @@ -71,7 +81,10 @@ def test_list_snapshot(db, client, username, password, snapshot_id): assert isinstance(response.json(), list) if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + if snapshot_id in snapshots_internal: + assert sorted([item['id'] for item in response.json()]) == values_snapshot_internal + else: + assert sorted([item['id'] for item in response.json()]) == [] else: values_list = Value.objects.filter(project__in=view_value_permission_map.get(username, [])) \ .filter(snapshot_id=snapshot_id) \ From a3bb72149eee6b3bb92a4898db59baad405a9753 Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Fri, 16 Aug 2024 13:38:17 +0200 Subject: [PATCH 04/10] Update projects test fixture --- testing/fixtures/projects.json | 461 +++++++++++++++++++++++++++++++-- 1 file changed, 434 insertions(+), 27 deletions(-) diff --git a/testing/fixtures/projects.json b/testing/fixtures/projects.json index 59bfbb2cc9..68fcd464cf 100644 --- a/testing/fixtures/projects.json +++ b/testing/fixtures/projects.json @@ -10,6 +10,17 @@ "page": 87 } }, + { + "model": "projects.continuation", + "pk": 2, + "fields": { + "created": "2024-08-16T10:46:55.752Z", + "updated": "2024-08-16T10:46:55.752Z", + "project": 12, + "user": 5, + "page": 1 + } + }, { "model": "projects.integration", "pk": 1, @@ -66,6 +77,30 @@ "secret": true } }, + { + "model": "projects.invite", + "pk": 1, + "fields": { + "project": 1, + "user": 11, + "email": "other@example.com", + "role": "author", + "token": "10159fadb66e97365a36", + "timestamp": "2023-07-10T14:06:55.112Z" + } + }, + { + "model": "projects.invite", + "pk": 2, + "fields": { + "project": 11, + "user": 4, + "email": "user@example.com", + "role": "author", + "token": "10159fadb66e97365a36", + "timestamp": "2023-07-10T14:06:55.112Z" + } + }, { "model": "projects.issue", "pk": 1, @@ -129,6 +164,24 @@ "status": "open" } }, + { + "model": "projects.issue", + "pk": 8, + "fields": { + "project": 12, + "task": 2, + "status": "open" + } + }, + { + "model": "projects.issue", + "pk": 9, + "fields": { + "project": 12, + "task": 1, + "status": "open" + } + }, { "model": "projects.issueresource", "pk": 1, @@ -273,17 +326,29 @@ "role": "owner" } }, + { + "model": "projects.membership", + "pk": 16, + "fields": { + "project": 12, + "user": 5, + "role": "owner" + } + }, { "model": "projects.project", "pk": 1, "fields": { "created": "2017-01-30T07:55:13.007Z", - "updated": "2017-03-01T13:57:34.993Z", + "updated": "2024-08-16T08:54:51.524Z", "parent": null, "site": 1, "title": "Test", "description": "This is a test!", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 1, "rght": 2, "tree_id": 1, @@ -304,9 +369,12 @@ "title": "Parent", "description": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est.", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 1, "rght": 6, - "tree_id": 2, + "tree_id": 4, "level": 0, "views": [ 1 @@ -324,9 +392,12 @@ "title": "Child1", "description": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 2, "rght": 5, - "tree_id": 2, + "tree_id": 4, "level": 1, "views": [ 1 @@ -344,9 +415,12 @@ "title": "Child2", "description": "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est.", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 10, "rght": 11, - "tree_id": 2, + "tree_id": 4, "level": 1, "views": [ 1 @@ -364,9 +438,12 @@ "title": "Child11", "description": "", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 3, "rght": 4, - "tree_id": 2, + "tree_id": 4, "level": 2, "views": [ 1 @@ -384,9 +461,12 @@ "title": "Prune Test", "description": "This is a test!", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 2, "rght": 7, - "tree_id": 3, + "tree_id": 5, "level": 0, "views": [ 1 @@ -404,9 +484,12 @@ "title": "Prune Test", "description": "This is a test!", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 6, "rght": 8, - "tree_id": 4, + "tree_id": 6, "level": 0, "views": [ 1 @@ -424,9 +507,12 @@ "title": "Prune Test", "description": "This is a test!", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 7, "rght": 9, - "tree_id": 5, + "tree_id": 7, "level": 0, "views": [ 1 @@ -444,9 +530,12 @@ "title": "Prune Test", "description": "This is a test!", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 8, "rght": 12, - "tree_id": 6, + "tree_id": 8, "level": 0, "views": [ 1 @@ -464,9 +553,12 @@ "title": "View Test", "description": "", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 1, "rght": 2, - "tree_id": 7, + "tree_id": 9, "level": 0, "views": [ 1, @@ -486,9 +578,37 @@ "title": "Other", "description": "", "catalog": 1, + "progress_total": null, + "progress_count": null, + "visibility": "private", "lft": 1, "rght": 2, - "tree_id": 2, + "tree_id": 4, + "level": 0, + "views": [ + 1, + 2, + 3 + ] + } + }, + { + "model": "projects.project", + "pk": 12, + "fields": { + "created": "2024-08-16T08:39:40.150Z", + "updated": "2024-08-16T10:46:55.669Z", + "parent": null, + "site": 1, + "title": "Internal", + "description": "", + "catalog": 1, + "progress_total": 29, + "progress_count": 1, + "visibility": "internal", + "lft": 1, + "rght": 2, + "tree_id": 3, "level": 0, "views": [ 1, @@ -563,6 +683,17 @@ "description": "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet." } }, + { + "model": "projects.snapshot", + "pk": 8, + "fields": { + "created": "2024-08-16T10:55:19.320Z", + "updated": "2024-08-16T10:55:19.320Z", + "project": 12, + "title": "Test", + "description": "" + } + }, { "model": "projects.value", "pk": 1, @@ -574,6 +705,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet", "option": null, @@ -594,6 +726,7 @@ "attribute": 4, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -614,6 +747,7 @@ "attribute": 8, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -634,6 +768,7 @@ "attribute": 11, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum", "option": 7, @@ -654,6 +789,7 @@ "attribute": 12, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -674,6 +810,7 @@ "attribute": 7, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "37", "option": null, @@ -694,6 +831,7 @@ "attribute": 9, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2018-01-01T00:00:00+01:00", "option": null, @@ -714,6 +852,7 @@ "attribute": 21, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -734,6 +873,7 @@ "attribute": 21, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua", "option": null, @@ -754,6 +894,7 @@ "attribute": 22, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -774,6 +915,7 @@ "attribute": 22, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -794,6 +936,7 @@ "attribute": 13, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "0", "option": null, @@ -814,6 +957,7 @@ "attribute": 13, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -834,6 +978,7 @@ "attribute": 13, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "1", "option": null, @@ -854,6 +999,7 @@ "attribute": 15, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -874,6 +1020,7 @@ "attribute": 15, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -894,6 +1041,7 @@ "attribute": 15, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -914,6 +1062,7 @@ "attribute": 20, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 2, @@ -934,6 +1083,7 @@ "attribute": 20, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 3, @@ -954,6 +1104,7 @@ "attribute": 20, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -974,6 +1125,7 @@ "attribute": 19, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "50", "option": null, @@ -994,6 +1146,7 @@ "attribute": 19, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "0", "option": null, @@ -1014,6 +1167,7 @@ "attribute": 19, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "100", "option": null, @@ -1034,6 +1188,7 @@ "attribute": 16, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "2017-04-02T00:00:00+02:00", "option": null, @@ -1054,6 +1209,7 @@ "attribute": 16, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2017-04-01T00:00:00+02:00", "option": null, @@ -1074,6 +1230,7 @@ "attribute": 16, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "2017-04-03T00:00:00+02:00", "option": null, @@ -1094,6 +1251,7 @@ "attribute": 14, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -1114,6 +1272,7 @@ "attribute": 49, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -1134,6 +1293,7 @@ "attribute": 50, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -1154,6 +1314,7 @@ "attribute": 47, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -1174,6 +1335,7 @@ "attribute": 48, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -1194,6 +1356,7 @@ "attribute": 48, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -1214,6 +1377,7 @@ "attribute": 47, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -1234,6 +1398,7 @@ "attribute": 14, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -1254,6 +1419,7 @@ "attribute": 49, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -1274,6 +1440,7 @@ "attribute": 50, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -1294,6 +1461,7 @@ "attribute": 74, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 5, @@ -1314,6 +1482,7 @@ "attribute": 70, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "0", "option": null, @@ -1334,6 +1503,7 @@ "attribute": 76, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 2, @@ -1354,6 +1524,7 @@ "attribute": 75, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2", "option": null, @@ -1374,6 +1545,7 @@ "attribute": 71, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2018-02-07T00:00:00+01:00", "option": null, @@ -1394,6 +1566,7 @@ "attribute": 70, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -1414,6 +1587,7 @@ "attribute": 75, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -1434,6 +1608,7 @@ "attribute": 74, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -1454,6 +1629,7 @@ "attribute": 76, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -1474,6 +1650,7 @@ "attribute": 71, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2018-01-07T00:00:00+01:00", "option": null, @@ -1494,6 +1671,7 @@ "attribute": 62, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -1514,6 +1692,7 @@ "attribute": 62, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "1", "option": null, @@ -1534,6 +1713,7 @@ "attribute": 62, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "0", "option": null, @@ -1554,6 +1734,7 @@ "attribute": 66, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -1574,6 +1755,7 @@ "attribute": 66, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -1594,6 +1776,7 @@ "attribute": 66, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -1614,6 +1797,7 @@ "attribute": 67, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "16", "option": null, @@ -1634,6 +1818,7 @@ "attribute": 68, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 2, @@ -1654,6 +1839,7 @@ "attribute": 68, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -1674,6 +1860,7 @@ "attribute": 67, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "31", "option": null, @@ -1694,6 +1881,7 @@ "attribute": 63, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2018-01-07T00:00:00+01:00", "option": null, @@ -1714,6 +1902,7 @@ "attribute": 63, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "2018-02-07T00:00:00+01:00", "option": null, @@ -1734,6 +1923,7 @@ "attribute": 78, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -1754,6 +1944,7 @@ "attribute": 78, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -1774,6 +1965,7 @@ "attribute": 62, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "0", "option": null, @@ -1794,6 +1986,7 @@ "attribute": 62, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "1", "option": null, @@ -1814,6 +2007,7 @@ "attribute": 62, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 2, "text": "0", "option": null, @@ -1834,6 +2028,7 @@ "attribute": 66, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 6, @@ -1854,6 +2049,7 @@ "attribute": 66, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 2, "text": "", "option": 4, @@ -1874,6 +2070,7 @@ "attribute": 66, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -1894,6 +2091,7 @@ "attribute": 68, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 3, @@ -1914,6 +2112,7 @@ "attribute": 67, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "86", "option": null, @@ -1934,6 +2133,7 @@ "attribute": 63, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2018-10-07T00:00:00+02:00", "option": null, @@ -1954,6 +2154,7 @@ "attribute": 63, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "2018-11-07T00:00:00+01:00", "option": null, @@ -1974,6 +2175,7 @@ "attribute": 78, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -1994,6 +2196,7 @@ "attribute": 79, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "First", "option": null, @@ -2014,6 +2217,7 @@ "attribute": 79, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Second", "option": null, @@ -2055,6 +2259,7 @@ "attribute": 82, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -2075,6 +2280,7 @@ "attribute": 82, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -2095,6 +2301,7 @@ "attribute": 81, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "test", "option": null, @@ -2115,6 +2322,7 @@ "attribute": 13, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -2135,6 +2343,7 @@ "attribute": 13, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "0", "option": null, @@ -2155,6 +2364,7 @@ "attribute": 13, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "1", "option": null, @@ -2175,6 +2385,7 @@ "attribute": 14, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -2195,6 +2406,7 @@ "attribute": 14, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -2215,6 +2427,7 @@ "attribute": 16, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2017-04-01T00:00:00+02:00", "option": null, @@ -2235,6 +2448,7 @@ "attribute": 16, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "2017-04-02T00:00:00+02:00", "option": null, @@ -2255,6 +2469,7 @@ "attribute": 16, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "2017-04-03T00:00:00+02:00", "option": null, @@ -2275,6 +2490,7 @@ "attribute": 15, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -2295,6 +2511,7 @@ "attribute": 15, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -2315,6 +2532,7 @@ "attribute": 15, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -2335,6 +2553,7 @@ "attribute": 19, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "0", "option": null, @@ -2355,6 +2574,7 @@ "attribute": 19, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "50", "option": null, @@ -2375,6 +2595,7 @@ "attribute": 19, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "100", "option": null, @@ -2395,6 +2616,7 @@ "attribute": 20, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -2415,6 +2637,7 @@ "attribute": 20, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 2, @@ -2435,6 +2658,7 @@ "attribute": 20, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 3, @@ -2455,6 +2679,7 @@ "attribute": 21, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -2475,6 +2700,7 @@ "attribute": 21, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua", "option": null, @@ -2495,6 +2721,7 @@ "attribute": 22, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -2515,6 +2742,7 @@ "attribute": 22, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -2535,6 +2763,7 @@ "attribute": 8, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -2555,6 +2784,7 @@ "attribute": 9, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2018-01-01T00:00:00+01:00", "option": null, @@ -2575,6 +2805,7 @@ "attribute": 11, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum", "option": 7, @@ -2595,6 +2826,7 @@ "attribute": 7, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "37", "option": null, @@ -2615,6 +2847,7 @@ "attribute": 12, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -2635,6 +2868,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet", "option": null, @@ -2655,6 +2889,7 @@ "attribute": 4, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -2675,6 +2910,7 @@ "attribute": 62, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -2695,6 +2931,7 @@ "attribute": 62, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "0", "option": null, @@ -2715,6 +2952,7 @@ "attribute": 62, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "1", "option": null, @@ -2735,6 +2973,7 @@ "attribute": 62, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "0", "option": null, @@ -2755,6 +2994,7 @@ "attribute": 62, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "1", "option": null, @@ -2775,6 +3015,7 @@ "attribute": 62, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 2, "text": "0", "option": null, @@ -2795,6 +3036,7 @@ "attribute": 78, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -2815,6 +3057,7 @@ "attribute": 78, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -2835,6 +3078,7 @@ "attribute": 78, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -2855,6 +3099,7 @@ "attribute": 63, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2018-01-07T00:00:00+01:00", "option": null, @@ -2875,6 +3120,7 @@ "attribute": 63, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "2018-02-07T00:00:00+01:00", "option": null, @@ -2895,6 +3141,7 @@ "attribute": 63, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2018-10-07T00:00:00+02:00", "option": null, @@ -2915,6 +3162,7 @@ "attribute": 63, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "2018-11-07T00:00:00+01:00", "option": null, @@ -2935,6 +3183,7 @@ "attribute": 66, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -2955,6 +3204,7 @@ "attribute": 66, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -2975,6 +3225,7 @@ "attribute": 66, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -2995,6 +3246,7 @@ "attribute": 66, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 6, @@ -3015,6 +3267,7 @@ "attribute": 66, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -3035,6 +3288,7 @@ "attribute": 66, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 2, "text": "", "option": 4, @@ -3055,6 +3309,7 @@ "attribute": 67, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "16", "option": null, @@ -3075,6 +3330,7 @@ "attribute": 67, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "31", "option": null, @@ -3095,6 +3351,7 @@ "attribute": 67, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "86", "option": null, @@ -3115,6 +3372,7 @@ "attribute": 68, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -3135,6 +3393,7 @@ "attribute": 68, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 2, @@ -3155,6 +3414,7 @@ "attribute": 68, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 3, @@ -3175,6 +3435,7 @@ "attribute": 47, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3195,6 +3456,7 @@ "attribute": 47, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3215,6 +3477,7 @@ "attribute": 48, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -3235,6 +3498,7 @@ "attribute": 48, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -3255,6 +3519,7 @@ "attribute": 79, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -3275,6 +3540,7 @@ "attribute": 79, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2", "option": null, @@ -3295,6 +3561,7 @@ "attribute": 70, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -3315,6 +3582,7 @@ "attribute": 70, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "0", "option": null, @@ -3335,6 +3603,7 @@ "attribute": 71, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2018-01-07T00:00:00+01:00", "option": null, @@ -3355,6 +3624,7 @@ "attribute": 71, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2018-02-07T00:00:00+01:00", "option": null, @@ -3375,6 +3645,7 @@ "attribute": 74, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -3395,6 +3666,7 @@ "attribute": 74, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 5, @@ -3415,6 +3687,7 @@ "attribute": 75, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -3435,6 +3708,7 @@ "attribute": 75, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2", "option": null, @@ -3455,6 +3729,7 @@ "attribute": 76, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -3475,6 +3750,7 @@ "attribute": 76, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 2, @@ -3495,6 +3771,7 @@ "attribute": 49, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3515,6 +3792,7 @@ "attribute": 49, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3535,6 +3813,7 @@ "attribute": 50, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -3555,6 +3834,7 @@ "attribute": 50, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -3575,6 +3855,7 @@ "attribute": 96, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -3595,6 +3876,7 @@ "attribute": 98, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": null, @@ -3615,6 +3897,7 @@ "attribute": 98, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": null, @@ -3635,6 +3918,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3655,6 +3939,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3675,6 +3960,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3695,6 +3981,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3715,6 +4002,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3735,6 +4023,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3755,6 +4044,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3775,6 +4065,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -3795,6 +4086,7 @@ "attribute": 82, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -3815,6 +4107,7 @@ "attribute": 81, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "test", "option": null, @@ -3835,6 +4128,7 @@ "attribute": 13, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -3855,6 +4149,7 @@ "attribute": 13, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "0", "option": null, @@ -3875,6 +4170,7 @@ "attribute": 13, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "1", "option": null, @@ -3895,6 +4191,7 @@ "attribute": 14, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -3915,6 +4212,7 @@ "attribute": 14, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -3935,6 +4233,7 @@ "attribute": 16, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2017-04-01T00:00:00+02:00", "option": null, @@ -3955,6 +4254,7 @@ "attribute": 16, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "2017-04-02T00:00:00+02:00", "option": null, @@ -3975,6 +4275,7 @@ "attribute": 16, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "2017-04-03T00:00:00+02:00", "option": null, @@ -3995,6 +4296,7 @@ "attribute": 98, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": null, @@ -4015,6 +4317,7 @@ "attribute": 98, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": null, @@ -4035,6 +4338,7 @@ "attribute": 15, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -4055,6 +4359,7 @@ "attribute": 15, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -4075,6 +4380,7 @@ "attribute": 15, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -4095,6 +4401,7 @@ "attribute": 19, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "0", "option": null, @@ -4115,6 +4422,7 @@ "attribute": 19, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "50", "option": null, @@ -4135,6 +4443,7 @@ "attribute": 19, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "100", "option": null, @@ -4155,6 +4464,7 @@ "attribute": 20, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -4175,6 +4485,7 @@ "attribute": 20, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 2, @@ -4195,6 +4506,7 @@ "attribute": 20, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 3, @@ -4215,6 +4527,7 @@ "attribute": 21, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -4235,6 +4548,7 @@ "attribute": 21, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua", "option": null, @@ -4255,6 +4569,7 @@ "attribute": 22, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -4275,6 +4590,7 @@ "attribute": 22, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -4295,6 +4611,7 @@ "attribute": 8, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -4315,6 +4632,7 @@ "attribute": 9, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2018-01-01T00:00:00+01:00", "option": null, @@ -4335,6 +4653,7 @@ "attribute": 96, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -4355,6 +4674,7 @@ "attribute": 11, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum", "option": 7, @@ -4375,6 +4695,7 @@ "attribute": 7, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "37", "option": null, @@ -4395,6 +4716,7 @@ "attribute": 12, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -4415,6 +4737,7 @@ "attribute": 3, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet", "option": null, @@ -4435,6 +4758,7 @@ "attribute": 4, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -4455,6 +4779,7 @@ "attribute": 62, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -4475,6 +4800,7 @@ "attribute": 62, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "0", "option": null, @@ -4495,6 +4821,7 @@ "attribute": 62, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "1", "option": null, @@ -4515,6 +4842,7 @@ "attribute": 62, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "0", "option": null, @@ -4535,6 +4863,7 @@ "attribute": 62, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "1", "option": null, @@ -4555,6 +4884,7 @@ "attribute": 62, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 2, "text": "0", "option": null, @@ -4575,6 +4905,7 @@ "attribute": 78, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -4595,6 +4926,7 @@ "attribute": 78, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -4615,6 +4947,7 @@ "attribute": 78, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -4635,6 +4968,7 @@ "attribute": 63, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2018-01-07T00:00:00+01:00", "option": null, @@ -4655,6 +4989,7 @@ "attribute": 63, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "2018-02-07T00:00:00+01:00", "option": null, @@ -4675,6 +5010,7 @@ "attribute": 63, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2018-10-07T00:00:00+02:00", "option": null, @@ -4695,6 +5031,7 @@ "attribute": 63, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "2018-11-07T00:00:00+01:00", "option": null, @@ -4715,6 +5052,7 @@ "attribute": 66, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -4735,6 +5073,7 @@ "attribute": 66, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -4755,6 +5094,7 @@ "attribute": 66, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 2, "text": "", "option": 6, @@ -4775,6 +5115,7 @@ "attribute": 66, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 6, @@ -4795,6 +5136,7 @@ "attribute": 66, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "", "option": 5, @@ -4815,6 +5157,7 @@ "attribute": 66, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 2, "text": "", "option": 4, @@ -4835,6 +5178,7 @@ "attribute": 67, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "16", "option": null, @@ -4855,6 +5199,7 @@ "attribute": 67, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "31", "option": null, @@ -4875,6 +5220,7 @@ "attribute": 67, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "86", "option": null, @@ -4895,6 +5241,7 @@ "attribute": 68, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -4915,6 +5262,7 @@ "attribute": 68, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "", "option": 2, @@ -4935,6 +5283,7 @@ "attribute": 68, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 3, @@ -4955,6 +5304,7 @@ "attribute": 47, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -4975,6 +5325,7 @@ "attribute": 47, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -4995,6 +5346,7 @@ "attribute": 48, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -5015,6 +5367,7 @@ "attribute": 48, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -5035,6 +5388,7 @@ "attribute": 79, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "First", "option": null, @@ -5055,6 +5409,7 @@ "attribute": 79, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Second", "option": null, @@ -5075,6 +5430,7 @@ "attribute": 70, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -5095,6 +5451,7 @@ "attribute": 70, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "0", "option": null, @@ -5115,6 +5472,7 @@ "attribute": 71, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "2018-01-07T00:00:00+01:00", "option": null, @@ -5135,6 +5493,7 @@ "attribute": 71, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2018-02-07T00:00:00+01:00", "option": null, @@ -5155,6 +5514,7 @@ "attribute": 74, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 4, @@ -5175,6 +5535,7 @@ "attribute": 74, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 5, @@ -5195,6 +5556,7 @@ "attribute": 75, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "1", "option": null, @@ -5215,6 +5577,7 @@ "attribute": 75, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "2", "option": null, @@ -5235,6 +5598,7 @@ "attribute": 76, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -5255,6 +5619,7 @@ "attribute": 76, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 2, @@ -5275,6 +5640,7 @@ "attribute": 49, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -5295,6 +5661,7 @@ "attribute": 49, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr", "option": null, @@ -5315,6 +5682,7 @@ "attribute": 50, "set_prefix": "", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -5335,6 +5703,7 @@ "attribute": 50, "set_prefix": "", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.", "option": null, @@ -5355,6 +5724,7 @@ "attribute": 109, "set_prefix": "0", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "b1", "option": null, @@ -5375,6 +5745,7 @@ "attribute": 108, "set_prefix": "0", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "a0", "option": null, @@ -5395,6 +5766,7 @@ "attribute": 110, "set_prefix": "0", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "c01", "option": null, @@ -5415,6 +5787,7 @@ "attribute": 108, "set_prefix": "0", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "a1", "option": null, @@ -5435,6 +5808,7 @@ "attribute": 110, "set_prefix": "0", "set_index": 0, + "set_collection": null, "collection_index": 1, "text": "c02", "option": null, @@ -5455,6 +5829,7 @@ "attribute": 109, "set_prefix": "0", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "b1", "option": null, @@ -5475,6 +5850,7 @@ "attribute": 110, "set_prefix": "0", "set_index": 1, + "set_collection": null, "collection_index": 1, "text": "c11", "option": null, @@ -5495,6 +5871,7 @@ "attribute": 110, "set_prefix": "0", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "c10", "option": null, @@ -5515,6 +5892,7 @@ "attribute": 108, "set_prefix": "1", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -5535,6 +5913,7 @@ "attribute": 108, "set_prefix": "1", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -5555,6 +5934,7 @@ "attribute": 112, "set_prefix": "1|2", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": 1, @@ -5575,6 +5955,7 @@ "attribute": 108, "set_prefix": "1", "set_index": 2, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -5595,6 +5976,7 @@ "attribute": 109, "set_prefix": "1", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -5615,6 +5997,7 @@ "attribute": 109, "set_prefix": "1", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -5635,6 +6018,7 @@ "attribute": 109, "set_prefix": "1", "set_index": 2, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -5655,6 +6039,7 @@ "attribute": 110, "set_prefix": "1", "set_index": 0, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -5675,6 +6060,7 @@ "attribute": 110, "set_prefix": "1", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -5695,6 +6081,7 @@ "attribute": 110, "set_prefix": "1", "set_index": 2, + "set_collection": null, "collection_index": 0, "text": "", "option": null, @@ -5715,6 +6102,7 @@ "attribute": 112, "set_prefix": "1|2", "set_index": 1, + "set_collection": null, "collection_index": 0, "text": "", "option": 2, @@ -5735,6 +6123,7 @@ "attribute": 112, "set_prefix": "1|2", "set_index": 2, + "set_collection": null, "collection_index": 0, "text": "", "option": 3, @@ -5745,27 +6134,45 @@ } }, { - "model": "projects.invite", - "pk": 1, + "model": "projects.value", + "pk": 456, "fields": { - "project": 1, - "user": 11, - "email": "other@example.com", - "role": "author", - "token": "10159fadb66e97365a36", - "timestamp": "2023-07-10T14:06:55.112Z" + "created": "2024-08-16T10:46:55.125Z", + "updated": "2024-08-16T10:46:55.125Z", + "project": 12, + "snapshot": null, + "attribute": 3, + "set_prefix": "", + "set_index": 0, + "set_collection": false, + "collection_index": 0, + "text": "TEST", + "option": null, + "file": "", + "value_type": "text", + "unit": "", + "external_id": "" } }, { - "model": "projects.invite", - "pk": 2, + "model": "projects.value", + "pk": 457, "fields": { - "project": 11, - "user": 4, - "email": "user@example.com", - "role": "author", - "token": "10159fadb66e97365a36", - "timestamp": "2023-07-10T14:06:55.112Z" + "created": "2024-08-16T10:46:55.125Z", + "updated": "2024-08-16T10:55:19.333Z", + "project": 12, + "snapshot": 8, + "attribute": 3, + "set_prefix": "", + "set_index": 0, + "set_collection": false, + "collection_index": 0, + "text": "TEST", + "option": null, + "file": "", + "value_type": "text", + "unit": "", + "external_id": "" } } ] From 493f4b52dc5286b2ea99ec85fcc7c8c80fa32c65 Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Sun, 25 Aug 2024 18:08:57 +0200 Subject: [PATCH 05/10] Fix migration and test (after rebase) --- .../{0061_project_visibility.py => 0062_project_visibility.py} | 2 +- rdmo/projects/tests/test_viewset_project.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename rdmo/projects/migrations/{0061_project_visibility.py => 0062_project_visibility.py} (90%) diff --git a/rdmo/projects/migrations/0061_project_visibility.py b/rdmo/projects/migrations/0062_project_visibility.py similarity index 90% rename from rdmo/projects/migrations/0061_project_visibility.py rename to rdmo/projects/migrations/0062_project_visibility.py index a956d40027..a19a585eda 100644 --- a/rdmo/projects/migrations/0061_project_visibility.py +++ b/rdmo/projects/migrations/0062_project_visibility.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('projects', '0060_alter_issue_options'), + ('projects', '0061_alter_value_value_type'), ] operations = [ diff --git a/rdmo/projects/tests/test_viewset_project.py b/rdmo/projects/tests/test_viewset_project.py index d23a268182..cbcc66fe6e 100644 --- a/rdmo/projects/tests/test_viewset_project.py +++ b/rdmo/projects/tests/test_viewset_project.py @@ -82,7 +82,7 @@ def test_list(db, client, username, password): assert isinstance(response_data, dict) if username == 'user': - assert sorted([item['id'] for item in response.json()]) == projects_internal + assert sorted([item['id'] for item in response.json().get('results')]) == projects_internal else: values_list = Project.objects.filter(id__in=view_project_permission_map.get(username, [])) \ .values_list('id', flat=True) From 6ebb805cfc0568dc0828b981f9dfbc51b6aa129f Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Fri, 6 Dec 2024 10:44:00 +0100 Subject: [PATCH 06/10] Add ProjectUserFilterBackend to take internal projects into account when filtering by user --- rdmo/projects/filters.py | 17 +++++++++++++++++ rdmo/projects/managers.py | 8 +++++++- rdmo/projects/tests/test_viewset_project.py | 15 +++++++++++++++ rdmo/projects/viewsets.py | 5 +++-- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/rdmo/projects/filters.py b/rdmo/projects/filters.py index 211ac992ca..1962d71d67 100644 --- a/rdmo/projects/filters.py +++ b/rdmo/projects/filters.py @@ -1,3 +1,4 @@ +from django.contrib.auth.models import User from django.db.models import F, OuterRef, Q, Subquery from django.db.models.functions import Concat from django.utils.dateparse import parse_datetime @@ -18,6 +19,22 @@ class Meta: fields = ('title', 'catalog') +class ProjectUserFilterBackend(BaseFilterBackend): + + def filter_queryset(self, request, queryset, view): + if view.detail: + return queryset + + user_id = request.GET.get('user') + user_username = request.GET.get('username') + if user_id or user_username: + user = User.objects.filter(Q(id=user_id) | Q(username=user_username)).first() + if user: + queryset = queryset.filter_visibility(user) + + return queryset + + class ProjectSearchFilterBackend(SearchFilter): def filter_queryset(self, request, queryset, view): diff --git a/rdmo/projects/managers.py b/rdmo/projects/managers.py index 988303ed92..289bf08544 100644 --- a/rdmo/projects/managers.py +++ b/rdmo/projects/managers.py @@ -23,13 +23,16 @@ def filter_user(self, user): elif is_site_manager(user): return self.filter_current_site() else: - queryset = self.filter(Q(user=user) | models.Q(visibility=VISIBILITY_INTERNAL)) + queryset = self.filter_visibility(user) for instance in queryset: queryset |= instance.get_descendants() return queryset.distinct() else: return self.none() + def filter_visibility(self, user): + return self.filter(Q(user=user) | models.Q(visibility=VISIBILITY_INTERNAL)) + class MembershipQuerySet(models.QuerySet): @@ -159,6 +162,9 @@ def get_queryset(self): def filter_user(self, user): return self.get_queryset().filter_user(user) + def filter_visibility(self, user): + return self.get_queryset().filter_visibility(user) + class MembershipManager(CurrentSiteManagerMixin, models.Manager): diff --git a/rdmo/projects/tests/test_viewset_project.py b/rdmo/projects/tests/test_viewset_project.py index cbcc66fe6e..779132d21d 100644 --- a/rdmo/projects/tests/test_viewset_project.py +++ b/rdmo/projects/tests/test_viewset_project.py @@ -92,6 +92,21 @@ def test_list(db, client, username, password): assert response.status_code == 401 +def test_list_user(db, client): + client.login(username='admin', password='admin') + + url = reverse(urlnames['list']) + '?user=1' + response = client.get(url) + response_data = response.json() + + assert response.status_code == 200 + assert isinstance(response_data, dict) + + values_list = Project.objects.filter(id__in=projects_internal).values_list('id', flat=True) + assert response_data['count'] == len(values_list) + assert [item['id'] for item in response_data['results']] == list(values_list[:page_size]) + + @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) def test_detail(db, client, username, password, project_id): diff --git a/rdmo/projects/viewsets.py b/rdmo/projects/viewsets.py index 3f8f51d7c3..6215be6912 100644 --- a/rdmo/projects/viewsets.py +++ b/rdmo/projects/viewsets.py @@ -31,6 +31,7 @@ ProjectDateFilterBackend, ProjectOrderingFilter, ProjectSearchFilterBackend, + ProjectUserFilterBackend, SnapshotFilterBackend, ValueFilterBackend, ) @@ -85,14 +86,14 @@ class ProjectViewSet(ModelViewSet): filter_backends = ( DjangoFilterBackend, + ProjectUserFilterBackend, ProjectDateFilterBackend, ProjectOrderingFilter, ProjectSearchFilterBackend, ) filterset_fields = ( 'title', - 'user', - 'user__username', + # user is part of ProjectUserFilterBackend 'catalog', 'catalog__uri' ) From d7e251e249759571a8c1b109babedecbffd8dcf1 Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Fri, 6 Dec 2024 16:59:43 +0100 Subject: [PATCH 07/10] Refactor visibility to use the Visbility model and restrict to site_admins --- rdmo/core/templates/core/bootstrap_form.html | 3 + .../templates/core/bootstrap_form_field.html | 7 + rdmo/core/templatetags/core_tags.py | 3 + rdmo/projects/admin.py | 21 ++ rdmo/projects/constants.py | 7 - rdmo/projects/forms.py | 41 +++- rdmo/projects/managers.py | 8 +- .../migrations/0062_project_visibility.py | 18 -- rdmo/projects/migrations/0062_visibility.py | 32 +++ rdmo/projects/models/__init__.py | 1 + rdmo/projects/models/project.py | 21 -- rdmo/projects/models/visibility.py | 66 ++++++ rdmo/projects/permissions.py | 22 ++ rdmo/projects/rules.py | 30 ++- rdmo/projects/serializers/v1/__init__.py | 24 ++- .../projects/project_detail_header.html | 3 +- .../project_detail_header_visibility.html | 7 +- .../projects/project_detail_sidebar.html | 14 +- .../templates/projects/project_form.html | 17 ++ rdmo/projects/tests/test_view_project.py | 62 +----- .../test_view_project_update_visibility.py | 148 ++++++++++++++ .../tests/test_view_project_visibility.py | 188 ++++++++++++++++++ rdmo/projects/tests/test_viewset_project.py | 2 +- .../tests/test_viewset_project_visibility.py | 139 +++++++++++++ rdmo/projects/views/project_update.py | 2 +- rdmo/projects/viewsets.py | 36 +++- testing/fixtures/projects.json | 29 +-- 27 files changed, 807 insertions(+), 144 deletions(-) delete mode 100644 rdmo/projects/migrations/0062_project_visibility.py create mode 100644 rdmo/projects/migrations/0062_visibility.py create mode 100644 rdmo/projects/models/visibility.py create mode 100644 rdmo/projects/tests/test_view_project_update_visibility.py create mode 100644 rdmo/projects/tests/test_view_project_visibility.py create mode 100644 rdmo/projects/tests/test_viewset_project_visibility.py diff --git a/rdmo/core/templates/core/bootstrap_form.html b/rdmo/core/templates/core/bootstrap_form.html index 6444d2ebf7..e15f020694 100644 --- a/rdmo/core/templates/core/bootstrap_form.html +++ b/rdmo/core/templates/core/bootstrap_form.html @@ -7,5 +7,8 @@ {% include 'core/bootstrap_form_fields.html' %} + {% if delete %} + + {% endif %} diff --git a/rdmo/core/templates/core/bootstrap_form_field.html b/rdmo/core/templates/core/bootstrap_form_field.html index f34e8b9454..2104e387d4 100644 --- a/rdmo/core/templates/core/bootstrap_form_field.html +++ b/rdmo/core/templates/core/bootstrap_form_field.html @@ -1,3 +1,4 @@ +{% load i18n %} {% load widget_tweaks %} {% load core_tags %} @@ -61,6 +62,12 @@ {% render_field field class="form-control" %} + {% if type == 'selectmultiple' %} +

    + {% trans 'Hold down "Control", or "Command" on a Mac, to select more than one.' %} +

    + {% endif %} + {% endif %} {% endwith %} diff --git a/rdmo/core/templatetags/core_tags.py b/rdmo/core/templatetags/core_tags.py index 52a094b9ee..548ded6d81 100644 --- a/rdmo/core/templatetags/core_tags.py +++ b/rdmo/core/templatetags/core_tags.py @@ -114,6 +114,9 @@ def bootstrap_form(context, **kwargs): if 'submit' in kwargs: form_context['submit'] = kwargs['submit'] + if 'delete' in kwargs: + form_context['delete'] = kwargs['delete'] + return render_to_string('core/bootstrap_form.html', form_context, request=context.request) diff --git a/rdmo/projects/admin.py b/rdmo/projects/admin.py index bf8956f28d..07c72480ed 100644 --- a/rdmo/projects/admin.py +++ b/rdmo/projects/admin.py @@ -3,6 +3,7 @@ from django.db.models import Prefetch from django.urls import reverse from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ from .models import ( Continuation, @@ -15,6 +16,7 @@ Project, Snapshot, Value, + Visibility, ) from .validators import ProjectParentValidator @@ -71,6 +73,25 @@ class ContinuationAdmin(admin.ModelAdmin): list_display = ('project', 'user', 'page') +@admin.register(Visibility) +class VisibilityAdmin(admin.ModelAdmin): + search_fields = ('project__title', 'sites', 'groups') + list_display = ('project', 'sites_list_display', 'groups_list_display') + filter_horizontal = ('sites', 'groups') + + @admin.display(description=_('Sites')) + def sites_list_display(self, obj): + return _('all Sites') if obj.sites.count() == 0 else ', '.join([ + site.domain for site in obj.sites.all() + ]) + + @admin.display(description=_('Groups')) + def groups_list_display(self, obj): + return _('all Groups') if obj.groups.count() == 0 else ', '.join([ + group.name for group in obj.groups.all() + ]) + + @admin.register(Integration) class IntegrationAdmin(admin.ModelAdmin): search_fields = ('project__title', 'provider_key') diff --git a/rdmo/projects/constants.py b/rdmo/projects/constants.py index acec835398..77add3ea45 100644 --- a/rdmo/projects/constants.py +++ b/rdmo/projects/constants.py @@ -10,10 +10,3 @@ (ROLE_AUTHOR, _('Author')), (ROLE_GUEST, _('Guest')), ) - -VISIBILITY_PRIVATE = 'private' -VISIBILITY_INTERNAL = 'internal' -VISIBILITY_CHOICES = ( - (VISIBILITY_PRIVATE, _('Private')), - (VISIBILITY_INTERNAL, _('Internal')) -) diff --git a/rdmo/projects/forms.py b/rdmo/projects/forms.py index c45008d9e3..3d2d1a220a 100644 --- a/rdmo/projects/forms.py +++ b/rdmo/projects/forms.py @@ -12,7 +12,7 @@ from rdmo.core.utils import markdown2html from .constants import ROLE_CHOICES -from .models import Integration, IntegrationOption, Invite, Membership, Project, Snapshot +from .models import Integration, IntegrationOption, Invite, Membership, Project, Snapshot, Visibility from .validators import ProjectParentValidator @@ -80,8 +80,6 @@ class Meta: fields = ['title', 'description', 'catalog'] if settings.NESTED_PROJECTS: fields += ['parent'] - if settings.PROJECT_VISIBILITY: - fields += ['visibility'] field_classes = { 'catalog': CatalogChoiceField @@ -104,9 +102,42 @@ class ProjectUpdateVisibilityForm(forms.ModelForm): use_required_attribute = False + def __init__(self, *args, **kwargs): + self.project = kwargs.pop('instance') + try: + instance = self.project.visibility + except Visibility.DoesNotExist: + instance = None + + super().__init__(*args, instance=instance, **kwargs) + + # remove the sites or group sets if they are not needed, doing this in Meta would break tests + if not settings.MULTISITE: + self.fields.pop('sites') + if not settings.GROUPS: + self.fields.pop('groups') + class Meta: - model = Project - fields = ('visibility', ) + model = Visibility + fields = ('sites', 'groups') + + def save(self, *args, **kwargs): + if 'cancel' in self.data: + pass + elif 'delete' in self.data: + self.instance.delete() + else: + visibility, created = Visibility.objects.update_or_create(project=self.project) + + sites = self.cleaned_data.get('sites') + if sites is not None: + visibility.sites.set(sites) + + groups = self.cleaned_data.get('groups') + if groups is not None: + visibility.groups.set(groups) + + return self.project class ProjectUpdateCatalogForm(forms.ModelForm): diff --git a/rdmo/projects/managers.py b/rdmo/projects/managers.py index 289bf08544..a6d49e4ff6 100644 --- a/rdmo/projects/managers.py +++ b/rdmo/projects/managers.py @@ -8,8 +8,6 @@ from rdmo.accounts.utils import is_site_manager from rdmo.core.managers import CurrentSiteManagerMixin -from .constants import VISIBILITY_INTERNAL - class ProjectQuerySet(TreeQuerySet): @@ -31,7 +29,11 @@ def filter_user(self, user): return self.none() def filter_visibility(self, user): - return self.filter(Q(user=user) | models.Q(visibility=VISIBILITY_INTERNAL)) + groups = user.groups.all() + sites_filter = Q(visibility__sites=None) | Q(visibility__sites=settings.SITE_ID) + groups_filter = Q(visibility__groups=None) | Q(visibility__groups__in=groups) + visibility_filter = Q(visibility__isnull=False) & sites_filter & groups_filter + return self.filter(Q(user=user) | visibility_filter) class MembershipQuerySet(models.QuerySet): diff --git a/rdmo/projects/migrations/0062_project_visibility.py b/rdmo/projects/migrations/0062_project_visibility.py deleted file mode 100644 index a19a585eda..0000000000 --- a/rdmo/projects/migrations/0062_project_visibility.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.8 on 2024-08-15 15:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0061_alter_value_value_type'), - ] - - operations = [ - migrations.AddField( - model_name='project', - name='visibility', - field=models.CharField(choices=[('private', 'Private'), ('internal', 'Internal')], default='private', help_text='The visibility for this project.', max_length=8, verbose_name='visibility'), - ), - ] diff --git a/rdmo/projects/migrations/0062_visibility.py b/rdmo/projects/migrations/0062_visibility.py new file mode 100644 index 0000000000..e418f334f2 --- /dev/null +++ b/rdmo/projects/migrations/0062_visibility.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.16 on 2024-12-06 10:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('sites', '0002_alter_domain_unique'), + ('projects', '0061_alter_value_value_type'), + ] + + operations = [ + migrations.CreateModel( + name='Visibility', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(editable=False, verbose_name='created')), + ('updated', models.DateTimeField(editable=False, verbose_name='updated')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups for which the project is visible.', to='auth.group', verbose_name='Group')), + ('project', models.OneToOneField(help_text='The project for this visibility.', on_delete=django.db.models.deletion.CASCADE, to='projects.project', verbose_name='Project')), + ('sites', models.ManyToManyField(blank=True, help_text='The sites for which the project is visible (in a multi site setup).', to='sites.site', verbose_name='Sites')), + ], + options={ + 'verbose_name': 'Visibility', + 'verbose_name_plural': 'Visibilities', + 'ordering': ('project',), + }, + ), + ] diff --git a/rdmo/projects/models/__init__.py b/rdmo/projects/models/__init__.py index 35dcab847f..16af648546 100644 --- a/rdmo/projects/models/__init__.py +++ b/rdmo/projects/models/__init__.py @@ -6,3 +6,4 @@ from .project import Project from .snapshot import Snapshot from .value import Value +from .visibility import Visibility diff --git a/rdmo/projects/models/project.py b/rdmo/projects/models/project.py index 4276e49b28..b992c0dc9d 100644 --- a/rdmo/projects/models/project.py +++ b/rdmo/projects/models/project.py @@ -14,7 +14,6 @@ from rdmo.tasks.models import Task from rdmo.views.models import View -from ..constants import VISIBILITY_CHOICES, VISIBILITY_INTERNAL, VISIBILITY_PRIVATE from ..managers import ProjectManager @@ -73,11 +72,6 @@ class Project(MPTTModel, Model): verbose_name=_('Progress count'), help_text=_('The number of values for the progress bar.') ) - visibility = models.CharField( - max_length=8, choices=VISIBILITY_CHOICES, default=VISIBILITY_PRIVATE, - verbose_name=_('visibility'), - help_text=_('The visibility for this project.') - ) class Meta: ordering = ('tree_id', 'level', 'title') @@ -134,21 +128,6 @@ def file_size(self): queryset = self.values.filter(snapshot=None).exclude(models.Q(file='') | models.Q(file=None)) return sum([value.file.size for value in queryset]) - @property - def is_private(self): - return self.visibility == VISIBILITY_PRIVATE - - @property - def is_internal(self): - return self.visibility == VISIBILITY_INTERNAL - - @property - def get_visibility_help(self): - if self.is_private: - return _('Project access must be granted explicitly to each user.') - elif self.is_internal: - return _('The project can be accessed by any logged in user.') - def get_members(self, role): try: # membership_list is created by the Prefetch call in the viewset diff --git a/rdmo/projects/models/visibility.py b/rdmo/projects/models/visibility.py new file mode 100644 index 0000000000..61e4e3d2f4 --- /dev/null +++ b/rdmo/projects/models/visibility.py @@ -0,0 +1,66 @@ +from django.conf import settings +from django.contrib.auth.models import Group +from django.contrib.sites.models import Site +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ngettext_lazy + +from rdmo.core.models import Model + + +class Visibility(Model): + + project = models.OneToOneField( + 'Project', on_delete=models.CASCADE, + verbose_name=_('Project'), + help_text=_('The project for this visibility.') + ) + sites = models.ManyToManyField( + Site, blank=True, + verbose_name=_('Sites'), + help_text=_('The sites for which the project is visible (in a multi site setup).') + ) + groups = models.ManyToManyField( + Group, blank=True, + verbose_name=_('Group'), + help_text=_('The groups for which the project is visible.') + ) + + class Meta: + ordering = ('project', ) + verbose_name = _('Visibility') + verbose_name_plural = _('Visibilities') + + def __str__(self): + return str(self.project) + + def is_visible(self, user): + return ( + not self.sites.exists() or self.sites.filter(id=settings.SITE_ID).exists() + ) and ( + not self.groups.exists() or self.groups.filter(id__in=[group.id for group in user.groups.all()]).exists() + ) + + def get_help_display(self): + sites = self.sites.values_list('domain', flat=True) + groups = self.groups.values_list('name', flat=True) + + if sites and groups: + return ngettext_lazy( + 'This project can be accessed by all users on %s or in the group %s.', + 'This project can be accessed by all users on %s or in the groups %s.', + len(groups) + ) % ( + ', '.join(sites), + ', '.join(groups) + ) + elif sites: + return _('This project can be accessed by all users on %s.') % ', '.join(sites) + elif groups: + return ngettext_lazy( + 'This project can be accessed by all users in the group %s.', + 'This project can be accessed by all users in the groups %s.', + len(groups) + ) % ', '.join(groups) + else: + return _('This project can be accessed by all users.') diff --git a/rdmo/projects/permissions.py b/rdmo/projects/permissions.py index 9fa7be5ffb..da693c8d0d 100644 --- a/rdmo/projects/permissions.py +++ b/rdmo/projects/permissions.py @@ -90,3 +90,25 @@ def get_required_object_permissions(self, method, model_cls): return ('projects.change_project_progress_object', ) else: return ('projects.view_project_object', ) + + +class HasProjectVisibilityModelPermission(HasModelPermission): + + def get_required_permissions(self, method, model_cls): + if method == 'POST': + return ('projects.change_visibility', ) + elif method == 'DELETE': + return ('projects.delete_visibility', ) + else: + return ('projects.view_visibility', ) + + +class HasProjectVisibilityObjectPermission(HasProjectPermission): + + def get_required_object_permissions(self, method, model_cls): + if method == 'POST': + return ('projects.change_visibility_object', ) + elif method == 'DELETE': + return ('projects.delete_visibility_object', ) + else: + return ('projects.view_visibility_object', ) diff --git a/rdmo/projects/rules.py b/rdmo/projects/rules.py index 2bb3c968ca..70e5f44326 100644 --- a/rdmo/projects/rules.py +++ b/rdmo/projects/rules.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib.sites.shortcuts import get_current_site +from django.core.exceptions import ObjectDoesNotExist import rules from rules.predicates import is_superuser @@ -47,8 +48,14 @@ def is_project_guest(user, project): @rules.predicate -def is_internal_project(user, project): - return user.is_authenticated and project.is_internal +def is_visible(user, project): + if user.is_authenticated: + try: + return project.visibility.is_visible(user) + except ObjectDoesNotExist: + return False + else: + return False @rules.predicate @@ -72,7 +79,7 @@ def is_site_manager_for_current_site(user, request): rules.add_rule('projects.can_view_all_projects', is_site_manager_for_current_site | is_superuser) rules.add_perm('projects.add_project', can_add_project) -rules.add_perm('projects.view_project_object', is_project_member | is_internal_project | is_site_manager) +rules.add_perm('projects.view_project_object', is_project_member | is_visible | is_site_manager) rules.add_perm('projects.change_project_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.change_project_progress_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) # noqa: E501 rules.add_perm('projects.delete_project_object', is_project_owner | is_site_manager) @@ -80,7 +87,12 @@ def is_site_manager_for_current_site(user, request): rules.add_perm('projects.export_project_object', is_project_owner | is_project_manager | is_site_manager) rules.add_perm('projects.import_project_object', is_project_owner | is_project_manager | is_site_manager) -rules.add_perm('projects.view_membership_object', is_project_member | is_internal_project | is_site_manager) +rules.add_perm('projects.view_visibility_object', is_site_manager) +rules.add_perm('projects.add_visibility_object', is_site_manager) +rules.add_perm('projects.change_visibility_object', is_site_manager) +rules.add_perm('projects.delete_visibility_object', is_site_manager) + +rules.add_perm('projects.view_membership_object', is_project_member | is_visible | is_site_manager) rules.add_perm('projects.add_membership_object', is_project_owner | is_site_manager) rules.add_perm('projects.change_membership_object', is_project_owner | is_site_manager) rules.add_perm('projects.delete_membership_object', is_project_owner | is_site_manager) @@ -90,28 +102,28 @@ def is_site_manager_for_current_site(user, request): rules.add_perm('projects.change_invite_object', is_project_owner | is_site_manager) rules.add_perm('projects.delete_invite_object', is_project_owner | is_site_manager) -rules.add_perm('projects.view_integration_object', is_project_member | is_internal_project | is_site_manager) +rules.add_perm('projects.view_integration_object', is_project_member | is_visible | is_site_manager) rules.add_perm('projects.add_integration_object', is_project_owner | is_project_manager | is_site_manager) rules.add_perm('projects.change_integration_object', is_project_owner | is_project_manager | is_site_manager) rules.add_perm('projects.delete_integration_object', is_project_owner | is_project_manager | is_site_manager) -rules.add_perm('projects.view_issue_object', is_project_member | is_internal_project | is_site_manager) +rules.add_perm('projects.view_issue_object', is_project_member | is_visible | is_site_manager) rules.add_perm('projects.add_issue_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.change_issue_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) # noqa: E501 rules.add_perm('projects.delete_issue_object', is_project_manager | is_project_owner | is_site_manager) -rules.add_perm('projects.view_snapshot_object', is_project_member | is_internal_project | is_site_manager) +rules.add_perm('projects.view_snapshot_object', is_project_member | is_visible | is_site_manager) rules.add_perm('projects.add_snapshot_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.change_snapshot_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.rollback_snapshot_object', is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.export_snapshot_object', is_project_owner | is_project_manager | is_site_manager) -rules.add_perm('projects.view_value_object', is_project_member | is_internal_project | is_site_manager) +rules.add_perm('projects.view_value_object', is_project_member | is_visible | is_site_manager) rules.add_perm('projects.add_value_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) rules.add_perm('projects.change_value_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) # noqa: E501 rules.add_perm('projects.delete_value_object', is_project_author | is_project_manager | is_project_owner | is_site_manager) # noqa: E501 -rules.add_perm('projects.view_page_object', is_project_member | is_internal_project | is_site_manager) +rules.add_perm('projects.view_page_object', is_project_member | is_visible | is_site_manager) # TODO: use one of the permissions above rules.add_perm('projects.is_project_owner', is_project_owner) diff --git a/rdmo/projects/serializers/v1/__init__.py b/rdmo/projects/serializers/v1/__init__.py index fe72127295..8e61748193 100644 --- a/rdmo/projects/serializers/v1/__init__.py +++ b/rdmo/projects/serializers/v1/__init__.py @@ -8,7 +8,18 @@ from rdmo.questions.models import Catalog from rdmo.services.validators import ProviderValidator -from ...models import Integration, IntegrationOption, Invite, Issue, IssueResource, Membership, Project, Snapshot, Value +from ...models import ( + Integration, + IntegrationOption, + Invite, + Issue, + IssueResource, + Membership, + Project, + Snapshot, + Value, + Visibility, +) from ...validators import ProjectParentValidator, ValueConflictValidator, ValueQuotaValidator, ValueTypeValidator @@ -91,6 +102,17 @@ class Meta: read_only_fields = ProjectSerializer.Meta.read_only_fields +class ProjectVisibilitySerializer(serializers.ModelSerializer): + + class Meta: + model = Visibility + fields = ( + 'project', + 'sites', + 'groups' + ) + + class ProjectMembershipSerializer(serializers.ModelSerializer): class Meta: diff --git a/rdmo/projects/templates/projects/project_detail_header.html b/rdmo/projects/templates/projects/project_detail_header.html index c93e2f6bfe..c6ca098c90 100644 --- a/rdmo/projects/templates/projects/project_detail_header.html +++ b/rdmo/projects/templates/projects/project_detail_header.html @@ -4,6 +4,7 @@ {% load accounts_tags %} {% has_perm 'projects.change_project_object' request.user project as can_change_project %} +{% has_perm 'projects.change_visibility_object' request.user project as can_change_visibility %}

    {{ project.title }}

    @@ -26,7 +27,7 @@

    {{ project.title }}

    {% include 'projects/project_detail_header_catalog.html' %} - {% if settings.PROJECT_VISIBILITY %} + {% if settings.PROJECT_VISIBILITY and project.visibility %} {% trans 'Visibility' %} diff --git a/rdmo/projects/templates/projects/project_detail_header_visibility.html b/rdmo/projects/templates/projects/project_detail_header_visibility.html index 15e20eeda4..4fa4952801 100644 --- a/rdmo/projects/templates/projects/project_detail_header_visibility.html +++ b/rdmo/projects/templates/projects/project_detail_header_visibility.html @@ -1,7 +1,7 @@ {% load i18n %} {% load core_tags %} -{% if can_change_project %} +{% if can_change_visibility %}

    @@ -9,5 +9,6 @@

    {% endif %} -{{ project.get_visibility_display }} -{{ project.get_visibility_help }} + +{{ project.visibility.get_help_display }} + diff --git a/rdmo/projects/templates/projects/project_detail_sidebar.html b/rdmo/projects/templates/projects/project_detail_sidebar.html index 19666138d9..67187f87b8 100644 --- a/rdmo/projects/templates/projects/project_detail_sidebar.html +++ b/rdmo/projects/templates/projects/project_detail_sidebar.html @@ -48,11 +48,6 @@

    {% trans 'Options' %}

  • {% trans 'Update project information' %}
  • - {% if settings.PROJECT_VISIBILITY %} -
  • - {% trans 'Update project visibility' %} -
  • - {% endif %}
  • {% trans 'Update project catalog' %}
  • @@ -82,6 +77,15 @@

    {% trans 'Options' %}

    {% endif %} +{% has_perm 'projects.change_visibility_object' request.user project as can_change_visibility %} +{% if settings.PROJECT_VISIBILITY and can_change_visibility %} + +{% endif %} + {% has_perm 'projects.add_membership_object' request.user project as can_add_membership %} {% if can_add_membership %}
      diff --git a/rdmo/projects/templates/projects/project_form.html b/rdmo/projects/templates/projects/project_form.html index 9fbd90f914..74a6c72a49 100644 --- a/rdmo/projects/templates/projects/project_form.html +++ b/rdmo/projects/templates/projects/project_form.html @@ -31,6 +31,23 @@

      {% trans 'Update project tasks' %}

      {% trans 'Update project views' %}

      {% bootstrap_form submit=_('Save views') %} + {% elif request.resolver_match.url_name == 'project_update_visibility' %} + +

      {% trans 'Update project visibility' %}

      + +

      + {% blocktrans trimmed %} + Projects can be made visible to all users, for example to be used as a template. + When a project is made visible, users can access it as if they were in the guest role. + {% endblocktrans %} +

      + + {% if object.visibility %} + {% bootstrap_form submit=_('Save visibility') delete=_('Remove visibility') %} + {% else %} + {% bootstrap_form submit=_('Make visible') %} + {% endif %} + {% else %}

      {% trans 'Update project information' %}

      diff --git a/rdmo/projects/tests/test_view_project.py b/rdmo/projects/tests/test_view_project.py index e51768bc7d..8f22d0eb19 100644 --- a/rdmo/projects/tests/test_view_project.py +++ b/rdmo/projects/tests/test_view_project.py @@ -57,7 +57,6 @@ } projects = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] -projects_internal = [12] export_formats = ('rtf', 'odt', 'docx', 'html', 'markdown', 'tex', 'pdf') @@ -213,8 +212,7 @@ def test_project_create_post(db, client, username, password): data = { 'title': 'A new project', 'description': 'Some description', - 'catalog': catalog_id, - 'visibility': 'private' + 'catalog': catalog_id } response = client.post(url, data) @@ -240,8 +238,7 @@ def test_project_create_post_restricted(db, client, settings): data = { 'title': 'A new project', 'description': 'Some description', - 'catalog': catalog_id, - 'visibility': 'private' + 'catalog': catalog_id } response = client.post(url, data) @@ -257,8 +254,7 @@ def test_project_create_post_forbidden(db, client, settings): data = { 'title': 'A new project', 'description': 'Some description', - 'catalog': catalog_id, - 'visibility': 'private' + 'catalog': catalog_id } response = client.post(url, data) @@ -275,8 +271,7 @@ def test_project_create_parent_post(db, client, username, password): 'title': 'A new project', 'description': 'Some description', 'catalog': catalog_id, - 'parent': project_id, - 'visibility': 'private' + 'parent': project_id } response = client.post(url, data) @@ -318,8 +313,7 @@ def test_project_update_post(db, client, username, password, project_id): data = { 'title': 'New title', 'description': project.description, - 'catalog': project.catalog.pk, - 'visibility': 'private' + 'catalog': project.catalog.pk } response = client.post(url, data) @@ -341,13 +335,14 @@ def test_project_update_post_parent(db, client, username, password, project_id): client.login(username=username, password=password) project = Project.objects.get(pk=project_id) + print(project, project_id, Project.objects.get(pk=parent_id)) + url = reverse('project_update', args=[project_id]) data = { 'title': project.title, 'description': project.description, 'catalog': project.catalog.pk, - 'parent': parent_id, - 'visibility': 'private' + 'parent': parent_id } response = client.post(url, data) @@ -413,47 +408,6 @@ def test_project_update_information_post(db, client, username, password, project assert Project.objects.get(pk=project_id).title == project.title -@pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) -def test_project_update_visibility_get(db, client, username, password, project_id): - client.login(username=username, password=password) - - url = reverse('project_update_visibility', args=[project_id]) - response = client.get(url) - - if project_id in change_project_permission_map.get(username, []): - assert response.status_code == 200 - else: - if password: - assert response.status_code == 403 - else: - assert response.status_code == 302 - - -@pytest.mark.parametrize('username,password', users) -@pytest.mark.parametrize('project_id', projects) -def test_project_update_visibility_post(db, client, username, password, project_id): - client.login(username=username, password=password) - project = Project.objects.get(pk=project_id) - - url = reverse('project_update_visibility', args=[project_id]) - data = { - 'visibility': 'internal' - } - response = client.post(url, data) - - if project_id in change_project_permission_map.get(username, []): - assert response.status_code == 302 - assert Project.objects.get(pk=project_id).visibility == 'internal' - else: - if password: - assert response.status_code == 403 - else: - assert response.status_code == 302 - - assert Project.objects.get(pk=project_id).visibility == project.visibility - - @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) def test_project_update_catalog_get(db, client, username, password, project_id): diff --git a/rdmo/projects/tests/test_view_project_update_visibility.py b/rdmo/projects/tests/test_view_project_update_visibility.py new file mode 100644 index 0000000000..fc67ef3184 --- /dev/null +++ b/rdmo/projects/tests/test_view_project_update_visibility.py @@ -0,0 +1,148 @@ +import pytest + +from django.urls import reverse + +from ..models import Project, Visibility + +users = ( + ('owner', 'owner'), + ('manager', 'manager'), + ('author', 'author'), + ('guest', 'guest'), + ('user', 'user'), + ('editor', 'editor'), + ('reviewer', 'reviewer'), + ('api', 'api'), + ('site', 'site'), + ('anonymous', None) +) + + +project_id = 12 + +@pytest.mark.parametrize('username,password', users) +def test_project_create_visibility_get(db, client, username, password): + client.login(username=username, password=password) + + project = Project.objects.get(id=project_id) + project.visibility.delete() + + url = reverse('project_update_visibility', args=[project_id]) + response = client.get(url) + + if username in ['admin', 'site', 'api']: + assert response.status_code == 200 + else: + if password: + assert response.status_code == 403 + else: + assert response.status_code == 302 + + +@pytest.mark.parametrize('username,password', users) +def test_project_create_visibility_post(db, client, username, password): + client.login(username=username, password=password) + + project = Project.objects.get(id=project_id) + project.visibility.delete() + + url = reverse('project_update_visibility', args=[project_id]) + data = {} + response = client.post(url, data) + + if username in ['admin', 'site', 'api']: + assert response.status_code == 302 + assert Project.objects.get(pk=project_id).visibility + else: + if password: + assert response.status_code == 403 + else: + assert response.status_code == 302 + + with pytest.raises(Visibility.DoesNotExist): + assert Project.objects.get(pk=project_id).visibility + + +def test_project_update_visibility_site_post(db, client, settings): + settings.MULTISITE = True + + client.login(username='site', password='site') + + url = reverse('project_update_visibility', args=[project_id]) + data = { + 'sites': [2] + } + response = client.post(url, data) + + assert response.status_code == 302 + + project = Project.objects.get(id=project_id) + + assert project.visibility + assert [site.id for site in project.visibility.sites.all()] == [2] + assert not project.visibility.groups.exists() + + +def test_project_update_visibility_group_post(db, client, settings): + settings.GROUPS = True + + client.login(username='site', password='site') + + url = reverse('project_update_visibility', args=[project_id]) + data = { + 'groups': [2] + } + response = client.post(url, data) + + assert response.status_code == 302 + + project = Project.objects.get(id=project_id) + + assert project.visibility + assert [site.id for site in project.visibility.sites.all()] == [1] # this site is in the fixture + assert [group.id for group in project.visibility.groups.all()] == [2] + + +def test_project_update_visibility_site_group_post(db, client, settings): + settings.MULTISITE = True + settings.GROUPS = True + + client.login(username='site', password='site') + + url = reverse('project_update_visibility', args=[project_id]) + data = { + 'sites': [2], + 'groups': [2] + } + response = client.post(url, data) + + assert response.status_code == 302 + + project = Project.objects.get(id=project_id) + + assert project.visibility + assert [site.id for site in project.visibility.sites.all()] == [2] + assert [group.id for group in project.visibility.groups.all()] == [2] + + +@pytest.mark.parametrize('username,password', users) +def test_project_delete_visibility_post(db, client, username, password): + client.login(username=username, password=password) + + url = reverse('project_update_visibility', args=[project_id]) + data = { + 'delete': 'some value' + } + response = client.post(url, data) + + if username in ['admin', 'site', 'api']: + assert response.status_code == 302 + with pytest.raises(Visibility.DoesNotExist): + assert Project.objects.get(pk=project_id).visibility + else: + if password: + assert response.status_code == 403 + else: + assert response.status_code == 302 + + assert Project.objects.get(pk=project_id).visibility diff --git a/rdmo/projects/tests/test_view_project_visibility.py b/rdmo/projects/tests/test_view_project_visibility.py new file mode 100644 index 0000000000..99590a63e6 --- /dev/null +++ b/rdmo/projects/tests/test_view_project_visibility.py @@ -0,0 +1,188 @@ +from django.contrib.auth.models import Group, User +from django.contrib.sites.models import Site +from django.urls import reverse + +from ..models import Project + +user_username = 'user' +project_id = 12 + + +def test_detail_cleared(db, client): + client.login(username='user', password='user') + + project = Project.objects.get(id=project_id) + project.visibility.sites.clear() + project.visibility.groups.clear() + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 200 + + +def test_detail_deleted(db, client): + client.login(username='user', password='user') + + project = Project.objects.get(id=project_id) + project.visibility.delete() + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 403 + + +def test_detail_site(db, client): + client.login(username='user', password='user') + + project = Project.objects.get(id=project_id) + project.visibility.sites.set(Site.objects.filter(id=1)) + project.visibility.groups.clear() + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 200 + + +def test_detail_multiple_sites(db, client): + client.login(username='user', password='user') + + project = Project.objects.get(id=project_id) + project.visibility.sites.set(Site.objects.filter(id__in=[1, 2])) + project.visibility.groups.clear() + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 200 + + +def test_detail_site_forbidden(db, client): + client.login(username='user', password='user') + + project = Project.objects.get(id=project_id) + project.visibility.sites.set(Site.objects.filter(id=2)) + project.visibility.groups.clear() + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 403 + + +def test_detail_group(db, client): + client.login(username='user', password='user') + + group = Group.objects.create(name='test') + user = User.objects.get(username='user') + user.groups.add(group) + + project = Project.objects.get(id=project_id) + project.visibility.sites.clear() + project.visibility.groups.add(group) + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 200 + + +def test_detail_multiple_groups(db, client): + client.login(username='user', password='user') + + group = Group.objects.create(name='test') + user = User.objects.get(username='user') + user.groups.add(group) + + project = Project.objects.get(id=project_id) + project.visibility.sites.clear() + project.visibility.groups.set([ + group, Group.objects.create(name='test2') + ]) + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 200 + + +def test_detail_group_forbidden(db, client): + client.login(username='user', password='user') + + group = Group.objects.create(name='test') + + project = Project.objects.get(id=project_id) + project.visibility.sites.clear() + project.visibility.groups.set([group]) + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 403 + + +def test_detail_site_group(db, client): + client.login(username='user', password='user') + + group = Group.objects.create(name='test') + user = User.objects.get(username='user') + user.groups.add(group) + + project = Project.objects.get(id=project_id) + project.visibility.sites.set(Site.objects.filter(id=1)) + project.visibility.groups.set([group]) + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 200 + + +def test_detail_site_group_forbidden(db, client): + client.login(username='user', password='user') + + group = Group.objects.create(name='test') + user = User.objects.get(username='user') + user.groups.add(group) + + project = Project.objects.get(id=project_id) + project.visibility.sites.set(Site.objects.filter(id=2)) + project.visibility.groups.set([group]) + + url = reverse('project', args=[project_id]) + response = client.get(url) + assert response.status_code == 403 + + +# @pytest.mark.parametrize('username,password', users) +# @pytest.mark.parametrize('project_id', projects) +# def test_project_update_visibility_get(db, client, username, password, project_id): +# client.login(username=username, password=password) + +# url = reverse('project_update_visibility', args=[project_id]) +# response = client.get(url) + +# if project_id in change_project_permission_map.get(username, []): +# assert response.status_code == 200 +# else: +# if password: +# assert response.status_code == 403 +# else: +# assert response.status_code == 302 + + +# @pytest.mark.parametrize('username,password', users) +# @pytest.mark.parametrize('project_id', projects) +# def test_project_update_visibility_post(db, client, username, password, project_id): +# client.login(username=username, password=password) +# project = Project.objects.get(pk=project_id) + +# url = reverse('project_update_visibility', args=[project_id]) +# data = { +# 'visibility': 'internal' +# } +# response = client.post(url, data) + +# if project_id in change_project_permission_map.get(username, []): +# assert response.status_code == 302 +# assert Project.objects.get(pk=project_id).visibility == 'internal' +# else: +# if password: +# assert response.status_code == 403 +# else: +# assert response.status_code == 302 + +# assert Project.objects.get(pk=project_id).visibility == project.visibility diff --git a/rdmo/projects/tests/test_viewset_project.py b/rdmo/projects/tests/test_viewset_project.py index 779132d21d..f45351743b 100644 --- a/rdmo/projects/tests/test_viewset_project.py +++ b/rdmo/projects/tests/test_viewset_project.py @@ -93,7 +93,7 @@ def test_list(db, client, username, password): def test_list_user(db, client): - client.login(username='admin', password='admin') + client.login(username='user', password='user') url = reverse(urlnames['list']) + '?user=1' response = client.get(url) diff --git a/rdmo/projects/tests/test_viewset_project_visibility.py b/rdmo/projects/tests/test_viewset_project_visibility.py new file mode 100644 index 0000000000..967ad27f88 --- /dev/null +++ b/rdmo/projects/tests/test_viewset_project_visibility.py @@ -0,0 +1,139 @@ +import pytest + +from django.urls import reverse + +from ..models import Project, Visibility + +users = ( + ('owner', 'owner'), + ('manager', 'manager'), + ('author', 'author'), + ('guest', 'guest'), + ('api', 'api'), + ('user', 'user'), + ('site', 'site'), + ('anonymous', None), +) + +project_id = 12 + +@pytest.mark.parametrize('username,password', users) +def test_project_visibility_get(db, client, username, password): + client.login(username=username, password=password) + + # project = Project.objects.get(id=project_id) + + url = reverse('v1-projects:project-visibility', args=[project_id]) + response = client.get(url) + + if username in ['admin', 'site', 'api']: + assert response.status_code == 200 + else: + if password: + assert response.status_code == 404 + else: + assert response.status_code == 401 + + +@pytest.mark.parametrize('username,password', users) +def test_project_visibility_get_not_found(db, client, username, password): + client.login(username=username, password=password) + + project = Project.objects.get(id=project_id) + project.visibility.delete() + + url = reverse('v1-projects:project-visibility', args=[project_id]) + response = client.get(url) + + if password: + assert response.status_code == 404 + else: + assert response.status_code == 401 + + +@pytest.mark.parametrize('username,password', users) +def test_project_visibility_post_create(db, client, username, password): + client.login(username=username, password=password) + + project = Project.objects.get(id=project_id) + project.visibility.delete() + + url = reverse('v1-projects:project-visibility', args=[project_id]) + data = {} + response = client.post(url, data) + + if username in ['admin', 'site', 'api']: + assert response.status_code == 200 + assert Project.objects.get(pk=project_id).visibility + else: + if password: + assert response.status_code == 404 + else: + assert response.status_code == 401 + + with pytest.raises(Visibility.DoesNotExist): + assert Project.objects.get(pk=project_id).visibility + + +@pytest.mark.parametrize('username,password', users) +def test_project_visibility_post_update(db, client, username, password): + client.login(username=username, password=password) + + url = reverse('v1-projects:project-visibility', args=[project_id]) + data = { + 'sites': [2] + } + response = client.post(url, data) + + project = Project.objects.get(pk=project_id) + + if username in ['admin', 'site', 'api']: + assert response.status_code == 200 + assert project.visibility + assert [site.id for site in project.visibility.sites.all()] == [2] + else: + if password: + assert response.status_code == 404 + else: + assert response.status_code == 401 + + assert project.visibility + assert [site.id for site in project.visibility.sites.all()] == [1] # from the fixture + + +@pytest.mark.parametrize('username,password', users) +def test_project_visibility_post_delete(db, client, username, password): + client.login(username=username, password=password) + + url = reverse('v1-projects:project-visibility', args=[project_id]) + response = client.delete(url) + + project = Project.objects.get(pk=project_id) + + if username in ['admin', 'site', 'api']: + assert response.status_code == 204 + with pytest.raises(Visibility.DoesNotExist): + assert project.visibility + else: + if password: + assert response.status_code == 404 + else: + assert response.status_code == 401 + + assert project.visibility + + +@pytest.mark.parametrize('username,password', users) +def test_project_visibility_post_delete_not_found(db, client, username, password): + client.login(username=username, password=password) + + project = Project.objects.get(pk=project_id) + project.visibility.delete() + + url = reverse('v1-projects:project-visibility', args=[project_id]) + response = client.delete(url) + + if password: + assert response.status_code == 404 + else: + assert response.status_code == 401 diff --git a/rdmo/projects/views/project_update.py b/rdmo/projects/views/project_update.py index f6203e493f..f5b79c8400 100644 --- a/rdmo/projects/views/project_update.py +++ b/rdmo/projects/views/project_update.py @@ -53,7 +53,7 @@ class ProjectUpdateVisibilityView(ObjectPermissionMixin, RedirectViewMixin, Upda model = Project queryset = Project.objects.all() form_class = ProjectUpdateVisibilityForm - permission_required = 'projects.change_project_object' + permission_required = 'projects.change_visibility_object' class ProjectUpdateCatalogView(ObjectPermissionMixin, RedirectViewMixin, UpdateView): diff --git a/rdmo/projects/viewsets.py b/rdmo/projects/viewsets.py index 6215be6912..a0221ee894 100644 --- a/rdmo/projects/viewsets.py +++ b/rdmo/projects/viewsets.py @@ -35,13 +35,15 @@ SnapshotFilterBackend, ValueFilterBackend, ) -from .models import Continuation, Integration, Invite, Issue, Membership, Project, Snapshot, Value +from .models import Continuation, Integration, Invite, Issue, Membership, Project, Snapshot, Value, Visibility from .permissions import ( HasProjectPagePermission, HasProjectPermission, HasProjectProgressModelPermission, HasProjectProgressObjectPermission, HasProjectsPermission, + HasProjectVisibilityModelPermission, + HasProjectVisibilityObjectPermission, ) from .progress import ( compute_navigation, @@ -66,6 +68,7 @@ ProjectSerializer, ProjectSnapshotSerializer, ProjectValueSerializer, + ProjectVisibilitySerializer, SnapshotSerializer, UserInviteSerializer, ValueSerializer, @@ -285,6 +288,37 @@ def progress(self, request, pk=None): 'ratio': ratio }) + @action(detail=True, methods=['get', 'post', 'delete'], + permission_classes=(HasProjectVisibilityModelPermission | HasProjectVisibilityObjectPermission, )) + def visibility(self, request, pk=None): + project = self.get_object() + + try: + instance = project.visibility + except Visibility.DoesNotExist: + instance = None + + serializer = ProjectVisibilitySerializer(instance) + + if request.method == 'POST': + serializer = ProjectVisibilitySerializer(instance, data=dict(**request.data, project=project.id)) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + elif request.method == 'DELETE': + if instance is not None: + instance.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + else: + if instance is not None: + serializer = ProjectVisibilitySerializer(instance) + return Response(serializer.data) + + # if nothing worked, raise 404 + raise Http404 + @action(detail=False, url_path='upload-accept', permission_classes=(IsAuthenticated, )) def upload_accept(self, request): return Response(get_upload_accept()) diff --git a/testing/fixtures/projects.json b/testing/fixtures/projects.json index 68fcd464cf..1d55116720 100644 --- a/testing/fixtures/projects.json +++ b/testing/fixtures/projects.json @@ -348,7 +348,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 1, "rght": 2, "tree_id": 1, @@ -371,7 +370,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 1, "rght": 6, "tree_id": 4, @@ -394,7 +392,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 2, "rght": 5, "tree_id": 4, @@ -417,7 +414,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 10, "rght": 11, "tree_id": 4, @@ -440,7 +436,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 3, "rght": 4, "tree_id": 4, @@ -463,7 +458,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 2, "rght": 7, "tree_id": 5, @@ -486,7 +480,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 6, "rght": 8, "tree_id": 6, @@ -509,7 +502,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 7, "rght": 9, "tree_id": 7, @@ -532,7 +524,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 8, "rght": 12, "tree_id": 8, @@ -555,7 +546,6 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 1, "rght": 2, "tree_id": 9, @@ -580,10 +570,9 @@ "catalog": 1, "progress_total": null, "progress_count": null, - "visibility": "private", "lft": 1, "rght": 2, - "tree_id": 4, + "tree_id": 10, "level": 0, "views": [ 1, @@ -605,10 +594,9 @@ "catalog": 1, "progress_total": 29, "progress_count": 1, - "visibility": "internal", "lft": 1, "rght": 2, - "tree_id": 3, + "tree_id": 11, "level": 0, "views": [ 1, @@ -6174,5 +6162,18 @@ "unit": "", "external_id": "" } + }, + { + "model": "projects.visibility", + "pk": 1, + "fields": { + "created": "2024-12-06T12:33:32.464Z", + "updated": "2024-12-06T12:34:35.096Z", + "project": 12, + "sites": [ + 1 + ], + "groups": [] + } } ] From 76f7c035b7e70214c52708a4f906fbfca6b96894 Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Thu, 12 Dec 2024 14:01:52 +0100 Subject: [PATCH 08/10] Cleanup tests --- .../tests/test_view_project_visibility.py | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/rdmo/projects/tests/test_view_project_visibility.py b/rdmo/projects/tests/test_view_project_visibility.py index 99590a63e6..04b8c2a8c5 100644 --- a/rdmo/projects/tests/test_view_project_visibility.py +++ b/rdmo/projects/tests/test_view_project_visibility.py @@ -145,44 +145,3 @@ def test_detail_site_group_forbidden(db, client): url = reverse('project', args=[project_id]) response = client.get(url) assert response.status_code == 403 - - -# @pytest.mark.parametrize('username,password', users) -# @pytest.mark.parametrize('project_id', projects) -# def test_project_update_visibility_get(db, client, username, password, project_id): -# client.login(username=username, password=password) - -# url = reverse('project_update_visibility', args=[project_id]) -# response = client.get(url) - -# if project_id in change_project_permission_map.get(username, []): -# assert response.status_code == 200 -# else: -# if password: -# assert response.status_code == 403 -# else: -# assert response.status_code == 302 - - -# @pytest.mark.parametrize('username,password', users) -# @pytest.mark.parametrize('project_id', projects) -# def test_project_update_visibility_post(db, client, username, password, project_id): -# client.login(username=username, password=password) -# project = Project.objects.get(pk=project_id) - -# url = reverse('project_update_visibility', args=[project_id]) -# data = { -# 'visibility': 'internal' -# } -# response = client.post(url, data) - -# if project_id in change_project_permission_map.get(username, []): -# assert response.status_code == 302 -# assert Project.objects.get(pk=project_id).visibility == 'internal' -# else: -# if password: -# assert response.status_code == 403 -# else: -# assert response.status_code == 302 - -# assert Project.objects.get(pk=project_id).visibility == project.visibility From d370398adb339d4b5ff4bf6d734c38d147d5cb74 Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Thu, 12 Dec 2024 14:06:07 +0100 Subject: [PATCH 09/10] Improve project visibility link --- .../templates/projects/project_detail_sidebar.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rdmo/projects/templates/projects/project_detail_sidebar.html b/rdmo/projects/templates/projects/project_detail_sidebar.html index 67187f87b8..11f8d8de02 100644 --- a/rdmo/projects/templates/projects/project_detail_sidebar.html +++ b/rdmo/projects/templates/projects/project_detail_sidebar.html @@ -81,7 +81,13 @@

      {% trans 'Options' %}

      {% if settings.PROJECT_VISIBILITY and can_change_visibility %} {% endif %} From 12798cee7824cc9ddf9e2a3465e3b9fc24410e5f Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Thu, 12 Dec 2024 14:20:56 +0100 Subject: [PATCH 10/10] Add separate form for project visibility and improve wording --- rdmo/core/templates/core/bootstrap_form.html | 2 ++ .../projects/project_detail_sidebar.html | 2 +- .../templates/projects/project_form.html | 17 ----------- .../projects/project_form_visibility.html | 30 +++++++++++++++++++ rdmo/projects/views/project_update.py | 1 + 5 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 rdmo/projects/templates/projects/project_form_visibility.html diff --git a/rdmo/core/templates/core/bootstrap_form.html b/rdmo/core/templates/core/bootstrap_form.html index e15f020694..4cbced2125 100644 --- a/rdmo/core/templates/core/bootstrap_form.html +++ b/rdmo/core/templates/core/bootstrap_form.html @@ -6,7 +6,9 @@ {% include 'core/bootstrap_form_fields.html' %} + {% if submit %} + {% endif %} {% if delete %} {% endif %} diff --git a/rdmo/projects/templates/projects/project_detail_sidebar.html b/rdmo/projects/templates/projects/project_detail_sidebar.html index 11f8d8de02..5c90ed7d5c 100644 --- a/rdmo/projects/templates/projects/project_detail_sidebar.html +++ b/rdmo/projects/templates/projects/project_detail_sidebar.html @@ -83,7 +83,7 @@

      {% trans 'Options' %}

    • {% if not project.visibility %} - {% trans 'Make project visible to users' %} + {% trans 'Make project visible' %} {% else %} {% trans 'Update project visibility' %} {% endif %} diff --git a/rdmo/projects/templates/projects/project_form.html b/rdmo/projects/templates/projects/project_form.html index 74a6c72a49..9fbd90f914 100644 --- a/rdmo/projects/templates/projects/project_form.html +++ b/rdmo/projects/templates/projects/project_form.html @@ -31,23 +31,6 @@

      {% trans 'Update project tasks' %}

      {% trans 'Update project views' %}

      {% bootstrap_form submit=_('Save views') %} - {% elif request.resolver_match.url_name == 'project_update_visibility' %} - -

      {% trans 'Update project visibility' %}

      - -

      - {% blocktrans trimmed %} - Projects can be made visible to all users, for example to be used as a template. - When a project is made visible, users can access it as if they were in the guest role. - {% endblocktrans %} -

      - - {% if object.visibility %} - {% bootstrap_form submit=_('Save visibility') delete=_('Remove visibility') %} - {% else %} - {% bootstrap_form submit=_('Make visible') %} - {% endif %} - {% else %}

      {% trans 'Update project information' %}

      diff --git a/rdmo/projects/templates/projects/project_form_visibility.html b/rdmo/projects/templates/projects/project_form_visibility.html new file mode 100644 index 0000000000..2356b821ee --- /dev/null +++ b/rdmo/projects/templates/projects/project_form_visibility.html @@ -0,0 +1,30 @@ +{% extends 'core/page.html' %} +{% load i18n %} +{% load core_tags %} + +{% block page %} + +

      + {% if object.visibility %} + {% trans 'Update project visibility' %} + {% else %} + {% trans 'Make project visible' %} + {% endif %} +

      + +

      + {% blocktrans trimmed %} + Projects can be made visible to all users, for example to be used as a template. + When a project is made visible, users can access it as if they were in the guest role. + {% endblocktrans %} +

      + + {% if object.visibility and 'sites' in form.fields or 'groups' in form.fields %} + {% bootstrap_form submit=_('Update visibility') delete=_('Remove visibility') %} + {% elif object.visibility %} + {% bootstrap_form delete=_('Remove visibility') %} + {% else %} + {% bootstrap_form submit=_('Make visible') %} + {% endif %} + +{% endblock %} diff --git a/rdmo/projects/views/project_update.py b/rdmo/projects/views/project_update.py index f5b79c8400..b1889f7d8e 100644 --- a/rdmo/projects/views/project_update.py +++ b/rdmo/projects/views/project_update.py @@ -54,6 +54,7 @@ class ProjectUpdateVisibilityView(ObjectPermissionMixin, RedirectViewMixin, Upda queryset = Project.objects.all() form_class = ProjectUpdateVisibilityForm permission_required = 'projects.change_visibility_object' + template_name = 'projects/project_form_visibility.html' class ProjectUpdateCatalogView(ObjectPermissionMixin, RedirectViewMixin, UpdateView):