diff --git a/.github/dependabot.yml b/.github/dependabot.yml index aa99d491..750edb2f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,8 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" + day: "sunday" # Enable version updates for Python/Pip - Production - package-ecosystem: "pip" @@ -13,4 +14,10 @@ updates: directory: "/" # Check for updates to GitHub Actions every weekday schedule: - interval: "daily" + interval: "weekly" + day: "sunday" + allow: + # Allow both direct and indirect updates for all packages + - dependency-type: "all" + # Allow up to 10 dependencies for pip dependencies + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b859332..dfd9c76e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,10 @@ jobs: steps: - name: Checkout Code Repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4 - name: Set up Python 3.9 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 cache: pip @@ -67,10 +67,10 @@ jobs: steps: - name: Checkout Code Repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip @@ -91,9 +91,9 @@ jobs: ANVIL_API_SERVICE_ACCOUNT_FILE: foo - name: Upload coverage data - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage-data + name: coverage-data-mysql-${{ strategy.job-index }} path: .coverage.* pytest-sqlite: @@ -101,10 +101,10 @@ jobs: steps: - name: Checkout Code Repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4 - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 cache: pip @@ -125,9 +125,9 @@ jobs: ANVIL_API_SERVICE_ACCOUNT_FILE: foo - name: Upload coverage data - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage-data + name: coverage-data-sqlite-${{ strategy.job-index }} path: .coverage.* coverage: @@ -137,10 +137,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the repo - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' @@ -148,10 +148,16 @@ jobs: run: | python -m pip install --upgrade pip pip install --upgrade coverage "django<4" django-coverage-plugin + - name: Download coverage data - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: coverage-data + path: ./artifacts/ + + - name: Merge coverage files + run: | + mv ./artifacts/coverage-data*/.coverage* . + ls -la .coverage* - name: Combine coverage data run: | diff --git a/.github/workflows/combine-prs.yml b/.github/workflows/combine-prs.yml index e7776e42..ef94fe7d 100644 --- a/.github/workflows/combine-prs.yml +++ b/.github/workflows/combine-prs.yml @@ -95,7 +95,7 @@ jobs: console.log('Combined: ' + combined); return combined # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v4 with: fetch-depth: 0 # Creates a branch with other PR branches merged together diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml new file mode 100644 index 00000000..9efde5a6 --- /dev/null +++ b/.github/workflows/gitleaks.yml @@ -0,0 +1,19 @@ +name: gitleaks +on: + pull_request: + push: + workflow_dispatch: + schedule: + - cron: "0 4 * * *" # run once a day at 4 AM +jobs: + scan: + name: gitleaks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE}} # Only required for Organizations, not personal accounts. diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000..4756f210 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,4 @@ +2f8573f44abd55cd30367f673bbbcb4ebc4861bd:test.secrets:generic-api-key:1 +2f8573f44abd55cd30367f673bbbcb4ebc4861bd:test.secrets:generic-api-key:2 +faa490c4de15c23401525ebf6ad532e2bd724e5e:config/settings/base.py:generic-api-key:83 +faa490c4de15c23401525ebf6ad532e2bd724e5e:config/settings/base.py:generic-api-key:84 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40b838ae..842469d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,6 +27,11 @@ repos: args: ['--config=setup.cfg'] additional_dependencies: [flake8-isort] + - repo: https://github.com/gitleaks/gitleaks + rev: v8.16.1 + hooks: + - id: gitleaks + # sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date ci: diff --git a/config/settings/base.py b/config/settings/base.py index fa97cc5a..7299a4e9 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -32,8 +32,6 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n USE_I18N = True # https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n -USE_L10N = True -# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz USE_TZ = True # https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths LOCALE_PATHS = [str(ROOT_DIR / "locale")] @@ -214,7 +212,9 @@ FORM_RENDERER = "django.forms.renderers.TemplatesSetting" # http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs -CRISPY_TEMPLATE_PACK = "bootstrap4" +# https://github.com/django-crispy-forms/crispy-bootstrap5 +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" +CRISPY_TEMPLATE_PACK = "bootstrap5" # FIXTURES # ------------------------------------------------------------------------------ @@ -370,14 +370,14 @@ ANVIL_API_SERVICE_ACCOUNT_FILE = env("ANVIL_API_SERVICE_ACCOUNT_FILE") # Specify workspace adapters. ANVIL_WORKSPACE_ADAPTERS = [ - "primed.miscellaneous_workspaces.adapters.TemplateWorkspaceAdapter", "primed.dbgap.adapters.dbGaPWorkspaceAdapter", "primed.cdsa.adapters.CDSAWorkspaceAdapter", + "primed.miscellaneous_workspaces.adapters.OpenAccessWorkspaceAdapter", + "primed.miscellaneous_workspaces.adapters.SimulatedDataWorkspaceAdapter", + "primed.miscellaneous_workspaces.adapters.ResourceWorkspaceAdapter", "primed.miscellaneous_workspaces.adapters.ConsortiumDevelWorkspaceAdapter", + "primed.miscellaneous_workspaces.adapters.TemplateWorkspaceAdapter", "primed.miscellaneous_workspaces.adapters.DataPrepWorkspaceAdapter", - "primed.miscellaneous_workspaces.adapters.ResourceWorkspaceAdapter", - "primed.miscellaneous_workspaces.adapters.SimulatedDataWorkspaceAdapter", - "primed.miscellaneous_workspaces.adapters.OpenAccessWorkspaceAdapter", ] ANVIL_ACCOUNT_ADAPTER = "primed.primed_anvil.adapters.AccountAdapter" @@ -386,4 +386,3 @@ # Specify the subject for AnVIL account verification emails. ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = "Verify your AnVIL account email" ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL = "primedconsortium@uw.edu" -ANVIL_CDSA_GROUP_NAME = "PRIMED_CDSA" diff --git a/config/settings/local.py b/config/settings/local.py index b6b17daa..789b1f6d 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -77,3 +77,6 @@ "ANVIL_DATA_ACCESS_GROUP_PREFIX", default="DEV_PRIMED" ) ANVIL_CDSA_GROUP_NAME = env("ANVIL_CDSA_GROUP_NAME", default="DEV_PRIMED_CDSA") +ANVIL_CC_ADMINS_GROUP_NAME = env( + "ANVIL_CC_ADMINS_GROUP_NAME", default="DEV_PRIMED_CC_ADMINS" +) diff --git a/config/settings/production.py b/config/settings/production.py index 3fd30c10..311aa3e8 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -168,3 +168,5 @@ # ANVIL_DBGAP_APPLICATION_GROUP_PREFIX = "PRIMED_DBGAP_ACCESS" # ANVIL_CDSA_GROUP_PREFIX = "PRIMED_CDSA_ACCESS" ANVIL_DATA_ACCESS_GROUP_PREFIX = "PRIMED" +ANVIL_CC_ADMINS_GROUP_NAME = "PRIMED_CC_ADMINS" +ANVIL_CDSA_GROUP_NAME = "PRIMED_CDSA" diff --git a/config/settings/test.py b/config/settings/test.py index 4e47bddf..190e71d7 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -30,6 +30,7 @@ ANVIL_API_SERVICE_ACCOUNT_FILE = "foo" ANVIL_DATA_ACCESS_GROUP_PREFIX = "TEST_PRIMED" ANVIL_CDSA_GROUP_NAME = "TEST_PRIMED_CDSA" +ANVIL_CC_ADMINS_GROUP_NAME = "TEST_PRIMED_CC_ADMINS" # template tests require debug to be set # get the last templates entry and set debug option diff --git a/primed/cdsa/adapters.py b/primed/cdsa/adapters.py index ea87a984..e4409ddd 100644 --- a/primed/cdsa/adapters.py +++ b/primed/cdsa/adapters.py @@ -10,9 +10,10 @@ class CDSAWorkspaceAdapter(BaseWorkspaceAdapter): type = "cdsa" name = "CDSA workspace" description = ( - "Workspaces containing data from the Consortium Data Sharing Agreement." + "Workspaces containing data from the Consortium Data Sharing Agreement" ) - list_table_class = tables.CDSAWorkspaceTable + list_table_class_staff_view = tables.CDSAWorkspaceStaffTable + list_table_class_view = tables.CDSAWorkspaceUserTable workspace_form_class = WorkspaceForm workspace_data_model = models.CDSAWorkspace workspace_data_form_class = forms.CDSAWorkspaceForm diff --git a/primed/cdsa/migrations/0010_alter_historicalsignedagreement_status_and_more.py b/primed/cdsa/migrations/0010_alter_historicalsignedagreement_status_and_more.py new file mode 100644 index 00000000..c6d18372 --- /dev/null +++ b/primed/cdsa/migrations/0010_alter_historicalsignedagreement_status_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.7 on 2023-12-04 23:27 + +from django.db import migrations +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("cdsa", "0009_signedagreement_add_field_status"), + ] + + operations = [ + migrations.AlterField( + model_name="historicalsignedagreement", + name="status", + field=model_utils.fields.StatusField( + choices=[ + ("active", "Active"), + ("withdrawn", "Withdrawn"), + ("lapsed", "Lapsed"), + ], + default="active", + max_length=100, + no_check_for_status=True, + verbose_name="status", + ), + ), + migrations.AlterField( + model_name="signedagreement", + name="status", + field=model_utils.fields.StatusField( + choices=[ + ("active", "Active"), + ("withdrawn", "Withdrawn"), + ("lapsed", "Lapsed"), + ], + default="active", + max_length=100, + no_check_for_status=True, + verbose_name="status", + ), + ), + ] diff --git a/primed/cdsa/migrations/0011_signedagreement_add_replaced_status.py b/primed/cdsa/migrations/0011_signedagreement_add_replaced_status.py new file mode 100644 index 00000000..f1c1bb06 --- /dev/null +++ b/primed/cdsa/migrations/0011_signedagreement_add_replaced_status.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.8 on 2024-01-18 23:49 + +from django.db import migrations +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("cdsa", "0010_alter_historicalsignedagreement_status_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="historicalsignedagreement", + name="status", + field=model_utils.fields.StatusField( + choices=[ + ("active", "Active"), + ("withdrawn", "Withdrawn"), + ("lapsed", "Lapsed"), + ("replaced", "Replaced"), + ], + default="active", + max_length=100, + no_check_for_status=True, + verbose_name="status", + ), + ), + migrations.AlterField( + model_name="signedagreement", + name="status", + field=model_utils.fields.StatusField( + choices=[ + ("active", "Active"), + ("withdrawn", "Withdrawn"), + ("lapsed", "Lapsed"), + ("replaced", "Replaced"), + ], + default="active", + max_length=100, + no_check_for_status=True, + verbose_name="status", + ), + ), + ] diff --git a/primed/cdsa/models.py b/primed/cdsa/models.py index 5dd8a053..a2b1e0dc 100644 --- a/primed/cdsa/models.py +++ b/primed/cdsa/models.py @@ -99,14 +99,17 @@ class SignedAgreementStatusMixin: class StatusChoices(models.TextChoices): ACTIVE = "active", "Active" - """SignedAgreements that are currently active.""" + """SignedAgreements that are currently active.""" # pragma: no cover WITHDRAWN = "withdrawn", "Withdrawn" """SignedAgreements that have been withdrawn for some reason (e.g., PI changed institution, study no longer wanted to participate.)""" LAPSED = "lapsed", "Lapsed" - """SignedAgreements from a AgreementMajorVersion that is no longer valid.""" + """SignedAgreements from a AgreementMajorVersion that is no longer valid.""" # pragma: no cover + + REPLACED = "replaced", "Replaced" + """SignedAgreements that have been replaced by a newer version.""" # pragma: no cover STATUS = StatusChoices.choices diff --git a/primed/cdsa/tables.py b/primed/cdsa/tables.py index 66b734da..3646f915 100644 --- a/primed/cdsa/tables.py +++ b/primed/cdsa/tables.py @@ -295,7 +295,7 @@ def render_date_shared(self, record): return "—" -class CDSAWorkspaceTable(tables.Table): +class CDSAWorkspaceStaffTable(tables.Table): """A table for the CDSAWorkspace model.""" name = tables.Column(linkify=True) @@ -324,10 +324,10 @@ class Meta: order_by = ("name",) -class CDSAWorkspaceLimitedViewTable(tables.Table): +class CDSAWorkspaceUserTable(tables.Table): """A table for the CDSAWorkspace model.""" - name = tables.Column() + name = tables.Column(linkify=True) billing_project = tables.Column() cdsaworkspace__data_use_permission__abbreviation = tables.Column( verbose_name="DUO permission", diff --git a/primed/cdsa/tests/factories.py b/primed/cdsa/tests/factories.py index bf0459b8..e71482ef 100644 --- a/primed/cdsa/tests/factories.py +++ b/primed/cdsa/tests/factories.py @@ -126,3 +126,4 @@ def authorization_domains(self, create, extracted, **kwargs): class Meta: model = models.CDSAWorkspace + skip_postgeneration_save = True diff --git a/primed/cdsa/tests/test_models.py b/primed/cdsa/tests/test_models.py index 32b35cd2..f39ba093 100644 --- a/primed/cdsa/tests/test_models.py +++ b/primed/cdsa/tests/test_models.py @@ -304,6 +304,11 @@ def test_status_field(self): ) self.assertEqual(instance.status, instance.StatusChoices.LAPSED) instance.full_clean() + instance = factories.SignedAgreementFactory.create( + status=models.SignedAgreement.StatusChoices.REPLACED + ) + self.assertEqual(instance.status, instance.StatusChoices.REPLACED) + instance.full_clean() # not allowed instance = factories.SignedAgreementFactory.create(status="foo") diff --git a/primed/cdsa/tests/test_tables.py b/primed/cdsa/tests/test_tables.py index 82b7b7dc..c7c8c9de 100644 --- a/primed/cdsa/tests/test_tables.py +++ b/primed/cdsa/tests/test_tables.py @@ -447,12 +447,12 @@ def test_ordering(self): self.assertEqual(table.data[1], instance_1) -class CDSAWorkspaceTableTest(TestCase): - """Tests for the CDSAWorkspaceTable class.""" +class CDSAWorkspaceStaffTableTest(TestCase): + """Tests for the CDSAWorkspaceStaffTable class.""" model = Workspace model_factory = factories.CDSAWorkspaceFactory - table_class = tables.CDSAWorkspaceTable + table_class = tables.CDSAWorkspaceStaffTable def test_row_count_with_no_objects(self): table = self.table_class(self.model.objects.all()) @@ -477,12 +477,12 @@ def test_ordering(self): self.assertEqual(table.data[1], instance_1.workspace) -class CDSAWorkspaceLimitedViewTableTest(TestCase): - """Tests for the CDSAWorkspaceLimitedViewTable class.""" +class CDSAWorkspaceUserTableTest(TestCase): + """Tests for the CDSAWorkspaceUserTable class.""" model = Workspace model_factory = factories.CDSAWorkspaceFactory - table_class = tables.CDSAWorkspaceLimitedViewTable + table_class = tables.CDSAWorkspaceUserTable def test_row_count_with_no_objects(self): table = self.table_class(self.model.objects.all()) diff --git a/primed/cdsa/tests/test_views.py b/primed/cdsa/tests/test_views.py index 86deb9d8..2302edb8 100644 --- a/primed/cdsa/tests/test_views.py +++ b/primed/cdsa/tests/test_views.py @@ -5,6 +5,7 @@ import responses from anvil_consortium_manager.models import ( AnVILProjectManagerAccess, + GroupGroupMembership, ManagedGroup, Workspace, ) @@ -547,6 +548,10 @@ def test_only_sets_active_signed_agreements_to_lapsed(self): version__major_version=instance, status=models.SignedAgreement.StatusChoices.LAPSED, ) + replaced_agreement = factories.SignedAgreementFactory.create( + version__major_version=instance, + status=models.SignedAgreement.StatusChoices.REPLACED, + ) self.client.force_login(self.user) response = self.client.post(self.get_url(instance.version), {}) self.assertEqual(response.status_code, 302) @@ -558,6 +563,10 @@ def test_only_sets_active_signed_agreements_to_lapsed(self): self.assertEqual( withdrawn_agreement.status, models.SignedAgreement.StatusChoices.WITHDRAWN ) + replaced_agreement.refresh_from_db() + self.assertEqual( + replaced_agreement.status, models.SignedAgreement.StatusChoices.REPLACED + ) def test_only_sets_associated_signed_agreements_to_lapsed(self): """Does not set SignedAgreements associated with a different version to LAPSED.""" @@ -1383,6 +1392,8 @@ def setUp(self): codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) + # Create the admins group. + self.cc_admins_group = ManagedGroupFactory.create(name="TEST_PRIMED_CC_ADMINS") def get_url(self, *args): """Get the url for the view being tested.""" @@ -1462,6 +1473,13 @@ def test_can_create_object(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -1519,6 +1537,13 @@ def test_redirect_url(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -1553,6 +1578,13 @@ def test_success_message(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -2063,6 +2095,13 @@ def test_creates_anvil_access_group(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -2082,15 +2121,20 @@ def test_creates_anvil_access_group(self): ) self.assertEqual(response.status_code, 302) new_object = models.SignedAgreement.objects.latest("pk") - self.assertEqual(ManagedGroup.objects.count(), 1) + self.assertEqual(ManagedGroup.objects.count(), 2) # A new group was created. new_group = ManagedGroup.objects.latest("pk") self.assertEqual(new_object.anvil_access_group, new_group) self.assertEqual(new_group.name, "TEST_PRIMED_CDSA_ACCESS_2345") self.assertTrue(new_group.is_managed_by_app) + # A group-group membership was created with PRIMED_CC_ADMINS as an admin of the access group. + new_membership = GroupGroupMembership.objects.get( + parent_group=new_object.anvil_access_group, child_group=self.cc_admins_group + ) + self.assertEqual(new_membership.role, GroupGroupMembership.ADMIN) @override_settings(ANVIL_DATA_ACCESS_GROUP_PREFIX="foo") - def test_creates_anvil_groups_different_setting(self): + def test_creates_anvil_groups_different_setting_access_group_prefix(self): """View creates a managed group upon when form is valid.""" self.client.force_login(self.user) representative = UserFactory.create() @@ -2102,6 +2146,13 @@ def test_creates_anvil_groups_different_setting(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/foo_CDSA_ACCESS_2345/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -2121,13 +2172,60 @@ def test_creates_anvil_groups_different_setting(self): ) self.assertEqual(response.status_code, 302) new_object = models.SignedAgreement.objects.latest("pk") - self.assertEqual(ManagedGroup.objects.count(), 1) + self.assertEqual(ManagedGroup.objects.count(), 2) # A new group was created. new_group = ManagedGroup.objects.latest("pk") self.assertEqual(new_object.anvil_access_group, new_group) self.assertEqual(new_group.name, "foo_CDSA_ACCESS_2345") self.assertTrue(new_group.is_managed_by_app) + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foo") + def test_creates_anvil_groups_different_setting_cc_admins_group_name(self): + """View creates a managed group upon when form is valid.""" + admin_group = ManagedGroupFactory.create(name="foo", email="foo@firecloud.org") + self.client.force_login(self.user) + representative = UserFactory.create() + agreement_version = factories.AgreementVersionFactory.create() + study_site = StudySiteFactory.create() + api_url = ( + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345" + ) + self.anvil_response_mock.add( + responses.POST, api_url, status=201, json={"message": "mock message"} + ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345/admin/foo@firecloud.org", + status=204, + ) + response = self.client.post( + self.get_url(), + { + "cc_id": 2345, + "representative": representative.pk, + "representative_role": "Test role", + "signing_institution": "Test institution", + "version": agreement_version.pk, + "date_signed": "2023-01-01", + "is_primary": True, + "agreementtype-TOTAL_FORMS": 1, + "agreementtype-INITIAL_FORMS": 0, + "agreementtype-MIN_NUM_FORMS": 1, + "agreementtype-MAX_NUM_FORMS": 1, + "agreementtype-0-study_site": study_site.pk, + }, + ) + self.assertEqual(response.status_code, 302) + new_object = models.SignedAgreement.objects.latest("pk") + membership = GroupGroupMembership.objects.get( + parent_group=new_object.anvil_access_group, + child_group=admin_group, + ) + self.assertEqual(membership.role, GroupGroupMembership.ADMIN) + def test_manage_group_create_api_error(self): """Nothing is created when the form is valid but there is an API error when creating the group.""" self.client.force_login(self.user) @@ -2171,7 +2269,7 @@ def test_manage_group_create_api_error(self): # No objects were created. self.assertEqual(models.SignedAgreement.objects.count(), 0) self.assertEqual(models.MemberAgreement.objects.count(), 0) - self.assertEqual(ManagedGroup.objects.count(), 0) + self.assertEqual(ManagedGroup.objects.count(), 1) # Just the admins group. def test_managed_group_already_exists_in_app(self): """No objects are created if the managed group already exists in the app.""" @@ -2211,6 +2309,57 @@ def test_managed_group_already_exists_in_app(self): # No dbGaPApplication was created. self.assertEqual(models.SignedAgreement.objects.count(), 0) + def test_admin_group_membership_api_error(self): + """Nothing is created when the form is valid but there is an API error when creating admin group membership.""" + representative = UserFactory.create() + agreement_version = factories.AgreementVersionFactory.create() + study_site = StudySiteFactory.create() + api_url = ( + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345" + ) + self.anvil_response_mock.add( + responses.POST, api_url, status=201, json={"message": "mock message"} + ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + api_url + "/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=400, + json={"message": "other error"}, + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(), + { + "cc_id": 2345, + "representative": representative.pk, + "representative_role": "Test role", + "signing_institution": "Test institution", + "version": agreement_version.pk, + "date_signed": "2023-01-01", + "is_primary": True, + "agreementtype-TOTAL_FORMS": 1, + "agreementtype-INITIAL_FORMS": 0, + "agreementtype-MIN_NUM_FORMS": 1, + "agreementtype-MAX_NUM_FORMS": 1, + "agreementtype-0-study_site": study_site.pk, + }, + ) + self.assertEqual(response.status_code, 200) + # The form is valid... + form = response.context["form"] + self.assertTrue(form.is_valid()) + formset = response.context["formset"] + self.assertTrue(formset.is_valid()) + # ...but there was some error from the API. + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual("AnVIL API Error: other error", str(messages[0])) + # No objects were created. + self.assertEqual(models.MemberAgreement.objects.count(), 0) + self.assertEqual(ManagedGroup.objects.count(), 1) # Just the admin group. + class MemberAgreementDetailTest(TestCase): """Tests for the MemberAgreementDetail view.""" @@ -2450,6 +2599,8 @@ def setUp(self): codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) + # Create the admins group. + self.cc_admins_group = ManagedGroupFactory.create(name="TEST_PRIMED_CC_ADMINS") def get_url(self, *args): """Get the url for the view being tested.""" @@ -2535,6 +2686,19 @@ def test_can_create_object(self): status=201, json={"message": "mock message"}, ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -2565,7 +2729,6 @@ def test_can_create_object(self): # Type was set correctly. self.assertEqual(new_agreement.type, new_agreement.DATA_AFFILIATE) # AnVIL group was set correctly. - self.assertEqual(ManagedGroup.objects.count(), 2) self.assertIsInstance(new_agreement.anvil_access_group, ManagedGroup) self.assertEqual( new_agreement.anvil_access_group.name, "TEST_PRIMED_CDSA_ACCESS_1234" @@ -2604,6 +2767,19 @@ def test_redirect_url(self): status=201, json={"message": "mock message"}, ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -2645,6 +2821,19 @@ def test_success_message(self): status=201, json={"message": "mock message"}, ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -3164,6 +3353,18 @@ def test_creates_anvil_groups(self): status=201, json={"message": "mock message"}, ) + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_2345/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -3183,8 +3384,6 @@ def test_creates_anvil_groups(self): ) self.assertEqual(response.status_code, 302) new_object = models.SignedAgreement.objects.latest("pk") - # An upload group and an access group - self.assertEqual(ManagedGroup.objects.count(), 2) # An access group was created. self.assertEqual( new_object.anvil_access_group.name, "TEST_PRIMED_CDSA_ACCESS_2345" @@ -3198,6 +3397,16 @@ def test_creates_anvil_groups(self): self.assertTrue( new_object.dataaffiliateagreement.anvil_upload_group.is_managed_by_app ) + # Group-group memberships was created with PRIMED_CC_ADMINS as an admin of the access/uploader group. + new_membership_1 = GroupGroupMembership.objects.get( + parent_group=new_object.anvil_access_group, child_group=self.cc_admins_group + ) + self.assertEqual(new_membership_1.role, GroupGroupMembership.ADMIN) + new_membership_2 = GroupGroupMembership.objects.get( + parent_group=new_object.dataaffiliateagreement.anvil_upload_group, + child_group=self.cc_admins_group, + ) + self.assertEqual(new_membership_2.role, GroupGroupMembership.ADMIN) @override_settings(ANVIL_DATA_ACCESS_GROUP_PREFIX="foo") def test_creates_anvil_access_group_different_setting(self): @@ -3218,6 +3427,19 @@ def test_creates_anvil_access_group_different_setting(self): status=201, json={"message": "mock message"}, ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/foo_CDSA_ACCESS_2345/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/foo_CDSA_UPLOAD_2345/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -3237,7 +3459,7 @@ def test_creates_anvil_access_group_different_setting(self): ) self.assertEqual(response.status_code, 302) new_object = models.SignedAgreement.objects.latest("pk") - self.assertEqual(ManagedGroup.objects.count(), 2) + self.assertEqual(ManagedGroup.objects.count(), 3) # A new group was created. self.assertEqual(new_object.anvil_access_group.name, "foo_CDSA_ACCESS_2345") self.assertTrue(new_object.anvil_access_group.is_managed_by_app) @@ -3250,6 +3472,71 @@ def test_creates_anvil_access_group_different_setting(self): new_object.dataaffiliateagreement.anvil_upload_group.is_managed_by_app ) + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foo") + def test_creates_anvil_groups_different_setting_cc_admins_group_name(self): + """View creates a managed group upon when form is valid.""" + admin_group = ManagedGroup.objects.create(name="foo", email="foo@firecloud.org") + self.client.force_login(self.user) + representative = UserFactory.create() + agreement_version = factories.AgreementVersionFactory.create() + study = StudyFactory.create() + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345", + status=201, + json={"message": "mock message"}, + ) + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_2345", + status=201, + json={"message": "mock message"}, + ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345/admin/foo@firecloud.org", + status=204, + ) + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_2345/admin/foo@firecloud.org", + status=204, + ) + response = self.client.post( + self.get_url(), + { + "cc_id": 2345, + "representative": representative.pk, + "representative_role": "Test role", + "signing_institution": "Test institution", + "version": agreement_version.pk, + "date_signed": "2023-01-01", + "is_primary": True, + "agreementtype-TOTAL_FORMS": 1, + "agreementtype-INITIAL_FORMS": 0, + "agreementtype-MIN_NUM_FORMS": 1, + "agreementtype-MAX_NUM_FORMS": 1, + "agreementtype-0-study": study.pk, + }, + ) + self.assertEqual(response.status_code, 302) + new_object = models.SignedAgreement.objects.latest("pk") + membership_1 = GroupGroupMembership.objects.get( + parent_group=new_object.anvil_access_group, + child_group=admin_group, + ) + self.assertEqual(membership_1.role, GroupGroupMembership.ADMIN) + membership_2 = GroupGroupMembership.objects.get( + parent_group=new_object.dataaffiliateagreement.anvil_upload_group, + child_group=admin_group, + ) + self.assertEqual(membership_2.role, GroupGroupMembership.ADMIN) + def test_access_group_create_api_error(self): """Nothing is created when the form is valid but there is an API error when creating the group.""" self.client.force_login(self.user) @@ -3294,7 +3581,7 @@ def test_access_group_create_api_error(self): # No objects were created. self.assertEqual(models.SignedAgreement.objects.count(), 0) self.assertEqual(models.DataAffiliateAgreement.objects.count(), 0) - self.assertEqual(ManagedGroup.objects.count(), 0) + self.assertEqual(ManagedGroup.objects.count(), 1) # Just the admins group. def test_upload_group_create_api_error(self): """Nothing is created when the form is valid but there is an API error when creating the group.""" @@ -3310,6 +3597,13 @@ def test_upload_group_create_api_error(self): status=201, json={"message": "mock message"}, ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) self.anvil_response_mock.add( responses.POST, self.api_client.sam_entry_point @@ -3347,7 +3641,7 @@ def test_upload_group_create_api_error(self): # No objects were created. self.assertEqual(models.SignedAgreement.objects.count(), 0) self.assertEqual(models.DataAffiliateAgreement.objects.count(), 0) - self.assertEqual(ManagedGroup.objects.count(), 0) + self.assertEqual(ManagedGroup.objects.count(), 1) # Just the admins group. def test_access_group_already_exists_in_app(self): """No objects are created if the managed group already exists in the app.""" @@ -3423,6 +3717,138 @@ def test_upload_group_already_exists_in_app(self): ) self.assertEqual(models.SignedAgreement.objects.count(), 0) + def test_admin_group_membership_access_api_error(self): + """Nothing is created when the form is valid but there is an API error when creating admin group membership.""" + self.client.force_login(self.user) + representative = UserFactory.create() + agreement_version = factories.AgreementVersionFactory.create() + study = StudyFactory.create() + # API response to create the associated anvil_access_group. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234", + status=201, + json={"message": "mock message"}, + ) + # self.anvil_response_mock.add( + # responses.POST, + # self.api_client.sam_entry_point + # + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234", + # status=201, + # json={"message": "mock message"}, + # ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=400, + ) + # self.anvil_response_mock.add( + # responses.PUT, + # self.api_client.sam_entry_point + # + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + # status=204, + # ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(), + { + "cc_id": 1234, + "representative": representative.pk, + "representative_role": "Test role", + "signing_institution": "Test institution", + "version": agreement_version.pk, + "date_signed": "2023-01-01", + "is_primary": True, + "agreementtype-TOTAL_FORMS": 1, + "agreementtype-INITIAL_FORMS": 0, + "agreementtype-MIN_NUM_FORMS": 1, + "agreementtype-MAX_NUM_FORMS": 1, + "agreementtype-0-study": study.pk, + }, + ) + self.assertEqual(response.status_code, 200) + # The form is valid... + form = response.context["form"] + self.assertTrue(form.is_valid()) + formset = response.context["formset"] + self.assertTrue(formset.is_valid()) + # ...but there was some error from the API. + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual("AnVIL API Error: other error", str(messages[0])) + # No objects were created. + self.assertEqual(models.DataAffiliateAgreement.objects.count(), 0) + self.assertEqual(ManagedGroup.objects.count(), 1) # Just the admin group. + + def test_admin_group_membership_upload_api_error(self): + """Nothing is created when the form is valid but there is an API error when creating admin group membership.""" + self.client.force_login(self.user) + representative = UserFactory.create() + agreement_version = factories.AgreementVersionFactory.create() + study = StudyFactory.create() + # API response to create the associated anvil_access_group. + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234", + status=201, + json={"message": "mock message"}, + ) + self.anvil_response_mock.add( + responses.POST, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234", + status=201, + json={"message": "mock message"}, + ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=404, + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(), + { + "cc_id": 1234, + "representative": representative.pk, + "representative_role": "Test role", + "signing_institution": "Test institution", + "version": agreement_version.pk, + "date_signed": "2023-01-01", + "is_primary": True, + "agreementtype-TOTAL_FORMS": 1, + "agreementtype-INITIAL_FORMS": 0, + "agreementtype-MIN_NUM_FORMS": 1, + "agreementtype-MAX_NUM_FORMS": 1, + "agreementtype-0-study": study.pk, + }, + ) + self.assertEqual(response.status_code, 200) + # The form is valid... + form = response.context["form"] + self.assertTrue(form.is_valid()) + formset = response.context["formset"] + self.assertTrue(formset.is_valid()) + # ...but there was some error from the API. + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual("AnVIL API Error: other error", str(messages[0])) + # No objects were created. + self.assertEqual(models.DataAffiliateAgreement.objects.count(), 0) + self.assertEqual(ManagedGroup.objects.count(), 1) # Just the admin group. + class DataAffiliateAgreementDetailTest(TestCase): """Tests for the DataAffiliateAgreement view.""" @@ -3668,6 +4094,8 @@ def setUp(self): codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) + # Create the admins group. + self.cc_admins_group = ManagedGroupFactory.create(name="TEST_PRIMED_CC_ADMINS") def get_url(self, *args): """Get the url for the view being tested.""" @@ -3746,6 +4174,13 @@ def test_can_create_object(self): status=201, json={"message": "mock message"}, ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -3776,7 +4211,6 @@ def test_can_create_object(self): # Type was set correctly. self.assertEqual(new_agreement.type, new_agreement.NON_DATA_AFFILIATE) # AnVIL group was set correctly. - self.assertEqual(ManagedGroup.objects.count(), 1) self.assertIsInstance(new_agreement.anvil_access_group, ManagedGroup) self.assertEqual( new_agreement.anvil_access_group.name, "TEST_PRIMED_CDSA_ACCESS_1234" @@ -3803,6 +4237,13 @@ def test_redirect_url(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -3836,6 +4277,13 @@ def test_success_message(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -4335,6 +4783,13 @@ def test_creates_anvil_access_group(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -4354,12 +4809,17 @@ def test_creates_anvil_access_group(self): ) self.assertEqual(response.status_code, 302) new_object = models.SignedAgreement.objects.latest("pk") - self.assertEqual(ManagedGroup.objects.count(), 1) + self.assertEqual(ManagedGroup.objects.count(), 2) # A new group was created. new_group = ManagedGroup.objects.latest("pk") self.assertEqual(new_object.anvil_access_group, new_group) self.assertEqual(new_group.name, "TEST_PRIMED_CDSA_ACCESS_2345") self.assertTrue(new_group.is_managed_by_app) + # A group-group membership was created with PRIMED_CC_ADMINS as an admin of the access group. + new_membership = GroupGroupMembership.objects.get( + parent_group=new_object.anvil_access_group, child_group=self.cc_admins_group + ) + self.assertEqual(new_membership.role, GroupGroupMembership.ADMIN) @override_settings(ANVIL_DATA_ACCESS_GROUP_PREFIX="foo") def test_creates_anvil_groups_different_setting(self): @@ -4373,6 +4833,13 @@ def test_creates_anvil_groups_different_setting(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/foo_CDSA_ACCESS_2345/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), { @@ -4392,13 +4859,59 @@ def test_creates_anvil_groups_different_setting(self): ) self.assertEqual(response.status_code, 302) new_object = models.SignedAgreement.objects.latest("pk") - self.assertEqual(ManagedGroup.objects.count(), 1) + self.assertEqual(ManagedGroup.objects.count(), 2) # A new group was created. new_group = ManagedGroup.objects.latest("pk") self.assertEqual(new_object.anvil_access_group, new_group) self.assertEqual(new_group.name, "foo_CDSA_ACCESS_2345") self.assertTrue(new_group.is_managed_by_app) + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foo") + def test_creates_anvil_groups_different_setting_cc_admins_group_name(self): + """View creates a managed group upon when form is valid.""" + admin_group = ManagedGroupFactory.create(name="foo", email="foo@firecloud.org") + self.client.force_login(self.user) + representative = UserFactory.create() + agreement_version = factories.AgreementVersionFactory.create() + api_url = ( + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345" + ) + self.anvil_response_mock.add( + responses.POST, api_url, status=201, json={"message": "mock message"} + ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_2345/admin/foo@firecloud.org", + status=204, + ) + response = self.client.post( + self.get_url(), + { + "cc_id": 2345, + "representative": representative.pk, + "representative_role": "Test role", + "signing_institution": "Test institution", + "version": agreement_version.pk, + "date_signed": "2023-01-01", + "is_primary": True, + "agreementtype-TOTAL_FORMS": 1, + "agreementtype-INITIAL_FORMS": 0, + "agreementtype-MIN_NUM_FORMS": 1, + "agreementtype-MAX_NUM_FORMS": 1, + "agreementtype-0-affiliation": "Foo Bar", + }, + ) + self.assertEqual(response.status_code, 302) + new_object = models.SignedAgreement.objects.latest("pk") + membership = GroupGroupMembership.objects.get( + parent_group=new_object.anvil_access_group, + child_group=admin_group, + ) + self.assertEqual(membership.role, GroupGroupMembership.ADMIN) + def test_manage_group_create_api_error(self): """Nothing is created when the form is valid but there is an API error when creating the group.""" self.client.force_login(self.user) @@ -4441,7 +4954,7 @@ def test_manage_group_create_api_error(self): # No objects were created. self.assertEqual(models.SignedAgreement.objects.count(), 0) self.assertEqual(models.NonDataAffiliateAgreement.objects.count(), 0) - self.assertEqual(ManagedGroup.objects.count(), 0) + self.assertEqual(ManagedGroup.objects.count(), 1) # Just the admins group. def test_managed_group_already_exists_in_app(self): """No objects are created if the managed group already exists in the app.""" @@ -4788,6 +5301,9 @@ def test_only_includes_active_agreements(self): withdrawn_agreement = factories.MemberAgreementFactory.create( signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN ) + replaced_agreement = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.REPLACED + ) self.client.force_login(self.user) response = self.client.get(self.get_url()) self.assertIn("table", response.context_data) @@ -4796,6 +5312,7 @@ def test_only_includes_active_agreements(self): self.assertIn(active_agreement.signed_agreement, table.data) self.assertNotIn(lapsed_agreement.signed_agreement, table.data) self.assertNotIn(withdrawn_agreement.signed_agreement, table.data) + self.assertNotIn(replaced_agreement.signed_agreement, table.data) class SignedAgreementAuditTest(TestCase): @@ -5253,6 +5770,9 @@ def test_only_includes_active_agreements(self): withdrawn_agreement = factories.DataAffiliateAgreementFactory.create( signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN ) + replaced_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.REPLACED + ) self.client.force_login(self.user) response = self.client.get(self.get_url()) self.assertIn("table", response.context_data) @@ -5261,6 +5781,7 @@ def test_only_includes_active_agreements(self): self.assertIn(active_agreement, table.data) self.assertNotIn(lapsed_agreement, table.data) self.assertNotIn(withdrawn_agreement, table.data) + self.assertNotIn(replaced_agreement, table.data) class UserAccessRecordsList(TestCase): @@ -5438,6 +5959,12 @@ def test_only_includes_active_agreements(self): withdrawn_member = GroupAccountMembershipFactory.create( group=withdrawn_agreement.signed_agreement.anvil_access_group ) + replaced_agreement = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.REPLACED + ) + replaced_member = GroupAccountMembershipFactory.create( + group=replaced_agreement.signed_agreement.anvil_access_group + ) self.client.force_login(self.user) response = self.client.get(self.get_url()) self.assertIn("table", response.context_data) @@ -5446,6 +5973,7 @@ def test_only_includes_active_agreements(self): self.assertIn(active_member, table.data) self.assertNotIn(lapsed_member, table.data) self.assertNotIn(withdrawn_member, table.data) + self.assertNotIn(replaced_member, table.data) class CDSAWorkspaceRecordsList(TestCase): @@ -5516,6 +6044,11 @@ def test_only_includes_workspaces_with_active_agreements(self): study=withdrawn_workspace.study, signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, ) + replaced_workspace = factories.CDSAWorkspaceFactory.create() + factories.DataAffiliateAgreementFactory.create( + study=replaced_workspace.study, + signed_agreement__status=models.SignedAgreement.StatusChoices.REPLACED, + ) self.client.force_login(self.user) response = self.client.get(self.get_url()) self.assertIn("table", response.context_data) @@ -5524,6 +6057,7 @@ def test_only_includes_workspaces_with_active_agreements(self): self.assertIn(active_workspace, table.data) self.assertNotIn(lapsed_workspace, table.data) self.assertNotIn(withdrawn_workspace, table.data) + self.assertNotIn(replaced_workspace, table.data) class CDSAWorkspaceDetailTest(TestCase): diff --git a/primed/cdsa/views.py b/primed/cdsa/views.py index 952804a7..b391b20c 100644 --- a/primed/cdsa/views.py +++ b/primed/cdsa/views.py @@ -6,7 +6,11 @@ AnVILConsortiumManagerStaffViewRequired, AnVILProjectManagerAccess, ) -from anvil_consortium_manager.models import GroupAccountMembership, ManagedGroup +from anvil_consortium_manager.models import ( + GroupAccountMembership, + GroupGroupMembership, + ManagedGroup, +) from django.conf import settings from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin @@ -15,7 +19,7 @@ from django.forms import inlineformset_factory from django.http import Http404, HttpResponseRedirect from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, FormView, TemplateView, UpdateView from django_tables2 import MultiTableMixin, SingleTableMixin, SingleTableView @@ -238,6 +242,18 @@ def get_agreement(self, form, formset): # Make sure the group doesn't exist already. access_group.full_clean() access_group.save() + # Add the cc admins group as a member. + cc_admins_group = ManagedGroup.objects.get( + name=settings.ANVIL_CC_ADMINS_GROUP_NAME + ) + self.admin_access_membership = GroupGroupMembership( + parent_group=access_group, + child_group=cc_admins_group, + role=GroupGroupMembership.ADMIN, + ) + self.admin_access_membership.full_clean() + self.admin_access_membership.save() + # Now finally save the agreement. agreement = form.save(commit=False) agreement.anvil_access_group = access_group agreement.type = self.agreement_type_model.AGREEMENT_TYPE @@ -254,6 +270,7 @@ def anvil_create(self): """Create resources on ANVIL.""" # Create AnVIL groups. self.object.anvil_access_group.anvil_create() + self.admin_access_membership.anvil_create() def form_valid(self, form): formset = self.get_formset() @@ -330,6 +347,7 @@ def anvil_create(self): """Create resources on ANVIL.""" super().anvil_create() self.object.dataaffiliateagreement.anvil_upload_group.anvil_create() + self.admin_upload_membership.anvil_create() def get_agreement_type(self, form, formset): agreement_type = super().get_agreement_type(form, formset) @@ -344,6 +362,17 @@ def get_agreement_type(self, form, formset): # Make sure the group doesn't exist already. upload_group.full_clean() upload_group.save() + # Add the cc admins group as a member. + cc_admins_group = ManagedGroup.objects.get( + name=settings.ANVIL_CC_ADMINS_GROUP_NAME + ) + self.admin_upload_membership = GroupGroupMembership( + parent_group=upload_group, + child_group=cc_admins_group, + role=GroupGroupMembership.ADMIN, + ) + self.admin_upload_membership.full_clean() + self.admin_upload_membership.save() agreement_type.anvil_upload_group = upload_group return agreement_type diff --git a/primed/dbgap/adapters.py b/primed/dbgap/adapters.py index 532b0cfe..5c567a5b 100644 --- a/primed/dbgap/adapters.py +++ b/primed/dbgap/adapters.py @@ -10,7 +10,8 @@ class dbGaPWorkspaceAdapter(BaseWorkspaceAdapter): type = "dbgap" name = "dbGaP workspace" description = "Workspaces containing data from released dbGaP accessions" - list_table_class = tables.dbGaPWorkspaceTable + list_table_class_staff_view = tables.dbGaPWorkspaceStaffTable + list_table_class_view = tables.dbGaPWorkspaceUserTable workspace_form_class = WorkspaceForm workspace_data_model = models.dbGaPWorkspace workspace_data_form_class = forms.dbGaPWorkspaceForm diff --git a/primed/dbgap/tables.py b/primed/dbgap/tables.py index 2e075bcd..3d23b07c 100644 --- a/primed/dbgap/tables.py +++ b/primed/dbgap/tables.py @@ -71,7 +71,7 @@ def render_dbgap_phs(self, value): return "phs{0:06d}".format(value) -class dbGaPWorkspaceTable(tables.Table): +class dbGaPWorkspaceStaffTable(tables.Table): """Class to render a table of Workspace objects with dbGaPWorkspace workspace data.""" name = tables.columns.Column(linkify=True) @@ -116,10 +116,10 @@ def render_number_approved_dars(self, record): return n -class dbGaPWorkspaceLimitedViewTable(tables.Table): +class dbGaPWorkspaceUserTable(tables.Table): """Class to render a table of Workspace objects with dbGaPWorkspace workspace data.""" - name = tables.columns.Column() + name = tables.columns.Column(linkify=True) billing_project = tables.Column() dbgap_accession = dbGaPAccessionColumn( accessor="dbgapworkspace__get_dbgap_accession", diff --git a/primed/dbgap/tests/factories.py b/primed/dbgap/tests/factories.py index de6f3e98..02f9bbd8 100644 --- a/primed/dbgap/tests/factories.py +++ b/primed/dbgap/tests/factories.py @@ -61,6 +61,7 @@ def studies(self, create, extracted, **kwargs): class Meta: model = models.dbGaPStudyAccession + skip_postgeneration_save = True class dbGaPWorkspaceFactory(TimeStampedModelFactory, DjangoModelFactory): @@ -78,6 +79,7 @@ class dbGaPWorkspaceFactory(TimeStampedModelFactory, DjangoModelFactory): class Meta: model = models.dbGaPWorkspace + skip_postgeneration_save = True @post_generation def authorization_domains(self, create, extracted, **kwargs): diff --git a/primed/dbgap/tests/test_tables.py b/primed/dbgap/tests/test_tables.py index 6db0ae99..514a27fd 100644 --- a/primed/dbgap/tests/test_tables.py +++ b/primed/dbgap/tests/test_tables.py @@ -116,10 +116,10 @@ def test_ordering(self): self.assertEqual(table.data[1], instance_1) -class dbGaPWorkspaceTableTest(TestCase): +class dbGaPWorkspaceStaffTableTest(TestCase): model = acm_models.Workspace model_factory = factories.dbGaPWorkspaceFactory - table_class = tables.dbGaPWorkspaceTable + table_class = tables.dbGaPWorkspaceStaffTable def test_row_count_with_no_objects(self): table = self.table_class(self.model.objects.all()) @@ -206,10 +206,10 @@ def test_render_number_approved_dars_only_most_recent(self): # self.assertEqual("", table.rows[0].get_cell_value("is_shared")) -class dbGaPWorkspaceLimitedViewTableTest(TestCase): +class dbGaPWorkspaceUserTableTest(TestCase): model = acm_models.Workspace model_factory = factories.dbGaPWorkspaceFactory - table_class = tables.dbGaPWorkspaceTable + table_class = tables.dbGaPWorkspaceStaffTable def test_row_count_with_no_objects(self): table = self.table_class(self.model.objects.all()) diff --git a/primed/dbgap/tests/test_views.py b/primed/dbgap/tests/test_views.py index 1791ca41..295c8981 100644 --- a/primed/dbgap/tests/test_views.py +++ b/primed/dbgap/tests/test_views.py @@ -7,6 +7,7 @@ from anvil_consortium_manager import views as acm_views from anvil_consortium_manager.models import ( AnVILProjectManagerAccess, + GroupGroupMembership, ManagedGroup, Workspace, ) @@ -267,7 +268,7 @@ def test_workspace_table(self): response = self.get_view()(request, dbgap_phs=self.obj.dbgap_phs) self.assertIn("workspace_table", response.context_data) self.assertIsInstance( - response.context_data["workspace_table"], tables.dbGaPWorkspaceTable + response.context_data["workspace_table"], tables.dbGaPWorkspaceStaffTable ) def test_workspace_table_none(self): @@ -856,7 +857,7 @@ def test_view_has_correct_table_class(self): response = self.get_view()(request, workspace_type=self.workspace_type) self.assertIn("table", response.context_data) self.assertIsInstance( - response.context_data["table"], tables.dbGaPWorkspaceTable + response.context_data["table"], tables.dbGaPWorkspaceStaffTable ) @@ -1587,6 +1588,8 @@ def setUp(self): codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME ) ) + # Create the admin group. + self.cc_admin_group = ManagedGroupFactory.create(name="TEST_PRIMED_CC_ADMINS") def get_url(self, *args): """Get the url for the view being tested.""" @@ -1662,6 +1665,13 @@ def test_can_create_object(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_DBGAP_ACCESS_1/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), {"principal_investigator": pi.pk, "dbgap_project_id": 1} ) @@ -1684,6 +1694,12 @@ def test_redirect_url(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + api_url + "/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), {"principal_investigator": pi.pk, "dbgap_project_id": 1} ) @@ -1702,6 +1718,12 @@ def test_success_message(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + api_url + "/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), {"principal_investigator": pi.pk, "dbgap_project_id": 1}, @@ -1817,20 +1839,33 @@ def test_creates_anvil_access_group(self): self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + api_url + "/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) response = self.client.post( self.get_url(), {"principal_investigator": pi.pk, "dbgap_project_id": 12498} ) self.assertEqual(response.status_code, 302) new_object = models.dbGaPApplication.objects.latest("pk") - self.assertEqual(ManagedGroup.objects.count(), 1) # A new group was created. new_group = ManagedGroup.objects.latest("pk") self.assertEqual(new_object.anvil_access_group, new_group) self.assertEqual(new_group.name, "TEST_PRIMED_DBGAP_ACCESS_12498") self.assertTrue(new_group.is_managed_by_app) + # Also creates the CC admins group membership + membership = GroupGroupMembership.objects.get( + parent_group=new_group, + child_group=self.cc_admin_group, + ) + self.assertEqual(membership.role, GroupGroupMembership.ADMIN) @override_settings(ANVIL_DATA_ACCESS_GROUP_PREFIX="foo") - def test_creates_anvil_access_group_different_setting(self): + def test_creates_anvil_access_group_different_setting_data_access_group_prefix( + self, + ): """View creates a managed group upon when form is valid.""" self.client.force_login(self.user) pi = UserFactory.create() @@ -1838,6 +1873,12 @@ def test_creates_anvil_access_group_different_setting(self): api_url = ( self.api_client.sam_entry_point + "/api/groups/v1/foo_DBGAP_ACCESS_12498" ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + api_url + "/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=204, + ) self.anvil_response_mock.add( responses.POST, api_url, status=201, json={"message": "mock message"} ) @@ -1846,12 +1887,54 @@ def test_creates_anvil_access_group_different_setting(self): ) self.assertEqual(response.status_code, 302) new_object = models.dbGaPApplication.objects.latest("pk") - self.assertEqual(ManagedGroup.objects.count(), 1) # A new group was created. new_group = ManagedGroup.objects.latest("pk") self.assertEqual(new_object.anvil_access_group, new_group) self.assertEqual(new_group.name, "foo_DBGAP_ACCESS_12498") self.assertTrue(new_group.is_managed_by_app) + # Also creates the CC admins group membership + membership = GroupGroupMembership.objects.get( + parent_group=new_group, + child_group=self.cc_admin_group, + ) + self.assertEqual(membership.role, GroupGroupMembership.ADMIN) + + @override_settings(ANVIL_CC_ADMINS_GROUP_NAME="foo") + def test_creates_anvil_access_group_different_setting_cc_admin_group(self): + """View creates a managed group upon when form is valid.""" + admin_group = ManagedGroupFactory.create(name="foo", email="foo@firecloud.org") + self.client.force_login(self.user) + pi = UserFactory.create() + # API response to create the associated anvil_access_group. + api_url = ( + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_DBGAP_ACCESS_12498" + ) + self.anvil_response_mock.add( + responses.POST, api_url, status=201, json={"message": "mock message"} + ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + api_url + "/admin/foo@firecloud.org", + status=204, + ) + response = self.client.post( + self.get_url(), {"principal_investigator": pi.pk, "dbgap_project_id": 12498} + ) + self.assertEqual(response.status_code, 302) + new_object = models.dbGaPApplication.objects.latest("pk") + # A new group was created. + new_group = ManagedGroup.objects.latest("pk") + self.assertEqual(new_object.anvil_access_group, new_group) + self.assertEqual(new_group.name, "TEST_PRIMED_DBGAP_ACCESS_12498") + self.assertTrue(new_group.is_managed_by_app) + # Also creates the CC admins group membership + membership = GroupGroupMembership.objects.get( + parent_group=new_group, + child_group=admin_group, + ) + self.assertEqual(membership.role, GroupGroupMembership.ADMIN) def test_manage_group_create_api_error(self): """Nothing is created when the form is valid but there is an API error when creating the group.""" @@ -1878,7 +1961,7 @@ def test_manage_group_create_api_error(self): self.assertEqual("AnVIL API Error: other error", str(messages[0])) # No objects were created. self.assertEqual(models.dbGaPApplication.objects.count(), 0) - self.assertEqual(ManagedGroup.objects.count(), 0) + self.assertEqual(ManagedGroup.objects.count(), 1) # Just the admin group. def test_managed_group_already_exists_in_app(self): """No objects are created if the managed group already exists in the app.""" @@ -1902,6 +1985,40 @@ def test_managed_group_already_exists_in_app(self): # No dbGaPApplication was created. self.assertEqual(models.dbGaPApplication.objects.count(), 0) + def test_admin_group_membership_api_error(self): + """Nothing is created when the form is valid but there is an API error when creating admin group membership.""" + self.client.force_login(self.user) + pi = UserFactory.create() + # API response to create the associated anvil_access_group. + api_url = ( + self.api_client.sam_entry_point + + "/api/groups/v1/TEST_PRIMED_DBGAP_ACCESS_1" + ) + self.anvil_response_mock.add( + responses.POST, api_url, status=201, json={"message": "other error"} + ) + # CC admins group membership. + self.anvil_response_mock.add( + responses.PUT, + api_url + "/admin/TEST_PRIMED_CC_ADMINS@firecloud.org", + status=404, + json={"message": "other error"}, + ) + response = self.client.post( + self.get_url(), {"principal_investigator": pi.pk, "dbgap_project_id": 1} + ) + self.assertEqual(response.status_code, 200) + # The form is valid... + form = response.context["form"] + self.assertTrue(form.is_valid()) + # ...but there was some error from the API. + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual("AnVIL API Error: other error", str(messages[0])) + # No objects were created. + self.assertEqual(models.dbGaPApplication.objects.count(), 0) + self.assertEqual(ManagedGroup.objects.count(), 1) # Just the admin group. + class dbGaPDataAccessSnapshotCreateTest(dbGaPResponseTestMixin, TestCase): """Tests for the dbGaPDataAccessRequestCreateFromJson view.""" diff --git a/primed/dbgap/views.py b/primed/dbgap/views.py index 545830b4..be06d329 100644 --- a/primed/dbgap/views.py +++ b/primed/dbgap/views.py @@ -8,6 +8,7 @@ ) from anvil_consortium_manager.models import ( AnVILProjectManagerAccess, + GroupGroupMembership, ManagedGroup, Workspace, ) @@ -57,7 +58,7 @@ def get_object(self, queryset=None): return obj def get_table(self): - return tables.dbGaPWorkspaceTable( + return tables.dbGaPWorkspaceStaffTable( Workspace.objects.filter(dbgapworkspace__dbgap_study_accession=self.object), exclude=( "dbgapworkspace__dbgap_study_accession__study", @@ -205,7 +206,6 @@ class dbGaPApplicationCreate( anvil_access_group_pattern = "PRIMED_DBGAP_ACCESS_{project_id}" ERROR_CREATING_GROUP = "Error creating Managed Group in app." - # @transaction.atomic def form_valid(self, form): """Create a managed group in the app on AnVIL and link it to this application.""" project_id = form.cleaned_data["dbgap_project_id"] @@ -229,7 +229,28 @@ def form_valid(self, form): self.request, messages.ERROR, "AnVIL API Error: " + str(e) ) return self.render_to_response(self.get_context_data(form=form)) - managed_group.save() + # Need to wrap this entire block in a transaction because we are creating multiple objects, and don't want + # any of them to be saved if the API call fails. + try: + with transaction.atomic(): + managed_group.save() + # Create the dbgap access group. + cc_admins_group = ManagedGroup.objects.get( + name=settings.ANVIL_CC_ADMINS_GROUP_NAME + ) + membership = GroupGroupMembership.objects.create( + parent_group=managed_group, + child_group=cc_admins_group, + role=GroupGroupMembership.ADMIN, + ) + membership.full_clean() + membership.anvil_create() + membership.save() + except AnVILAPIError as e: + messages.add_message( + self.request, messages.ERROR, "AnVIL API Error: " + str(e) + ) + return self.render_to_response(self.get_context_data(form=form)) form.instance.anvil_access_group = managed_group return super().form_valid(form) diff --git a/primed/duo/tests/test_views.py b/primed/duo/tests/test_views.py index 5ebd2bc6..96f2a5af 100644 --- a/primed/duo/tests/test_views.py +++ b/primed/duo/tests/test_views.py @@ -19,7 +19,7 @@ class DataUsePermissionListTest(TestCase): def setUp(self): """Set up test class.""" self.factory = RequestFactory() - # Create a user with both view and edit permission. + # Create a user with staff view permission. self.user = UserFactory.create(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( @@ -122,11 +122,11 @@ class DataUsePermissionDetailTest(TestCase): def setUp(self): """Set up test class.""" self.factory = RequestFactory() - # Create a user with both view and edit permission. + # Create a user with view permission. self.user = UserFactory.create(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( - codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.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.STAFF_VIEW_PERMISSION_CODENAME + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME ) ) diff --git a/primed/duo/views.py b/primed/duo/views.py index 06c7c749..f27c1e25 100644 --- a/primed/duo/views.py +++ b/primed/duo/views.py @@ -1,4 +1,7 @@ -from anvil_consortium_manager.auth import AnVILConsortiumManagerStaffViewRequired +from anvil_consortium_manager.auth import ( + AnVILConsortiumManagerStaffViewRequired, + AnVILConsortiumManagerViewRequired, +) from django.http import Http404 from django.views.generic import DetailView, ListView @@ -18,7 +21,7 @@ def get_context_data(self, **kwargs): return context -class DataUsePermissionDetail(AnVILConsortiumManagerStaffViewRequired, DetailView): +class DataUsePermissionDetail(AnVILConsortiumManagerViewRequired, DetailView): model = models.DataUsePermission @@ -51,7 +54,7 @@ def get_context_data(self, **kwargs): return context -class DataUseModifierDetail(AnVILConsortiumManagerStaffViewRequired, DetailView): +class DataUseModifierDetail(AnVILConsortiumManagerViewRequired, DetailView): model = models.DataUseModifier diff --git a/primed/miscellaneous_workspaces/adapters.py b/primed/miscellaneous_workspaces/adapters.py index d4c9e8e8..24262017 100644 --- a/primed/miscellaneous_workspaces/adapters.py +++ b/primed/miscellaneous_workspaces/adapters.py @@ -3,7 +3,10 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter from anvil_consortium_manager.forms import WorkspaceForm -from primed.primed_anvil.tables import DefaultWorkspaceTable +from primed.primed_anvil.tables import ( + DefaultWorkspaceStaffTable, + DefaultWorkspaceUserTable, +) from . import forms, models, tables @@ -14,7 +17,8 @@ class SimulatedDataWorkspaceAdapter(BaseWorkspaceAdapter): type = "simulated_data" name = "Simulated Data workspace" description = "Workspaces containing simulated data" - list_table_class = DefaultWorkspaceTable + list_table_class_staff_view = DefaultWorkspaceStaffTable + list_table_class_view = DefaultWorkspaceUserTable workspace_form_class = WorkspaceForm workspace_data_model = models.SimulatedDataWorkspace workspace_data_form_class = forms.SimulatedDataWorkspaceForm @@ -29,7 +33,8 @@ class ConsortiumDevelWorkspaceAdapter(BaseWorkspaceAdapter): type = "devel" name = "Consortium development workspace" description = "Workspaces intended for consortium development of methods" - list_table_class = DefaultWorkspaceTable + list_table_class_staff_view = DefaultWorkspaceStaffTable + list_table_class_view = DefaultWorkspaceUserTable workspace_form_class = WorkspaceForm workspace_data_model = models.ConsortiumDevelWorkspace workspace_data_form_class = forms.ConsortiumDevelWorkspaceForm @@ -42,7 +47,8 @@ class ResourceWorkspaceAdapter(BaseWorkspaceAdapter): type = "resource" name = "Resource workspace" description = "Workspaces containing consortium resources (e.g., examples of using AnVIL, data inventories)" - list_table_class = DefaultWorkspaceTable + list_table_class_staff_view = DefaultWorkspaceStaffTable + list_table_class_view = DefaultWorkspaceUserTable workspace_form_class = WorkspaceForm workspace_data_model = models.ResourceWorkspace workspace_data_form_class = forms.ResourceWorkspaceForm @@ -54,8 +60,11 @@ class TemplateWorkspaceAdapter(BaseWorkspaceAdapter): type = "template" name = "Template workspace" - description = "Template workspaces that can be cloned to create other workspaces" - list_table_class = DefaultWorkspaceTable + description = ( + "Template workspaces that will be cloned by the CC to create other workspaces" + ) + list_table_class_staff_view = DefaultWorkspaceStaffTable + list_table_class_view = DefaultWorkspaceUserTable workspace_form_class = WorkspaceForm workspace_data_model = models.TemplateWorkspace workspace_data_form_class = forms.TemplateWorkspaceForm @@ -68,7 +77,8 @@ class OpenAccessWorkspaceAdapter(BaseWorkspaceAdapter): type = "open_access" name = "Open access workspace" description = "Workspaces containing open access data" - list_table_class = tables.OpenAccessWorkspaceTable + list_table_class_staff_view = tables.OpenAccessWorkspaceStaffTable + list_table_class_view = tables.OpenAccessWorkspaceUserTable workspace_form_class = WorkspaceForm workspace_data_model = models.OpenAccessWorkspace workspace_data_form_class = forms.OpenAccessWorkspaceForm @@ -82,8 +92,9 @@ class DataPrepWorkspaceAdapter(BaseWorkspaceAdapter): type = "data_prep" name = "Data prep workspace" - description = "Workspaces used to prepare data." - list_table_class = tables.DataPrepWorkspaceTable + description = "Workspaces used to prepare data for sharing or update data that is already shared" + list_table_class_staff_view = tables.DataPrepWorkspaceTable + list_table_class_view = tables.DataPrepWorkspaceTable workspace_form_class = WorkspaceForm workspace_data_model = models.DataPrepWorkspace workspace_data_form_class = forms.DataPrepWorkspaceForm diff --git a/primed/miscellaneous_workspaces/tables.py b/primed/miscellaneous_workspaces/tables.py index f9763621..fd1132df 100644 --- a/primed/miscellaneous_workspaces/tables.py +++ b/primed/miscellaneous_workspaces/tables.py @@ -9,7 +9,7 @@ ) -class OpenAccessWorkspaceTable(tables.Table): +class OpenAccessWorkspaceStaffTable(tables.Table): """Class to render a table of Workspace objects with OpenAccessWorkspace workspace data.""" name = tables.columns.Column(linkify=True) @@ -27,10 +27,10 @@ class Meta: order_by = ("name",) -class OpenAccessWorkspaceLimitedViewTable(tables.Table): +class OpenAccessWorkspaceUserTable(tables.Table): """Class to render a table of Workspace objects with OpenAccessWorkspace workspace data.""" - name = tables.columns.Column() + name = tables.columns.Column(linkify=True) billing_project = tables.Column() is_shared = WorkspaceSharedWithConsortiumColumn() diff --git a/primed/miscellaneous_workspaces/tests/test_tables.py b/primed/miscellaneous_workspaces/tests/test_tables.py index 992ffcb3..de536164 100644 --- a/primed/miscellaneous_workspaces/tests/test_tables.py +++ b/primed/miscellaneous_workspaces/tests/test_tables.py @@ -7,11 +7,11 @@ from . import factories -class OpenAccessWorkspaceTableTest(TestCase): - """Tests for the OpenAccessWorkspaceTable table.""" +class OpenAccessWorkspaceStaffTableTest(TestCase): + """Tests for the OpenAccessWorkspaceStaffTable table.""" model_factory = factories.OpenAccessWorkspaceFactory - table_class = tables.OpenAccessWorkspaceTable + table_class = tables.OpenAccessWorkspaceStaffTable def test_row_count_with_no_objects(self): table = self.table_class(Workspace.objects.all()) @@ -28,11 +28,11 @@ def test_row_count_with_two_objects(self): self.assertEqual(len(table.rows), 2) -class OpenAccessWorkspaceLimitedViewTableTest(TestCase): - """Tests for the OpenAccessWorkspaceTable table.""" +class OpenAccessWorkspaceUserTableTest(TestCase): + """Tests for the OpenAccessWorkspaceStaffTable table.""" model_factory = factories.OpenAccessWorkspaceFactory - table_class = tables.OpenAccessWorkspaceLimitedViewTable + table_class = tables.OpenAccessWorkspaceUserTable def test_row_count_with_no_objects(self): table = self.table_class(Workspace.objects.all()) diff --git a/primed/primed_anvil/tables.py b/primed/primed_anvil/tables.py index 05e680bf..71fb7fac 100644 --- a/primed/primed_anvil/tables.py +++ b/primed/primed_anvil/tables.py @@ -47,31 +47,7 @@ def _get_bool_value(self, record, value, bound_column): return is_shared -# class WorkspaceSharedWithConsortiumTable(tables.Table): -# """Table including a column to indicate if a workspace is shared with PRIMED_ALL.""" - -# is_shared = tables.columns.Column( -# accessor="pk", -# verbose_name="Shared with PRIMED?", -# orderable=False, -# ) - -# def render_is_shared(self, record): -# is_shared = record.workspacegroupsharing_set.filter( -# group__name="PRIMED_ALL" -# ).exists() -# if is_shared: -# icon = "check-circle-fill" -# color = "green" -# value = format_html( -# """""".format(icon, color) -# ) -# else: -# value = "" -# return value - - -class DefaultWorkspaceTable(tables.Table): +class DefaultWorkspaceStaffTable(tables.Table): """Class to use for default workspace tables in PRIMED.""" name = tables.Column(linkify=True, verbose_name="Workspace") @@ -95,6 +71,23 @@ class Meta: order_by = ("name",) +class DefaultWorkspaceUserTable(tables.Table): + """Class to use for default workspace tables in PRIMED.""" + + name = tables.Column(linkify=True, verbose_name="Workspace") + billing_project = tables.Column() + is_shared = WorkspaceSharedWithConsortiumColumn() + + class Meta: + model = Workspace + fields = ( + "name", + "billing_project", + "is_shared", + ) + order_by = ("name",) + + class StudyTable(tables.Table): """A table for `Study`s.""" diff --git a/primed/primed_anvil/tests/test_views.py b/primed/primed_anvil/tests/test_views.py index 85d59706..b84d4c5f 100644 --- a/primed/primed_anvil/tests/test_views.py +++ b/primed/primed_anvil/tests/test_views.py @@ -12,21 +12,21 @@ from django.test import RequestFactory, TestCase from django.urls import reverse -from primed.cdsa.tables import CDSAWorkspaceLimitedViewTable, CDSAWorkspaceTable +from primed.cdsa.tables import CDSAWorkspaceStaffTable, CDSAWorkspaceUserTable from primed.cdsa.tests.factories import ( CDSAWorkspaceFactory, DataAffiliateAgreementFactory, MemberAgreementFactory, ) -from primed.dbgap.tables import dbGaPWorkspaceLimitedViewTable, dbGaPWorkspaceTable +from primed.dbgap.tables import dbGaPWorkspaceStaffTable, dbGaPWorkspaceUserTable from primed.dbgap.tests.factories import ( dbGaPApplicationFactory, dbGaPStudyAccessionFactory, dbGaPWorkspaceFactory, ) from primed.miscellaneous_workspaces.tables import ( - OpenAccessWorkspaceLimitedViewTable, - OpenAccessWorkspaceTable, + OpenAccessWorkspaceStaffTable, + OpenAccessWorkspaceUserTable, ) from primed.miscellaneous_workspaces.tests.factories import OpenAccessWorkspaceFactory from primed.primed_anvil.tests.factories import AvailableDataFactory, StudyFactory @@ -257,10 +257,14 @@ def test_table_classes_view_permission(self): self.client.force_login(self.user) response = self.client.get(self.get_url(obj.pk)) self.assertIn("tables", response.context_data) - self.assertIsInstance(response.context_data["tables"][0], dbGaPWorkspaceTable) - self.assertIsInstance(response.context_data["tables"][1], CDSAWorkspaceTable) self.assertIsInstance( - response.context_data["tables"][3], OpenAccessWorkspaceTable + response.context_data["tables"][0], dbGaPWorkspaceStaffTable + ) + self.assertIsInstance( + response.context_data["tables"][1], CDSAWorkspaceStaffTable + ) + self.assertIsInstance( + response.context_data["tables"][3], OpenAccessWorkspaceStaffTable ) def test_table_classes_limited_view_permission(self): @@ -276,13 +280,13 @@ def test_table_classes_limited_view_permission(self): response = self.client.get(self.get_url(obj.pk)) self.assertIn("tables", response.context_data) self.assertIsInstance( - response.context_data["tables"][0], dbGaPWorkspaceLimitedViewTable + response.context_data["tables"][0], dbGaPWorkspaceUserTable ) self.assertIsInstance( - response.context_data["tables"][1], CDSAWorkspaceLimitedViewTable + response.context_data["tables"][1], CDSAWorkspaceUserTable ) self.assertIsInstance( - response.context_data["tables"][3], OpenAccessWorkspaceLimitedViewTable + response.context_data["tables"][3], OpenAccessWorkspaceUserTable ) def test_dbgap_workspace_table(self): diff --git a/primed/primed_anvil/views.py b/primed/primed_anvil/views.py index b4fdb98b..09b0d672 100644 --- a/primed/primed_anvil/views.py +++ b/primed/primed_anvil/views.py @@ -15,20 +15,20 @@ from primed.cdsa.models import DataAffiliateAgreement, MemberAgreement from primed.cdsa.tables import ( - CDSAWorkspaceLimitedViewTable, - CDSAWorkspaceTable, + CDSAWorkspaceStaffTable, + CDSAWorkspaceUserTable, DataAffiliateAgreementTable, MemberAgreementTable, ) from primed.dbgap.models import dbGaPApplication from primed.dbgap.tables import ( dbGaPApplicationTable, - dbGaPWorkspaceLimitedViewTable, - dbGaPWorkspaceTable, + dbGaPWorkspaceStaffTable, + dbGaPWorkspaceUserTable, ) from primed.miscellaneous_workspaces.tables import ( - OpenAccessWorkspaceLimitedViewTable, - OpenAccessWorkspaceTable, + OpenAccessWorkspaceStaffTable, + OpenAccessWorkspaceUserTable, ) from primed.users.tables import UserTable @@ -42,12 +42,12 @@ class StudyDetail(AnVILConsortiumManagerViewRequired, MultiTableMixin, DetailVie model = models.Study tables = [ - dbGaPWorkspaceTable, - CDSAWorkspaceTable, + dbGaPWorkspaceStaffTable, + CDSAWorkspaceStaffTable, DataAffiliateAgreementTable, - OpenAccessWorkspaceTable, + OpenAccessWorkspaceStaffTable, ] - # table_class = dbGaPWorkspaceTable + # table_class = dbGaPWorkspaceStaffTable # context_table_name = "dbgap_workspace_table" def get_tables(self): @@ -64,18 +64,18 @@ def get_tables(self): 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), - CDSAWorkspaceTable(cdsa_qs), + dbGaPWorkspaceStaffTable(dbgap_qs), + CDSAWorkspaceStaffTable(cdsa_qs), DataAffiliateAgreementTable(agreement_qs), - OpenAccessWorkspaceTable(open_access_qs), + OpenAccessWorkspaceStaffTable(open_access_qs), ) else: # Assume they have limited view due to auth mixin. return ( - dbGaPWorkspaceLimitedViewTable(dbgap_qs), - CDSAWorkspaceLimitedViewTable(cdsa_qs), + dbGaPWorkspaceUserTable(dbgap_qs), + CDSAWorkspaceUserTable(cdsa_qs), DataAffiliateAgreementTable(agreement_qs), - OpenAccessWorkspaceLimitedViewTable(open_access_qs), + OpenAccessWorkspaceUserTable(open_access_qs), ) @@ -168,7 +168,7 @@ class AvailableDataDetail( model = models.AvailableData context_table_name = "dbgap_workspace_table" - table_class = dbGaPWorkspaceTable + table_class = dbGaPWorkspaceStaffTable context_table_name = "dbgap_workspace_table" def get_table_data(self): diff --git a/primed/templates/base.html b/primed/templates/base.html index c6b0a9aa..b25b6cd1 100644 --- a/primed/templates/base.html +++ b/primed/templates/base.html @@ -118,8 +118,15 @@ {% endfor %} {% endif %} - {% block content %} + {% if not request.user.account %} +
This will change the status of all "Active" agreements associated with this version to "Lapsed".