diff --git a/config/settings/base.py b/config/settings/base.py index 81499eeb..ed2cacd2 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -308,6 +308,8 @@ "cdsa:records:studies", "cdsa:records:workspaces", "cdsa:records:user_access", + "dbgap:records:index", + "dbgap:records:applications", ] # django-dbbackup @@ -372,6 +374,7 @@ "primed.dbgap.adapters.dbGaPWorkspaceAdapter", "primed.cdsa.adapters.CDSAWorkspaceAdapter", "primed.miscellaneous_workspaces.adapters.ConsortiumDevelWorkspaceAdapter", + "primed.miscellaneous_workspaces.adapters.DataPrepWorkspaceAdapter", "primed.miscellaneous_workspaces.adapters.ExampleWorkspaceAdapter", "primed.miscellaneous_workspaces.adapters.SimulatedDataWorkspaceAdapter", "primed.miscellaneous_workspaces.adapters.OpenAccessWorkspaceAdapter", diff --git a/primed/cdsa/tests/test_views.py b/primed/cdsa/tests/test_views.py index 1689582b..86deb9d8 100644 --- a/primed/cdsa/tests/test_views.py +++ b/primed/cdsa/tests/test_views.py @@ -40,6 +40,70 @@ User = get_user_model() +class NavbarTest(TestCase): + """Tests for the navbar involving CDSA links.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with both view and edit permission. + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:index", args=args) + + def test_links_for_staff_view(self): + """Returns successful response code.""" + user = User.objects.create_user(username="test", password="test") + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url()) + self.assertContains(response, reverse("cdsa:agreement_versions:list")) + self.assertContains(response, reverse("cdsa:audit:signed_agreements")) + self.assertContains(response, reverse("cdsa:audit:workspaces")) + self.assertContains(response, reverse("cdsa:records:index")) + # Links to add CDSAs. + self.assertNotContains(response, reverse("cdsa:signed_agreements:members:new")) + self.assertNotContains( + response, reverse("cdsa:signed_agreements:data_affiliates:new") + ) + self.assertNotContains( + response, reverse("cdsa:signed_agreements:non_data_affiliates:new") + ) + + def test_links_for_staff_edit(self): + """Returns successful response code.""" + user = User.objects.create_user(username="test", password="test") + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url()) + self.assertContains(response, reverse("cdsa:agreement_versions:list")) + self.assertContains(response, reverse("cdsa:audit:signed_agreements")) + self.assertContains(response, reverse("cdsa:audit:workspaces")) + self.assertContains(response, reverse("cdsa:records:index")) + # Links to add CDSAs. + self.assertContains(response, reverse("cdsa:signed_agreements:members:new")) + self.assertContains( + response, reverse("cdsa:signed_agreements:data_affiliates:new") + ) + self.assertContains( + response, reverse("cdsa:signed_agreements:non_data_affiliates:new") + ) + + class AgreementVersionListTest(TestCase): """Tests for the AgreementVersionList view.""" @@ -50,7 +114,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -122,7 +186,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) # Create an object test this with. @@ -259,12 +323,12 @@ def test_invalidate_button_valid_user_has_edit_perm(self): user = User.objects.create_user(username="test_edit", password="test_edit") user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.client.force_login(user) @@ -296,12 +360,12 @@ def test_invalidate_button_invalid_user_has_edit_perm(self): user = User.objects.create_user(username="test_edit", password="test_edit") user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.client.force_login(user) @@ -340,12 +404,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -390,7 +454,7 @@ def test_access_without_user_permission_view(self): ) user_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url(instance.version)) @@ -558,7 +622,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) # Create an object test this with. @@ -756,7 +820,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -831,12 +895,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -870,7 +934,7 @@ def test_access_with_view_permission(self): ) user_with_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url(1)) @@ -991,12 +1055,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -1030,7 +1094,7 @@ def test_access_with_view_permission(self): ) user_with_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url(1)) @@ -1151,12 +1215,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -1190,7 +1254,7 @@ def test_access_with_view_permission(self): ) user_with_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url(1)) @@ -1311,12 +1375,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -1359,7 +1423,7 @@ def test_access_without_user_permission_view(self): ) user_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url()) @@ -2158,7 +2222,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) # Create an object test this with. @@ -2258,12 +2322,12 @@ def test_change_status_button_user_has_edit_perm(self): user = User.objects.create_user(username="test_edit", password="test_edit") user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.client.force_login(user) @@ -2305,7 +2369,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -2378,12 +2442,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -2426,7 +2490,7 @@ def test_access_without_user_permission_view(self): ) user_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url()) @@ -3370,7 +3434,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) # Create an object test this with. @@ -3476,12 +3540,12 @@ def test_change_status_button_user_has_edit_perm(self): user = User.objects.create_user(username="test_edit", password="test_edit") user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.client.force_login(user) @@ -3523,7 +3587,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -3596,12 +3660,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -3644,7 +3708,7 @@ def test_access_without_user_permission_view(self): ) user_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url()) @@ -4427,7 +4491,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) # Create an object test this with. @@ -4521,12 +4585,12 @@ def test_change_status_button_user_has_edit_perm(self): user = User.objects.create_user(username="test_edit", password="test_edit") user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.client.force_login(user) @@ -4568,7 +4632,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -4744,7 +4808,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) # Create the test group. @@ -4923,7 +4987,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.anvil_cdsa_group = ManagedGroupFactory.create(name="TEST_PRIMED_CDSA") @@ -5471,7 +5535,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -5540,12 +5604,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.requester = UserFactory.create() diff --git a/primed/cdsa/views.py b/primed/cdsa/views.py index df0c4f8b..952804a7 100644 --- a/primed/cdsa/views.py +++ b/primed/cdsa/views.py @@ -2,8 +2,8 @@ from anvil_consortium_manager.anvil_api import AnVILAPIError from anvil_consortium_manager.auth import ( - AnVILConsortiumManagerEditRequired, - AnVILConsortiumManagerViewRequired, + AnVILConsortiumManagerStaffEditRequired, + AnVILConsortiumManagerStaffViewRequired, AnVILProjectManagerAccess, ) from anvil_consortium_manager.models import GroupAccountMembership, ManagedGroup @@ -26,7 +26,7 @@ class AgreementMajorVersionDetail( - AnVILConsortiumManagerViewRequired, MultiTableMixin, DetailView + AnVILConsortiumManagerStaffViewRequired, MultiTableMixin, DetailView ): """Display a "detail" page for an agreement major version (e.g., 1.x).""" @@ -59,7 +59,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["show_deprecation_message"] = not self.object.is_valid edit_permission_codename = "anvil_consortium_manager." + ( - AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) context[ "show_invalidate_button" @@ -70,7 +70,7 @@ def get_context_data(self, **kwargs): class AgreementMajorVersionInvalidate( - AnVILConsortiumManagerEditRequired, SuccessMessageMixin, UpdateView + AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, UpdateView ): """A view to invalidate an AgreementMajorVersion instance. @@ -132,7 +132,7 @@ def get_success_url(self): class AgreementVersionDetail( - AnVILConsortiumManagerViewRequired, SingleTableMixin, DetailView + AnVILConsortiumManagerStaffViewRequired, SingleTableMixin, DetailView ): """Display a "detail" page for an agreement major/minor version (e.g., 1.3).""" @@ -167,14 +167,14 @@ def get_context_data(self, **kwargs): return context -class AgreementVersionList(AnVILConsortiumManagerViewRequired, SingleTableView): +class AgreementVersionList(AnVILConsortiumManagerStaffViewRequired, SingleTableView): """Display a list of AgreementVersions.""" model = models.AgreementVersion table_class = tables.AgreementVersionTable -class SignedAgreementList(AnVILConsortiumManagerViewRequired, SingleTableView): +class SignedAgreementList(AnVILConsortiumManagerStaffViewRequired, SingleTableView): """Display a list of SignedAgreement objects.""" model = models.SignedAgreement @@ -294,7 +294,7 @@ def get_success_url(self): class MemberAgreementCreate( - AnVILConsortiumManagerEditRequired, + AnVILConsortiumManagerStaffEditRequired, AgreementTypeCreateMixin, SuccessMessageMixin, FormView, @@ -311,7 +311,7 @@ class MemberAgreementCreate( class DataAffiliateAgreementCreate( - AnVILConsortiumManagerEditRequired, + AnVILConsortiumManagerStaffEditRequired, AgreementTypeCreateMixin, SuccessMessageMixin, FormView, @@ -348,7 +348,7 @@ def get_agreement_type(self, form, formset): return agreement_type -class MemberAgreementDetail(AnVILConsortiumManagerViewRequired, DetailView): +class MemberAgreementDetail(AnVILConsortiumManagerStaffViewRequired, DetailView): """View to show details about a `MemberAgreement`.""" model = models.MemberAgreement @@ -371,7 +371,7 @@ def get_context_data(self, **kwargs): "show_deprecation_message" ] = not self.object.signed_agreement.version.major_version.is_valid edit_permission_codename = "anvil_consortium_manager." + ( - AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) context["show_update_button"] = self.request.user.has_perm( edit_permission_codename @@ -379,7 +379,7 @@ def get_context_data(self, **kwargs): return context -class MemberAgreementList(AnVILConsortiumManagerViewRequired, SingleTableView): +class MemberAgreementList(AnVILConsortiumManagerStaffViewRequired, SingleTableView): """Display a list of MemberAgreement objects.""" model = models.MemberAgreement @@ -387,7 +387,7 @@ class MemberAgreementList(AnVILConsortiumManagerViewRequired, SingleTableView): class SignedAgreementStatusUpdate( - AnVILConsortiumManagerEditRequired, SuccessMessageMixin, UpdateView + AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, UpdateView ): model = models.SignedAgreement @@ -411,7 +411,7 @@ def get_object(self, queryset=None): return obj -class DataAffiliateAgreementDetail(AnVILConsortiumManagerViewRequired, DetailView): +class DataAffiliateAgreementDetail(AnVILConsortiumManagerStaffViewRequired, DetailView): """View to show details about a `DataAffiliateAgreement`.""" model = models.DataAffiliateAgreement @@ -434,7 +434,7 @@ def get_context_data(self, **kwargs): "show_deprecation_message" ] = not self.object.signed_agreement.version.major_version.is_valid edit_permission_codename = "anvil_consortium_manager." + ( - AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) context["show_update_button"] = self.request.user.has_perm( edit_permission_codename @@ -442,7 +442,9 @@ def get_context_data(self, **kwargs): return context -class DataAffiliateAgreementList(AnVILConsortiumManagerViewRequired, SingleTableView): +class DataAffiliateAgreementList( + AnVILConsortiumManagerStaffViewRequired, SingleTableView +): """Display a list of DataAffiliateAgreement objects.""" model = models.DataAffiliateAgreement @@ -450,7 +452,7 @@ class DataAffiliateAgreementList(AnVILConsortiumManagerViewRequired, SingleTable class NonDataAffiliateAgreementCreate( - AnVILConsortiumManagerEditRequired, + AnVILConsortiumManagerStaffEditRequired, AgreementTypeCreateMixin, SuccessMessageMixin, FormView, @@ -466,7 +468,9 @@ class NonDataAffiliateAgreementCreate( ERROR_CREATING_GROUP = "Error creating access group on AnVIL." -class NonDataAffiliateAgreementDetail(AnVILConsortiumManagerViewRequired, DetailView): +class NonDataAffiliateAgreementDetail( + AnVILConsortiumManagerStaffViewRequired, DetailView +): """View to show details about a `NonDataAffiliateAgreement`.""" model = models.NonDataAffiliateAgreement @@ -489,7 +493,7 @@ def get_context_data(self, **kwargs): "show_deprecation_message" ] = not self.object.signed_agreement.version.major_version.is_valid edit_permission_codename = "anvil_consortium_manager." + ( - AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) context["show_update_button"] = self.request.user.has_perm( edit_permission_codename @@ -498,7 +502,7 @@ def get_context_data(self, **kwargs): class NonDataAffiliateAgreementList( - AnVILConsortiumManagerViewRequired, SingleTableView + AnVILConsortiumManagerStaffViewRequired, SingleTableView ): """Display a list of NonDataAffiliateAgreement objects.""" @@ -506,7 +510,7 @@ class NonDataAffiliateAgreementList( table_class = tables.NonDataAffiliateAgreementTable -class SignedAgreementAudit(AnVILConsortiumManagerViewRequired, TemplateView): +class SignedAgreementAudit(AnVILConsortiumManagerStaffViewRequired, TemplateView): """View to show audit results for `SignedAgreements`.""" template_name = "cdsa/signedagreement_audit.html" @@ -538,7 +542,7 @@ def get_context_data(self, **kwargs): return context -class CDSAWorkspaceAudit(AnVILConsortiumManagerViewRequired, TemplateView): +class CDSAWorkspaceAudit(AnVILConsortiumManagerStaffViewRequired, TemplateView): """View to show audit results for `CDSAWorkspaces`.""" template_name = "cdsa/cdsaworkspace_audit.html" diff --git a/primed/dbgap/tables.py b/primed/dbgap/tables.py index cfff4d04..2e075bcd 100644 --- a/primed/dbgap/tables.py +++ b/primed/dbgap/tables.py @@ -330,3 +330,40 @@ class Meta: model = models.dbGaPDataAccessRequest fields = ("dbgap_dac", "dbgap_current_status", "total") attrs = {"class": "table table-sm"} + + +class dbGaPApplicationRecordsTable(tables.Table): + """Class to render a publicly-viewable table of dbGaPApplication objects.""" + + dbgap_project_id = tables.columns.Column() + principal_investigator = tables.columns.Column( + verbose_name="Application PI", + accessor="principal_investigator__name", + ) + principal_investigator__study_sites = tables.columns.ManyToManyColumn( + verbose_name="Study site(s)", + ) + number_approved_dars = tables.columns.ManyToManyColumn( + accessor="dbgapdataaccesssnapshot_set", + filter=lambda qs: qs.filter(is_most_recent=True), + verbose_name="Number of approved DARs", + transform=lambda obj: obj.dbgapdataaccessrequest_set.approved().count(), + ) + number_requested_dars = tables.columns.ManyToManyColumn( + accessor="dbgapdataaccesssnapshot_set", + filter=lambda qs: qs.filter(is_most_recent=True), + verbose_name="Number of requested DARs", + transform=lambda obj: obj.dbgapdataaccessrequest_set.count(), + ) + last_update = ManyToManyDateTimeColumn( + accessor="dbgapdataaccesssnapshot_set", + filter=lambda qs: qs.filter(is_most_recent=True), + ) + + class Meta: + model = models.dbGaPApplication + fields = ( + "dbgap_project_id", + "principal_investigator", + ) + order_by = ("dbgap_project_id",) diff --git a/primed/dbgap/tests/test_views.py b/primed/dbgap/tests/test_views.py index 66ae2c8b..1791ca41 100644 --- a/primed/dbgap/tests/test_views.py +++ b/primed/dbgap/tests/test_views.py @@ -57,6 +57,60 @@ def tearDown(self): self.dbgap_response_mock.reset() +class NavbarTest(TestCase): + """Tests for the navbar involving CDSA links.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with both view and edit permission. + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:index", args=args) + + def test_links_for_staff_view(self): + """Returns successful response code.""" + user = User.objects.create_user(username="test", password="test") + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url()) + self.assertContains(response, reverse("dbgap:dbgap_study_accessions:list")) + self.assertContains(response, reverse("dbgap:dbgap_applications:list")) + # Links to add dbGaP info. + self.assertNotContains(response, reverse("dbgap:dbgap_study_accessions:new")) + self.assertNotContains(response, reverse("dbgap:dbgap_applications:new")) + self.assertNotContains( + response, reverse("dbgap:dbgap_applications:update_dars") + ) + + def test_links_for_staff_edit(self): + """Returns successful response code.""" + user = User.objects.create_user(username="test", password="test") + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url()) + self.assertContains(response, reverse("dbgap:dbgap_study_accessions:list")) + self.assertContains(response, reverse("dbgap:dbgap_applications:list")) + # Links to add dbGaP info. + self.assertContains(response, reverse("dbgap:dbgap_study_accessions:new")) + self.assertContains(response, reverse("dbgap:dbgap_applications:new")) + self.assertContains(response, reverse("dbgap:dbgap_applications:update_dars")) + + class dbGaPStudyAccessionListTest(TestCase): """Tests for the dbGaPStudyAccessionList view.""" @@ -67,7 +121,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -150,7 +204,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) # Create an object test this with. @@ -258,10 +312,10 @@ def test_context_show_edit_links_with_edit_permission(self): edit_user = User.objects.create_user(username="edit", password="test") edit_user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ), Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ), ) self.client.force_login(edit_user) @@ -281,7 +335,7 @@ def test_context_show_edit_links_with_view_permission(self): view_user = User.objects.create_user(username="edit", password="test") view_user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ), ) self.client.force_login(view_user) @@ -309,12 +363,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -358,7 +412,7 @@ def test_access_without_user_permission_view(self): ) user_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url()) @@ -506,12 +560,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -545,7 +599,7 @@ def test_access_with_view_permission(self): ) user_with_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url(instance.dbgap_phs)) @@ -645,7 +699,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -782,7 +836,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.workspace_type = "dbgap" @@ -815,7 +869,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -871,12 +925,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.requester = UserFactory.create() @@ -1021,12 +1075,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.requester = UserFactory.create() @@ -1250,7 +1304,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -1307,7 +1361,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) # Create an object test this with. @@ -1363,6 +1417,43 @@ def test_view_status_code_with_invalid_pk(self): with self.assertRaises(Http404): self.get_view()(request, pk=self.obj.dbgap_project_id + 1) + def test_staff_edit_links(self): + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME + ) + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.dbgap_project_id)) + self.assertContains( + response, + reverse( + "dbgap:dbgap_applications:dbgap_data_access_snapshots:new", + args=[self.obj.dbgap_project_id], + ), + ) + self.assertContains( + response, + reverse("dbgap:dbgap_applications:audit", args=[self.obj.dbgap_project_id]), + ) + "dbgap:dbgap_applications:dbgap_data_access_snapshots:new" + + def test_staff_view_links(self): + """No edit links if staff user only has view permission.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.dbgap_project_id)) + self.assertNotContains( + response, + reverse( + "dbgap:dbgap_applications:dbgap_data_access_snapshots:new", + args=[self.obj.dbgap_project_id], + ), + ) + self.assertContains( + response, + reverse("dbgap:dbgap_applications:audit", args=[self.obj.dbgap_project_id]), + ) + def test_context_snapshot_table(self): """The data_access_snapshot_table exists in the context.""" request = self.factory.get(self.get_url(self.obj.dbgap_project_id)) @@ -1488,12 +1579,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -1537,7 +1628,7 @@ def test_access_without_user_permission_view(self): ) user_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url()) @@ -1824,12 +1915,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.dbgap_application = factories.dbGaPApplicationFactory.create() @@ -1891,7 +1982,7 @@ def test_access_without_user_permission_view(self): ) user_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get( @@ -2550,12 +2641,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -2606,7 +2697,7 @@ def test_access_without_user_permission_view(self): ) user_view_perm.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url()) @@ -3097,7 +3188,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.application = factories.dbGaPApplicationFactory.create() @@ -3416,7 +3507,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -3513,7 +3604,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.application = factories.dbGaPApplicationFactory.create() @@ -3777,7 +3868,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.dbgap_workspace = factories.dbGaPWorkspaceFactory.create() @@ -4216,3 +4307,87 @@ def test_context_errors_table_no_dar_has_access(self): audit.dbGaPWorkspaceAccessAudit.ERROR_HAS_ACCESS, ) self.assertIsNotNone(table.rows[0].get_cell_value("action")) + + +class dbGaPRecordsIndexTest(TestCase): + """Tests for the dbGaPRecordsIndex view.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("dbgap:records:index", args=args) + + def test_status_code_not_logged_in(self): + "View redirects to login view when user is not logged in." + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_status_code_user_logged_in(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_links(self): + """response includes the correct links.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertContains(response, reverse("dbgap:records:applications")) + # In case we add similar public views later. + # self.assertContains(response, reverse("cdsa:records:user_access")) + # self.assertContains(response, reverse("cdsa:records:workspaces")) + + +class dbGaPApplicationRecordsListTest(TestCase): + """Tests for the dbGaPApplicationRecordsList view.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("dbgap:records:applications", args=args) + + def test_status_code_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_status_code_user_logged_in(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_table_class(self): + """The table is the correct class.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + self.assertIsInstance( + response.context_data["table"], tables.dbGaPApplicationRecordsTable + ) + + def test_table_no_rows(self): + """No rows are shown if there are no dbGaPApplications objects.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 0) + + def test_table_three_rows(self): + """Three rows are shown if there are three dbGaPApplication objects.""" + factories.dbGaPApplicationFactory.create_batch(3) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 3) diff --git a/primed/dbgap/urls.py b/primed/dbgap/urls.py index 9b0395eb..e49c633a 100644 --- a/primed/dbgap/urls.py +++ b/primed/dbgap/urls.py @@ -76,8 +76,23 @@ ], "workspaces", ) + +records_patterns = ( + [ + path("", views.dbGaPRecordsIndex.as_view(), name="index"), + path( + "applications/", + views.dbGaPApplicationRecords.as_view(), + name="applications", + ), + ], + "records", +) + + urlpatterns = [ path("studies/", include(dbgap_study_accession_patterns)), path("applications/", include(dbgap_application_patterns)), path("workspaces/", include(dbgap_workspace_patterns)), + path("records/", include(records_patterns)), ] diff --git a/primed/dbgap/views.py b/primed/dbgap/views.py index faffbbbf..545830b4 100644 --- a/primed/dbgap/views.py +++ b/primed/dbgap/views.py @@ -3,8 +3,8 @@ import requests from anvil_consortium_manager.anvil_api import AnVILAPIError from anvil_consortium_manager.auth import ( - AnVILConsortiumManagerEditRequired, - AnVILConsortiumManagerViewRequired, + AnVILConsortiumManagerStaffEditRequired, + AnVILConsortiumManagerStaffViewRequired, ) from anvil_consortium_manager.models import ( AnVILProjectManagerAccess, @@ -22,7 +22,13 @@ from django.http import Http404 from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, DetailView, FormView, UpdateView +from django.views.generic import ( + CreateView, + DetailView, + FormView, + TemplateView, + UpdateView, +) from django_tables2 import SingleTableMixin, SingleTableView from django_tables2.export.views import ExportMixin @@ -32,7 +38,7 @@ class dbGaPStudyAccessionDetail( - AnVILConsortiumManagerViewRequired, SingleTableMixin, DetailView + AnVILConsortiumManagerStaffViewRequired, SingleTableMixin, DetailView ): """View to show details about a `dbGaPStudyAccession`.""" @@ -62,14 +68,16 @@ def get_table(self): def get_context_data(self, **kwargs): """Add show_edit_links to context data.""" context = super().get_context_data(**kwargs) - edit_permission_codename = AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + edit_permission_codename = ( + AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME + ) context["show_edit_links"] = self.request.user.has_perm( "anvil_consortium_manager." + edit_permission_codename ) return context -class dbGaPStudyAccessionList(AnVILConsortiumManagerViewRequired, SingleTableView): +class dbGaPStudyAccessionList(AnVILConsortiumManagerStaffViewRequired, SingleTableView): """View to show a list of dbGaPStudyAccession objects.""" model = models.dbGaPStudyAccession @@ -77,7 +85,7 @@ class dbGaPStudyAccessionList(AnVILConsortiumManagerViewRequired, SingleTableVie class dbGaPStudyAccessionCreate( - AnVILConsortiumManagerEditRequired, SuccessMessageMixin, CreateView + AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, CreateView ): """View to create a new dbGaPStudyAccession.""" @@ -88,7 +96,7 @@ class dbGaPStudyAccessionCreate( class dbGaPStudyAccessionUpdate( - AnVILConsortiumManagerEditRequired, SuccessMessageMixin, UpdateView + AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, UpdateView ): """View to update a dbGaPStudyAccession.""" @@ -110,7 +118,7 @@ def get_object(self, queryset=None): class dbGaPStudyAccessionAutocomplete( - AnVILConsortiumManagerViewRequired, autocomplete.Select2QuerySetView + AnVILConsortiumManagerStaffViewRequired, autocomplete.Select2QuerySetView ): """View to provide autocompletion for dbGaPStudyAccessions.""" @@ -129,7 +137,7 @@ def get_queryset(self): class dbGaPApplicationDetail( - AnVILConsortiumManagerViewRequired, SingleTableMixin, DetailView + AnVILConsortiumManagerStaffViewRequired, SingleTableMixin, DetailView ): """View to show details about a `dbGaPApplication`.""" @@ -179,7 +187,7 @@ def get_context_data(self, *args, **kwargs): return context -class dbGaPApplicationList(AnVILConsortiumManagerViewRequired, SingleTableView): +class dbGaPApplicationList(AnVILConsortiumManagerStaffViewRequired, SingleTableView): """View to show a list of dbGaPApplication objects.""" model = models.dbGaPApplication @@ -187,7 +195,7 @@ class dbGaPApplicationList(AnVILConsortiumManagerViewRequired, SingleTableView): class dbGaPApplicationCreate( - AnVILConsortiumManagerEditRequired, SuccessMessageMixin, CreateView + AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, CreateView ): """View to create a new dbGaPApplication.""" @@ -227,7 +235,7 @@ def form_valid(self, form): class dbGaPDataAccessSnapshotCreate( - AnVILConsortiumManagerEditRequired, SuccessMessageMixin, FormView + AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, FormView ): form_class = forms.dbGaPDataAccessSnapshotForm @@ -315,7 +323,7 @@ def get_context_data(self, *args, **kwargs): class dbGaPDataAccessSnapshotCreateMultiple( - AnVILConsortiumManagerEditRequired, SuccessMessageMixin, FormView + AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, FormView ): form_class = forms.dbGaPDataAccessSnapshotMultipleForm @@ -425,7 +433,9 @@ def form_valid(self, form): return super().form_valid(form) -class dbGaPDataAccessSnapshotDetail(AnVILConsortiumManagerViewRequired, DetailView): +class dbGaPDataAccessSnapshotDetail( + AnVILConsortiumManagerStaffViewRequired, DetailView +): """View to show details about a `dbGaPDataAccessSnapshot`.""" model = models.dbGaPDataAccessSnapshot @@ -472,7 +482,7 @@ def get_context_data(self, **kwargs): class dbGaPDataAccessRequestList( - AnVILConsortiumManagerViewRequired, ExportMixin, SingleTableView + AnVILConsortiumManagerStaffViewRequired, ExportMixin, SingleTableView ): """View to show current DARs.""" @@ -486,7 +496,7 @@ def get_table_data(self): ) -class dbGaPApplicationAudit(AnVILConsortiumManagerViewRequired, DetailView): +class dbGaPApplicationAudit(AnVILConsortiumManagerStaffViewRequired, DetailView): """View to show audit results for a `dbGaPApplication`.""" model = models.dbGaPApplication @@ -530,7 +540,7 @@ def get_context_data(self, **kwargs): return context -class dbGaPWorkspaceAudit(AnVILConsortiumManagerViewRequired, DetailView): +class dbGaPWorkspaceAudit(AnVILConsortiumManagerStaffViewRequired, DetailView): """View to show audit results for a `dbGaPWorkspace`.""" model = models.dbGaPWorkspace @@ -570,3 +580,17 @@ def get_context_data(self, **kwargs): context["needs_action_table"] = data_access_audit.get_needs_action_table() context["data_access_audit"] = data_access_audit return context + + +class dbGaPRecordsIndex(TemplateView): + """Index page for dbGaP records.""" + + template_name = "dbgap/records_index.html" + + +class dbGaPApplicationRecords(SingleTableView): + """Display a public list of dbGaP applications.""" + + model = models.dbGaPApplication + template_name = "dbgap/dbgapapplication_records.html" + table_class = tables.dbGaPApplicationRecordsTable diff --git a/primed/duo/tests/test_views.py b/primed/duo/tests/test_views.py index 8988271f..5ebd2bc6 100644 --- a/primed/duo/tests/test_views.py +++ b/primed/duo/tests/test_views.py @@ -23,7 +23,7 @@ def setUp(self): self.user = UserFactory.create(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -126,7 +126,7 @@ def setUp(self): self.user = UserFactory.create(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -180,7 +180,7 @@ def setUp(self): self.user = UserFactory.create(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -282,7 +282,7 @@ def setUp(self): self.user = UserFactory.create(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) diff --git a/primed/duo/views.py b/primed/duo/views.py index 308cc3e1..06c7c749 100644 --- a/primed/duo/views.py +++ b/primed/duo/views.py @@ -1,11 +1,11 @@ -from anvil_consortium_manager.auth import AnVILConsortiumManagerViewRequired +from anvil_consortium_manager.auth import AnVILConsortiumManagerStaffViewRequired from django.http import Http404 from django.views.generic import DetailView, ListView from . import models -class DataUsePermissionList(AnVILConsortiumManagerViewRequired, ListView): +class DataUsePermissionList(AnVILConsortiumManagerStaffViewRequired, ListView): model = models.DataUsePermission @@ -18,7 +18,7 @@ def get_context_data(self, **kwargs): return context -class DataUsePermissionDetail(AnVILConsortiumManagerViewRequired, DetailView): +class DataUsePermissionDetail(AnVILConsortiumManagerStaffViewRequired, DetailView): model = models.DataUsePermission @@ -38,7 +38,7 @@ def get_context_data(self, **kwargs): return context -class DataUseModifierList(AnVILConsortiumManagerViewRequired, ListView): +class DataUseModifierList(AnVILConsortiumManagerStaffViewRequired, ListView): model = models.DataUseModifier @@ -51,7 +51,7 @@ def get_context_data(self, **kwargs): return context -class DataUseModifierDetail(AnVILConsortiumManagerViewRequired, DetailView): +class DataUseModifierDetail(AnVILConsortiumManagerStaffViewRequired, DetailView): model = models.DataUseModifier diff --git a/primed/miscellaneous_workspaces/adapters.py b/primed/miscellaneous_workspaces/adapters.py index 30d5d66b..feb17164 100644 --- a/primed/miscellaneous_workspaces/adapters.py +++ b/primed/miscellaneous_workspaces/adapters.py @@ -77,3 +77,18 @@ class OpenAccessWorkspaceAdapter(BaseWorkspaceAdapter): workspace_detail_template_name = ( "miscellaneous_workspaces/openaccessworkspace_detail.html" ) + + +class DataPrepWorkspaceAdapter(BaseWorkspaceAdapter): + """Adapter for DataPrepWorkspace.""" + + type = "data_prep" + name = "Data prep workspace" + description = "Workspaces used to prepare data." + list_table_class = tables.DataPrepWorkspaceTable + workspace_form_class = WorkspaceForm + workspace_data_model = models.DataPrepWorkspace + workspace_data_form_class = forms.DataPrepWorkspaceForm + workspace_detail_template_name = ( + "miscellaneous_workspaces/dataprepworkspace_detail.html" + ) diff --git a/primed/miscellaneous_workspaces/forms.py b/primed/miscellaneous_workspaces/forms.py index 0aa1a1d3..42856cfc 100644 --- a/primed/miscellaneous_workspaces/forms.py +++ b/primed/miscellaneous_workspaces/forms.py @@ -106,3 +106,27 @@ class Meta: ), "available_data": forms.CheckboxSelectMultiple, } + + +class DataPrepWorkspaceForm(Bootstrap5MediaFormMixin, forms.ModelForm): + """Form for a DataPrepWorkspace object.""" + + class Meta: + model = models.DataPrepWorkspace + fields = ( + "workspace", + "target_workspace", + "requested_by", + "is_active", + ) + # widgets = { + # "studies": autocomplete.ModelSelect2Multiple( + # url="primed_anvil:studies:autocomplete", + # attrs={"data-theme": "bootstrap-5"}, + # ), + # "requested_by": autocomplete.ModelSelect2( + # url="users:autocomplete", + # attrs={"data-theme": "bootstrap-5"}, + # ), + # "available_data": forms.CheckboxSelectMultiple, + # } diff --git a/primed/miscellaneous_workspaces/migrations/0008_dataprepworkspace_historicaldataprepworkspace.py b/primed/miscellaneous_workspaces/migrations/0008_dataprepworkspace_historicaldataprepworkspace.py new file mode 100644 index 00000000..eace0215 --- /dev/null +++ b/primed/miscellaneous_workspaces/migrations/0008_dataprepworkspace_historicaldataprepworkspace.py @@ -0,0 +1,58 @@ +# Generated by Django 3.2.19 on 2023-11-09 22:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields +import simple_history.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('anvil_consortium_manager', '0015_add_new_permissions'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('miscellaneous_workspaces', '0007_openaccessworkspace_data_url'), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalDataPrepWorkspace', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('is_active', models.BooleanField(default=True, help_text='Indicator of whether this workspace is currently being used for data preparation.')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('requested_by', models.ForeignKey(blank=True, db_constraint=False, help_text='The user who requested creation.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ('target_workspace', models.ForeignKey(blank=True, db_constraint=False, help_text='The workspace for which data is being prepared or updated.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='anvil_consortium_manager.workspace')), + ('workspace', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='anvil_consortium_manager.workspace')), + ], + options={ + 'verbose_name': 'historical data prep workspace', + 'verbose_name_plural': 'historical data prep workspaces', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='DataPrepWorkspace', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('is_active', models.BooleanField(default=True, help_text='Indicator of whether this workspace is currently being used for data preparation.')), + ('requested_by', models.ForeignKey(help_text='The user who requested creation.', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('target_workspace', models.ForeignKey(help_text='The workspace for which data is being prepared or updated.', on_delete=django.db.models.deletion.PROTECT, related_name='data_prep_workspaces', to='anvil_consortium_manager.workspace')), + ('workspace', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='anvil_consortium_manager.workspace')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/primed/miscellaneous_workspaces/models.py b/primed/miscellaneous_workspaces/models.py index f1f15b71..7329551f 100644 --- a/primed/miscellaneous_workspaces/models.py +++ b/primed/miscellaneous_workspaces/models.py @@ -1,7 +1,7 @@ """Model definitions for the `miscellaneous_workspaces` app.""" from anvil_consortium_manager.adapters.workspace import workspace_adapter_registry -from anvil_consortium_manager.models import BaseWorkspaceData +from anvil_consortium_manager.models import BaseWorkspaceData, Workspace from django.core.exceptions import ValidationError from django.db import models from django_extensions.db.models import TimeStampedModel @@ -62,3 +62,35 @@ class OpenAccessWorkspace(RequesterModel, TimeStampedModel, BaseWorkspaceData): help_text="The types of data available in this workspace.", blank=True, ) + + +class DataPrepWorkspace(RequesterModel, TimeStampedModel, BaseWorkspaceData): + """A model to track workspaces that are used to update data in another workspace.""" + + target_workspace = models.ForeignKey( + Workspace, + on_delete=models.PROTECT, + related_name="data_prep_workspaces", + help_text="The workspace for which data is being prepared or updated.", + ) + is_active = models.BooleanField( + default=True, + help_text="Indicator of whether this workspace is currently being used to prepare data.", + ) + + def clean(self): + if hasattr(self, "target_workspace"): + if self.target_workspace.workspace_type == "data_prep": + raise ValidationError( + { + "target_workspace": "target_workspace cannot be a DataPrepWorkspace." + } + ) + + if hasattr(self, "target_workspace") and hasattr(self, "workspace"): + if self.target_workspace == self.workspace: + raise ValidationError( + { + "target_workspace": "target_workspace must be different than workspace." + } + ) diff --git a/primed/miscellaneous_workspaces/tables.py b/primed/miscellaneous_workspaces/tables.py index 1a0a6c31..f9763621 100644 --- a/primed/miscellaneous_workspaces/tables.py +++ b/primed/miscellaneous_workspaces/tables.py @@ -3,7 +3,10 @@ import django_tables2 as tables from anvil_consortium_manager.models import Workspace -from primed.primed_anvil.tables import WorkspaceSharedWithConsortiumColumn +from primed.primed_anvil.tables import ( + BooleanIconColumn, + WorkspaceSharedWithConsortiumColumn, +) class OpenAccessWorkspaceTable(tables.Table): @@ -40,3 +43,25 @@ class Meta: "is_shared", ) order_by = ("name",) + + +class DataPrepWorkspaceTable(tables.Table): + """Class to render a table of Workspace objects with DataPrepWorkspace workspace data.""" + + name = tables.columns.Column(linkify=True) + # TODO: Figure out why this is not showing up + dataprepworkspace__target_workspace__name = tables.columns.Column( + linkify=True, verbose_name="Target workspace" + ) + dataprepworkspace__is_active = BooleanIconColumn( + verbose_name="Active?", show_false_icon=True + ) + + class Meta: + model = Workspace + fields = ( + "name", + "dataprepworkspace__target_workspace__name", + "dataprepworkspace__is_active", + ) + order_by = ("name",) diff --git a/primed/miscellaneous_workspaces/tests/factories.py b/primed/miscellaneous_workspaces/tests/factories.py index db096983..a13699a8 100644 --- a/primed/miscellaneous_workspaces/tests/factories.py +++ b/primed/miscellaneous_workspaces/tests/factories.py @@ -76,3 +76,14 @@ class OpenAccessWorkspaceFactory(DjangoModelFactory): class Meta: model = models.OpenAccessWorkspace + + +class DataPrepWorkspaceFactory(DjangoModelFactory): + """A factory for the DataPrepWorkspace model.""" + + workspace = SubFactory(WorkspaceFactory, workspace_type="data_prep") + target_workspace = SubFactory(WorkspaceFactory) + requested_by = SubFactory(UserFactory) + + class Meta: + model = models.DataPrepWorkspace diff --git a/primed/miscellaneous_workspaces/tests/test_forms.py b/primed/miscellaneous_workspaces/tests/test_forms.py index 74f98a63..033443e8 100644 --- a/primed/miscellaneous_workspaces/tests/test_forms.py +++ b/primed/miscellaneous_workspaces/tests/test_forms.py @@ -8,6 +8,7 @@ from primed.users.tests.factories import UserFactory from .. import forms +from . import factories class SimulatedDataWorkspaceFormTest(TestCase): @@ -365,7 +366,6 @@ def test_valid_data_url(self): def test_invalid_data_url_is_not_url(self): """Form is invalid if data_url is not a valid url.""" - """Form is invalid when missing data_source.""" form_data = { "workspace": self.workspace, "requested_by": self.requester, @@ -379,3 +379,86 @@ def test_invalid_data_url_is_not_url(self): self.assertIn("data_url", form.errors) self.assertEqual(len(form.errors["data_url"]), 1) self.assertIn("valid URL", form.errors["data_url"][0]) + + +class DataPrepWorkspaceFormTest(TestCase): + + form_class = forms.DataPrepWorkspaceForm + + def setUp(self): + """Create a workspace for use in the form.""" + self.workspace = WorkspaceFactory.create() + # Use OpenAccessData workspaces for now. + self.target_workspace = factories.OpenAccessWorkspaceFactory.create().workspace + self.requester = UserFactory.create() + + def test_valid(self): + """Form is valid with necessary input.""" + form_data = { + "workspace": self.workspace, + "target_workspace": self.target_workspace, + "requested_by": self.requester, + } + form = self.form_class(data=form_data) + self.assertTrue(form.is_valid()) + + def test_invalid_missing_workspace(self): + """Form is invalid when missing workspace.""" + form_data = { + # "workspace": self.workspace, + "target_workspace": self.target_workspace, + "requested_by": self.requester, + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("workspace", form.errors) + self.assertEqual(len(form.errors["workspace"]), 1) + self.assertIn("required", form.errors["workspace"][0]) + + def test_invalid_missing_target_workspace(self): + """Form is invalid if target_workspace is missing.""" + form_data = { + "workspace": self.workspace, + # "target_workspace": self.target_workspace, + "requested_by": self.requester, + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("target_workspace", form.errors) + self.assertEqual(len(form.errors["target_workspace"]), 1) + self.assertIn("required", form.errors["target_workspace"][0]) + + def test_invalid_missing_requested_by(self): + """Form is invalid if requested_by is missing.""" + form_data = { + "workspace": self.workspace, + "target_workspace": self.target_workspace, + # "requested_by": self.requester + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("requested_by", form.errors) + self.assertEqual(len(form.errors["requested_by"]), 1) + self.assertIn("required", form.errors["requested_by"][0]) + + def test_form_all_registered_workspaces(self): + """Form is invalid if intended_workspace_type is not a registered type.""" + workspace_types = list(workspace_adapter_registry.get_registered_names().keys()) + for workspace_type in workspace_types: + if workspace_type == "data_prep": + # Cannot create data prep workspaces for data prep workspace target_workspaces. + pass + else: + target_workspace = WorkspaceFactory.create( + workspace_type=workspace_type + ) + form_data = { + "workspace": self.workspace, + "target_workspace": target_workspace, + "requested_by": self.requester, + } + form = self.form_class(data=form_data) + self.assertTrue(form.is_valid()) diff --git a/primed/miscellaneous_workspaces/tests/test_models.py b/primed/miscellaneous_workspaces/tests/test_models.py index a4d23a4b..661c89f0 100644 --- a/primed/miscellaneous_workspaces/tests/test_models.py +++ b/primed/miscellaneous_workspaces/tests/test_models.py @@ -223,3 +223,75 @@ def test_data_url(self): workspace=workspace, requested_by=user, data_url="http://www.example.com" ) self.assertEqual(instance.data_url, "http://www.example.com") + + +class DataPrepWorkspaceTest(TestCase): + """Tests for the DataPrepWorkspace model.""" + + def test_model_saving(self): + """Creation using the model constructor and .save() works.""" + workspace = WorkspaceFactory.create() + target_workspace = WorkspaceFactory.create() + user = UserFactory.create() + instance = models.DataPrepWorkspace( + workspace=workspace, target_workspace=target_workspace, requested_by=user + ) + instance.save() + self.assertIsInstance(instance, models.DataPrepWorkspace) + + def test_str_method(self): + workspace = WorkspaceFactory.create( + billing_project__name="test-bp", name="test-ws" + ) + instance = factories.DataPrepWorkspaceFactory.create(workspace=workspace) + self.assertIsInstance(str(instance), str) + self.assertEqual(str(instance), "test-bp/test-ws") + + def test_two_update_workspaces_for_same_final_workspace(self): + target_workspace = WorkspaceFactory.create() + instance_1 = factories.DataPrepWorkspaceFactory.create( + target_workspace=target_workspace + ) + instance_2 = factories.DataPrepWorkspaceFactory.create( + target_workspace=target_workspace + ) + self.assertEqual(target_workspace.data_prep_workspaces.count(), 2) + self.assertIn(instance_1, target_workspace.data_prep_workspaces.all()) + self.assertIn(instance_2, target_workspace.data_prep_workspaces.all()) + + def test_clean_original_workspace_different_than_workspace(self): + """Clean method raises ValidationError when workspace is the same as original_workspace.""" + workspace = WorkspaceFactory.create() + user = UserFactory.create() + instance = models.DataPrepWorkspace( + requested_by=user, workspace=workspace, target_workspace=workspace + ) + with self.assertRaises(ValidationError) as e: + instance.full_clean() + self.assertEqual(len(e.exception.message_dict), 1) + self.assertIn("target_workspace", e.exception.message_dict) + self.assertEqual(len(e.exception.message_dict["target_workspace"]), 1) + self.assertIn( + "target_workspace must be different", + e.exception.message_dict["target_workspace"][0], + ) + + def test_clean_target_workspace_cannot_be_a_data_prep_workspace(self): + """Clean method raises ValidationError when the original_workspace is a data prep workspace.""" + workspace = WorkspaceFactory.create() + target_workspace = factories.DataPrepWorkspaceFactory.create() + user = UserFactory.create() + instance = models.DataPrepWorkspace( + requested_by=user, + workspace=workspace, + target_workspace=target_workspace.workspace, + ) + with self.assertRaises(ValidationError) as e: + instance.full_clean() + self.assertEqual(len(e.exception.message_dict), 1) + self.assertIn("target_workspace", e.exception.message_dict) + self.assertEqual(len(e.exception.message_dict["target_workspace"]), 1) + self.assertIn( + "cannot be a DataPrepWorkspace", + e.exception.message_dict["target_workspace"][0], + ) diff --git a/primed/miscellaneous_workspaces/tests/test_tables.py b/primed/miscellaneous_workspaces/tests/test_tables.py index dd36bc69..992ffcb3 100644 --- a/primed/miscellaneous_workspaces/tests/test_tables.py +++ b/primed/miscellaneous_workspaces/tests/test_tables.py @@ -47,3 +47,24 @@ def test_row_count_with_two_objects(self): self.model_factory.create_batch(2) table = self.table_class(Workspace.objects.all()) self.assertEqual(len(table.rows), 2) + + +class DataPrepWorkspaceTableTest(TestCase): + """Tests for the DataPrepWorkspaceTable table.""" + + model_factory = factories.DataPrepWorkspaceFactory + table_class = tables.DataPrepWorkspaceTable + + def test_row_count_with_no_objects(self): + table = self.table_class(Workspace.objects.filter(workspace_type="data_prep")) + self.assertEqual(len(table.rows), 0) + + def test_row_count_with_one_object(self): + self.model_factory.create() + table = self.table_class(Workspace.objects.filter(workspace_type="data_prep")) + self.assertEqual(len(table.rows), 1) + + def test_row_count_with_two_objects(self): + self.model_factory.create_batch(2) + table = self.table_class(Workspace.objects.filter(workspace_type="data_prep")) + self.assertEqual(len(table.rows), 2) diff --git a/primed/miscellaneous_workspaces/tests/test_views.py b/primed/miscellaneous_workspaces/tests/test_views.py index 1272d501..5f623bb4 100644 --- a/primed/miscellaneous_workspaces/tests/test_views.py +++ b/primed/miscellaneous_workspaces/tests/test_views.py @@ -2,7 +2,10 @@ import responses from anvil_consortium_manager.models import AnVILProjectManagerAccess, Workspace -from anvil_consortium_manager.tests.factories import BillingProjectFactory +from anvil_consortium_manager.tests.factories import ( + BillingProjectFactory, + WorkspaceFactory, +) from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission @@ -27,7 +30,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -52,12 +55,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.requester = UserFactory.create() @@ -67,16 +70,6 @@ def get_url(self, *args): """Get the url for the view being tested.""" return reverse("anvil_consortium_manager:workspaces:new", args=args) - def get_api_url(self, billing_project_name, workspace_name): - """Return the Terra API url for a given billing project and workspace.""" - return ( - self.api_client.rawls_entry_point - + "/api/workspaces/" - + billing_project_name - + "/" - + workspace_name - ) - def test_creates_workspace(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" billing_project = BillingProjectFactory.create(name="test-billing-project") @@ -128,12 +121,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.requester = UserFactory.create() @@ -250,7 +243,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -275,12 +268,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.requester = UserFactory.create() @@ -290,16 +283,6 @@ def get_url(self, *args): """Get the url for the view being tested.""" return reverse("anvil_consortium_manager:workspaces:new", args=args) - def get_api_url(self, billing_project_name, workspace_name): - """Return the Terra API url for a given billing project and workspace.""" - return ( - self.api_client.rawls_entry_point - + "/api/workspaces/" - + billing_project_name - + "/" - + workspace_name - ) - def test_creates_workspace(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" billing_project = BillingProjectFactory.create(name="test-billing-project") @@ -351,12 +334,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.requester = UserFactory.create() @@ -473,7 +456,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -498,12 +481,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.requester = UserFactory.create() @@ -513,16 +496,6 @@ def get_url(self, *args): """Get the url for the view being tested.""" return reverse("anvil_consortium_manager:workspaces:new", args=args) - def get_api_url(self, billing_project_name, workspace_name): - """Return the Terra API url for a given billing project and workspace.""" - return ( - self.api_client.rawls_entry_point - + "/api/workspaces/" - + billing_project_name - + "/" - + workspace_name - ) - def test_creates_workspace(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" billing_project = BillingProjectFactory.create(name="test-billing-project") @@ -574,12 +547,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.requester = UserFactory.create() @@ -696,7 +669,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -721,12 +694,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.workspace_type = "template" @@ -735,16 +708,6 @@ def get_url(self, *args): """Get the url for the view being tested.""" return reverse("anvil_consortium_manager:workspaces:new", args=args) - def get_api_url(self, billing_project_name, workspace_name): - """Return the Terra API url for a given billing project and workspace.""" - return ( - self.api_client.rawls_entry_point - + "/api/workspaces/" - + billing_project_name - + "/" - + workspace_name - ) - def test_creates_workspace(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" billing_project = BillingProjectFactory.create(name="test-billing-project") @@ -797,12 +760,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.workspace_type = "template" @@ -919,7 +882,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -948,12 +911,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.workspace_type = "open_access" @@ -964,16 +927,6 @@ def get_url(self, *args): """Get the url for the view being tested.""" return reverse("anvil_consortium_manager:workspaces:new", args=args) - def get_api_url(self, billing_project_name, workspace_name): - """Return the Terra API url for a given billing project and workspace.""" - return ( - self.api_client.rawls_entry_point - + "/api/workspaces/" - + billing_project_name - + "/" - + workspace_name - ) - def test_creates_workspace(self): """Posting valid data to the form creates a workspace data object when using a custom adapter.""" billing_project = BillingProjectFactory.create(name="test-billing-project") @@ -1031,12 +984,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) self.workspace_type = "open_access" @@ -1149,3 +1102,238 @@ def test_creates_workspace(self): self.assertEqual(new_workspace_data.studies.count(), 1) self.assertIn(self.study, new_workspace_data.studies.all()) self.assertEqual(new_workspace_data.data_source, "test source") + + +class DataPrepWorkspaceDetailTest(TestCase): + """Tests of the WorkspaceDetail view from ACM with this app's DataPrepWorkspace model.""" + + def setUp(self): + """Set up test class.""" + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + obj = factories.DataPrepWorkspaceFactory.create() + self.client.force_login(self.user) + response = self.client.get(obj.workspace.get_absolute_url()) + self.assertEqual(response.status_code, 200) + # Includes link to target workspace. + self.assertContains(response, obj.target_workspace.get_absolute_url()) + + def test_template_active(self): + """Returns successful response code.""" + obj = factories.DataPrepWorkspaceFactory.create() + self.client.force_login(self.user) + response = self.client.get(obj.workspace.get_absolute_url()) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Active") + + def test_template_inactive(self): + """Returns successful response code.""" + obj = factories.DataPrepWorkspaceFactory.create(is_active=False) + self.client.force_login(self.user) + response = self.client.get(obj.workspace.get_absolute_url()) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Inactive") + + +class DataPrepWorkspaceCreateTest(AnVILAPIMockTestMixin, TestCase): + """Tests of the WorkspaceCreate view from ACM with this app's DataPrepWorkspace model.""" + + api_success_code = 201 + + def setUp(self): + """Set up test class.""" + # The superclass uses the responses package to mock API responses. + super().setUp() + # Create a user with both view and edit permissions. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME + ) + ) + self.requester = UserFactory.create() + self.target_workspace = WorkspaceFactory.create() + self.workspace_type = "data_prep" + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:workspaces:new", args=args) + + def test_creates_workspace(self): + """Posting valid data to the form creates a workspace data object when using a custom adapter.""" + billing_project = BillingProjectFactory.create(name="test-billing-project") + url = self.api_client.rawls_entry_point + "/api/workspaces" + json_data = { + "namespace": "test-billing-project", + "name": "test-workspace", + "attributes": {}, + } + self.anvil_response_mock.add( + responses.POST, + url, + status=self.api_success_code, + match=[responses.matchers.json_params_matcher(json_data)], + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.workspace_type), + { + "billing_project": billing_project.pk, + "name": "test-workspace", + # Workspace data form. + "workspacedata-TOTAL_FORMS": 1, + "workspacedata-INITIAL_FORMS": 0, + "workspacedata-MIN_NUM_FORMS": 1, + "workspacedata-MAX_NUM_FORMS": 1, + "workspacedata-0-target_workspace": self.target_workspace.pk, + "workspacedata-0-requested_by": self.requester.pk, + }, + ) + self.assertEqual(response.status_code, 302) + # The workspace is created. + new_workspace = Workspace.objects.latest("pk") + # Workspace data is added. + self.assertEqual(models.DataPrepWorkspace.objects.count(), 1) + new_workspace_data = models.DataPrepWorkspace.objects.latest("pk") + self.assertEqual(new_workspace_data.workspace, new_workspace) + + +class DataPrepWorkspaceImportTest(AnVILAPIMockTestMixin, TestCase): + """Tests of the WorkspaceImport view from ACM with this app's DataPrepWorkspace model.""" + + api_success_code = 200 + + def setUp(self): + """Set up test class.""" + # The superclass uses the responses package to mock API responses. + super().setUp() + # Create a user with both view and edit permissions. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME + ) + ) + self.requester = UserFactory.create() + self.target_workspace = WorkspaceFactory.create() + self.workspace_type = "data_prep" + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:workspaces:import", args=args) + + def get_api_url(self, billing_project_name, workspace_name): + """Return the Terra API url for a given billing project and workspace.""" + return ( + self.api_client.rawls_entry_point + + "/api/workspaces/" + + billing_project_name + + "/" + + workspace_name + ) + + def get_api_json_response( + self, billing_project, workspace, authorization_domains=[], access="OWNER" + ): + """Return a pared down version of the json response from the AnVIL API with only fields we need.""" + json_data = { + "accessLevel": access, + "owners": [], + "workspace": { + "authorizationDomain": [ + {"membersGroupName": x} for x in authorization_domains + ], + "name": workspace, + "namespace": billing_project, + "isLocked": False, + }, + } + return json_data + + def test_creates_workspace(self): + """Posting valid data to the form creates an UploadWorkspace object.""" + billing_project = BillingProjectFactory.create(name="billing-project") + workspace_name = "workspace" + # Available workspaces API call. + workspace_list_url = self.api_client.rawls_entry_point + "/api/workspaces" + self.anvil_response_mock.add( + responses.GET, + workspace_list_url, + match=[ + responses.matchers.query_param_matcher( + {"fields": "workspace.namespace,workspace.name,accessLevel"} + ) + ], + status=200, + json=[self.get_api_json_response(billing_project.name, workspace_name)], + ) + url = self.get_api_url(billing_project.name, workspace_name) + self.anvil_response_mock.add( + responses.GET, + url, + status=self.api_success_code, + json=self.get_api_json_response(billing_project.name, workspace_name), + ) + # ACL API call. + api_url_acl = ( + self.api_client.rawls_entry_point + + "/api/workspaces/" + + billing_project.name + + "/" + + workspace_name + + "/acl" + ) + self.anvil_response_mock.add( + responses.GET, + api_url_acl, + status=200, + json={ + "acl": { + self.service_account_email: { + "accessLevel": "OWNER", + "canCompute": True, + "canShare": True, + "pending": False, + } + } + }, + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.workspace_type), + { + "workspace": billing_project.name + "/" + workspace_name, + # Workspace data form. + "workspacedata-TOTAL_FORMS": 1, + "workspacedata-INITIAL_FORMS": 0, + "workspacedata-MIN_NUM_FORMS": 1, + "workspacedata-MAX_NUM_FORMS": 1, + "workspacedata-0-target_workspace": self.target_workspace.pk, + "workspacedata-0-requested_by": self.requester.pk, + }, + ) + self.assertEqual(response.status_code, 302) + # The workspace is created. + new_workspace = Workspace.objects.latest("pk") + # Workspace data is added. + self.assertEqual(models.DataPrepWorkspace.objects.count(), 1) + new_workspace_data = models.DataPrepWorkspace.objects.latest("pk") + self.assertEqual(new_workspace_data.workspace, new_workspace) diff --git a/primed/primed_anvil/tests/test_views.py b/primed/primed_anvil/tests/test_views.py index 42066720..85d59706 100644 --- a/primed/primed_anvil/tests/test_views.py +++ b/primed/primed_anvil/tests/test_views.py @@ -40,6 +40,53 @@ User = get_user_model() +class ACMNavbarTest(TestCase): + """Tests for the ACM navbar.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + self.model_factory = factories.StudyFactory + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:index", args=args) + + def test_staff_view_links(self): + user = UserFactory.create() + user.user_permissions.add( + Permission.objects.get( + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url()) + self.assertNotContains(response, reverse("primed_anvil:studies:new")) + + def test_staff_edit_links(self): + user = UserFactory.create() + user.user_permissions.add( + Permission.objects.get( + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + user.user_permissions.add( + Permission.objects.get( + codename=acm_models.AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url()) + self.assertContains(response, reverse("primed_anvil:studies:new")) + + class HomeTest(TestCase): """Tests of the home page. This is maybe not the best place to put this test?""" @@ -51,7 +98,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -86,6 +133,36 @@ def test_user_has_not_linked_account(self): response, reverse("anvil_consortium_manager:accounts:link") ) + def test_staff_view_links(self): + user = UserFactory.create() + user.user_permissions.add( + Permission.objects.get( + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url()) + # Note: we need quotes around the link because anvil/accounts/link does appear in the response, + # so we can't test if "anvil/" is in the response. We need to test if '"anvil/"' is in the response. + self.assertContains( + response, '"{}"'.format(reverse("anvil_consortium_manager:index")) + ) + + def test_view_links(self): + user = UserFactory.create() + user.user_permissions.add( + Permission.objects.get( + codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url()) + # Note: we need quotes around the link because anvil/accounts/link does appear in the response, + # so we can't test if "anvil/" is in the response. We need to test if '"anvil/"' is in the response. + self.assertNotContains( + response, '"{}"'.format(reverse("anvil_consortium_manager:index")) + ) + class StudyDetailTest(TestCase): """Tests for the StudyDetail view.""" @@ -98,7 +175,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -150,13 +227,31 @@ def test_status_code_with_limited_view_permission(self): user = User.objects.create_user(username="test-2", password="test-2") user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME ) ) self.client.force_login(user) response = self.client.get(self.get_url(obj.pk)) self.assertEqual(response.status_code, 200) + def test_content_staff_view_permission(self): + obj = self.model_factory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.pk)) + self.assertContains(response, "Date created") + + def test_content_view_permission(self): + obj = self.model_factory.create() + user = User.objects.create_user(username="test-2", password="test-2") + user.user_permissions.add( + Permission.objects.get( + codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url(obj.pk)) + self.assertNotContains(response, "Date created") + def test_table_classes_view_permission(self): obj = self.model_factory.create() self.client.force_login(self.user) @@ -173,7 +268,7 @@ def test_table_classes_limited_view_permission(self): user = User.objects.create_user(username="test-2", password="test-2") user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME ) ) obj = self.model_factory.create() @@ -251,7 +346,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -455,12 +550,12 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) @@ -504,7 +599,7 @@ def test_access_without_user_permission_view(self): ) user_view_perm.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) request = self.factory.get(self.get_url()) @@ -633,7 +728,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -673,7 +768,7 @@ def test_status_code_with_limited_view_permission(self): user = User.objects.create_user(username="test-2", password="test-2") user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.LIMITED_VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME ) ) self.client.force_login(user) @@ -739,7 +834,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -844,7 +939,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -938,7 +1033,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) @@ -995,7 +1090,7 @@ def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=acm_models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + codename=acm_models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME ) ) diff --git a/primed/primed_anvil/views.py b/primed/primed_anvil/views.py index 0d4b5cb3..b4fdb98b 100644 --- a/primed/primed_anvil/views.py +++ b/primed/primed_anvil/views.py @@ -1,6 +1,6 @@ from anvil_consortium_manager.auth import ( - AnVILConsortiumManagerEditRequired, - AnVILConsortiumManagerLimitedViewRequired, + AnVILConsortiumManagerStaffEditRequired, + AnVILConsortiumManagerStaffViewRequired, AnVILConsortiumManagerViewRequired, ) from anvil_consortium_manager.models import AnVILProjectManagerAccess, Workspace @@ -37,9 +37,7 @@ User = get_user_model() -class StudyDetail( - AnVILConsortiumManagerLimitedViewRequired, MultiTableMixin, DetailView -): +class StudyDetail(AnVILConsortiumManagerViewRequired, MultiTableMixin, DetailView): """View to show details about a `Study`.""" model = models.Study @@ -63,7 +61,7 @@ def get_tables(self): ) # Check permissions to determine table type. apm_content_type = ContentType.objects.get_for_model(AnVILProjectManagerAccess) - full_view_perm = f"{apm_content_type.app_label}.{AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME}" + full_view_perm = f"{apm_content_type.app_label}.{AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME}" if self.request.user.has_perm(full_view_perm): return ( dbGaPWorkspaceTable(dbgap_qs), @@ -81,14 +79,16 @@ def get_tables(self): ) -class StudyList(AnVILConsortiumManagerLimitedViewRequired, SingleTableView): +class StudyList(AnVILConsortiumManagerViewRequired, SingleTableView): """View to show a list of `Study`s.""" model = models.Study table_class = tables.StudyTable -class StudyCreate(AnVILConsortiumManagerEditRequired, SuccessMessageMixin, CreateView): +class StudyCreate( + AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, CreateView +): """View to create a new `Study`.""" model = models.Study @@ -100,7 +100,7 @@ def get_success_url(self): class StudyAutocomplete( - AnVILConsortiumManagerViewRequired, autocomplete.Select2QuerySetView + AnVILConsortiumManagerStaffViewRequired, autocomplete.Select2QuerySetView ): """View to provide autocompletion for `Study`s. Match either the `short_name` or `full_name`.""" @@ -124,7 +124,9 @@ def get_queryset(self): return qs -class StudySiteDetail(AnVILConsortiumManagerViewRequired, MultiTableMixin, DetailView): +class StudySiteDetail( + AnVILConsortiumManagerStaffViewRequired, MultiTableMixin, DetailView +): """View to show details about a `StudySite`.""" model = models.StudySite @@ -145,14 +147,14 @@ def get_tables_data(self): return [user_qs, dbgap_qs, cdsa_qs] -class StudySiteList(AnVILConsortiumManagerViewRequired, SingleTableView): +class StudySiteList(AnVILConsortiumManagerStaffViewRequired, SingleTableView): """View to show a list of `StudySite`s.""" model = models.StudySite table_class = tables.StudySiteTable -class AvailableDataList(AnVILConsortiumManagerViewRequired, SingleTableView): +class AvailableDataList(AnVILConsortiumManagerStaffViewRequired, SingleTableView): """View to show a list of `AvailableData`.""" model = models.AvailableData @@ -160,7 +162,7 @@ class AvailableDataList(AnVILConsortiumManagerViewRequired, SingleTableView): class AvailableDataDetail( - AnVILConsortiumManagerViewRequired, SingleTableMixin, DetailView + AnVILConsortiumManagerStaffViewRequired, SingleTableMixin, DetailView ): """View to show details about a `AvailableData`.""" diff --git a/primed/templates/anvil_consortium_manager/navbar.html b/primed/templates/anvil_consortium_manager/navbar.html index 27f8ac29..6dc96662 100644 --- a/primed/templates/anvil_consortium_manager/navbar.html +++ b/primed/templates/anvil_consortium_manager/navbar.html @@ -15,7 +15,7 @@
  • List studies
  • - {% if perms.anvil_consortium_manager.anvil_project_manager_edit %} + {% if perms.anvil_consortium_manager.anvil_consortium_manager_staff_edit %}
  • Add a new study
  • diff --git a/primed/templates/base.html b/primed/templates/base.html index 9a696dd6..c6b0a9aa 100644 --- a/primed/templates/base.html +++ b/primed/templates/base.html @@ -96,7 +96,7 @@ - {% if perms.anvil_consortium_manager.anvil_project_manager_view %} + {% if perms.anvil_consortium_manager.anvil_consortium_manager_staff_view %} diff --git a/primed/templates/cdsa/nav_items.html b/primed/templates/cdsa/nav_items.html index 13e26408..04081cc1 100644 --- a/primed/templates/cdsa/nav_items.html +++ b/primed/templates/cdsa/nav_items.html @@ -25,7 +25,7 @@
  • - {% if perms.anvil_consortium_manager.anvil_project_manager_edit %} + {% if perms.anvil_consortium_manager.anvil_consortium_manager_staff_edit %}
  • Member diff --git a/primed/templates/dbgap/dbgapapplication_detail.html b/primed/templates/dbgap/dbgapapplication_detail.html index 7fe61e84..d1fa63d6 100644 --- a/primed/templates/dbgap/dbgapapplication_detail.html +++ b/primed/templates/dbgap/dbgapapplication_detail.html @@ -51,7 +51,7 @@

    {% block action_buttons %}

    - {% if perms.anvil_consortium_manager.anvil_project_manager_edit %} + {% if perms.anvil_consortium_manager.anvil_consortium_manager_staff_edit %} Update data access requests {% endif %} Audit workspace access diff --git a/primed/templates/dbgap/dbgapapplication_records.html b/primed/templates/dbgap/dbgapapplication_records.html new file mode 100644 index 00000000..89cd6019 --- /dev/null +++ b/primed/templates/dbgap/dbgapapplication_records.html @@ -0,0 +1,15 @@ +{% extends "anvil_consortium_manager/base.html" %} +{% load render_table from django_tables2 %} + +{% block title %}dbGaP application records{% endblock %} + +{% block content %} +

    dbGaP application records

    + +
    +

    The following table shows the list of PRIMED coordinated dbGaP applications that the Coordinating Center is tracking as of {% now "SHORT_DATETIME_FORMAT" %}.

    +
    + +{% render_table table %} + +{% endblock content %} diff --git a/primed/templates/dbgap/nav_items.html b/primed/templates/dbgap/nav_items.html index 64d9e009..b35bf5c1 100644 --- a/primed/templates/dbgap/nav_items.html +++ b/primed/templates/dbgap/nav_items.html @@ -6,7 +6,7 @@
  • List dbGaP study accessions
  • - {% if perms.anvil_consortium_manager.anvil_project_manager_edit %} + {% if perms.anvil_consortium_manager.anvil_consortium_manager_staff_edit %}
  • Add a new dbGaP study accession
  • @@ -17,7 +17,7 @@
  • List dbGaP applications
  • - {% if perms.anvil_consortium_manager.anvil_project_manager_edit %} + {% if perms.anvil_consortium_manager.anvil_consortium_manager_staff_edit %}
  • Add a new dbGaP application
  • diff --git a/primed/templates/dbgap/records_index.html b/primed/templates/dbgap/records_index.html new file mode 100644 index 00000000..23d72e8e --- /dev/null +++ b/primed/templates/dbgap/records_index.html @@ -0,0 +1,27 @@ +{% extends "anvil_consortium_manager/base.html" %} + +{% block title %}dbGaP records{% endblock %} + +{% block content %} +

    dbGaP records

    + + +
    + +
    +
    +

    +
    +
    +

    dbGaP coordinated applications

    +

    View dbGaP coordinated applications for PRIMED.

    + + View applications + +
    +
    + +
    + + +{% endblock content %} diff --git a/primed/templates/miscellaneous_workspaces/dataprepworkspace_detail.html b/primed/templates/miscellaneous_workspaces/dataprepworkspace_detail.html new file mode 100644 index 00000000..7ff1c45a --- /dev/null +++ b/primed/templates/miscellaneous_workspaces/dataprepworkspace_detail.html @@ -0,0 +1,20 @@ +{% extends "anvil_consortium_manager/workspace_detail.html" %} + +{% block workspace_data %} +
    +
    +
    Target workspace
    + {{ object.dataprepworkspace.target_workspace }} +
    +
    Target type
    + {{ object.dataprepworkspace.target_workspace.workspace_type }} +
    +
    Status
    + {% if object.dataprepworkspace.is_active %} + Active + {% else %} + Inactive + {% endif %} +
    +
    +{% endblock workspace_data %} diff --git a/primed/templates/primed_anvil/study_detail.html b/primed/templates/primed_anvil/study_detail.html index 313fcdf7..998f2f33 100644 --- a/primed/templates/primed_anvil/study_detail.html +++ b/primed/templates/primed_anvil/study_detail.html @@ -8,7 +8,7 @@
    Short name
    {{ object.short_name }}
    Full name
    {{ object.full_name }}
    - {% if perms.anvil_consortium_manager.anvil_project_manager_view %} + {% if perms.anvil_consortium_manager.anvil_consortium_manager_staff_view %}
    Date created
    {{ object.created }}
    Date modified
    {{ object.modified }}
    {% endif %} diff --git a/primed/templates/users/user_detail.html b/primed/templates/users/user_detail.html index d80f6b94..c58c5171 100644 --- a/primed/templates/users/user_detail.html +++ b/primed/templates/users/user_detail.html @@ -59,7 +59,7 @@

    {% if object == request.user %}My{% else %}