{% endif %}
{{ block.super }}
From 977d4af8afbbcf65ddbfc3c2ebee0fe06decd869 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 20 Aug 2024 14:52:49 -0700
Subject: [PATCH 034/125] Add a placeholder button for auditing auth domain
membership
The code to audit auth domain membership for an upload workspace
does not yet exist, but it will. For now, add a button to the
workspace detail page without actually linking to a view (since it
doesn't exist yet).
---
.../templates/gregor_anvil/uploadworkspace_detail.html | 3 +++
1 file changed, 3 insertions(+)
diff --git a/gregor_django/templates/gregor_anvil/uploadworkspace_detail.html b/gregor_django/templates/gregor_anvil/uploadworkspace_detail.html
index f682cc8c..7f8cb82b 100644
--- a/gregor_django/templates/gregor_anvil/uploadworkspace_detail.html
+++ b/gregor_django/templates/gregor_anvil/uploadworkspace_detail.html
@@ -44,6 +44,9 @@
{% if record.action %}
- {{ record.action }}
+
+
{% else %}
—
{% endif %}
From 1ff62269f06cca3a6f44d8f896f6cc70117fd6f0 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 10:45:04 -0700
Subject: [PATCH 050/125] Add sharing records to the test data
For each point in the upload cycle, add sharing records based on
what they would have been at the previous point in the upload cycle.
This allows us to see what will change at each step during the audit.
---
add_upload_workspace_audit_test_data.py | 144 ++++++++++++++++++++++--
1 file changed, 135 insertions(+), 9 deletions(-)
diff --git a/add_upload_workspace_audit_test_data.py b/add_upload_workspace_audit_test_data.py
index 429e693e..e33783cb 100644
--- a/add_upload_workspace_audit_test_data.py
+++ b/add_upload_workspace_audit_test_data.py
@@ -1,5 +1,7 @@
+from anvil_consortium_manager.models import WorkspaceGroupSharing
from anvil_consortium_manager.tests.factories import (
ManagedGroupFactory,
+ WorkspaceGroupSharingFactory,
)
from django.conf import settings
from django.utils import timezone
@@ -29,7 +31,7 @@
is_future=True,
is_ready_for_compute=False,
)
-factories.UploadWorkspaceFactory.create(
+workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
research_center=rc,
workspace__name="TEST_U01_RC1",
@@ -46,45 +48,154 @@
research_center=rc,
workspace__name="TEST_U02_RC1",
)
+# Create records as appropriate for the previous point in the cycle - future cycle.
+# Auth domain.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=workspace.workspace.authorization_domains.first(),
+ access=WorkspaceGroupSharing.READER,
+ can_compute=False,
+)
+# DCC admins.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=dcc_admin_group,
+ access=WorkspaceGroupSharing.OWNER,
+ can_compute=True,
+)
+# DCC writers.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=dcc_writer_group,
+ access=WorkspaceGroupSharing.WRITER,
+ can_compute=True,
+)
+# RC uploaders.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=rc_1_uploader_group,
+ access=WorkspaceGroupSharing.WRITER,
+ can_compute=False,
+)
-# Create a current upload cycle before compute.
+# Create a current upload cycle after compute.
upload_cycle = factories.UploadCycleFactory.create(
cycle=3,
is_current=True,
is_ready_for_compute=True,
)
-factories.UploadWorkspaceFactory.create(
+workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
research_center=rc,
workspace__name="TEST_U03_RC1",
)
+# Create records as appropriate for the previous point in the cycle - current cycle before compute.
+# Auth domain.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=workspace.workspace.authorization_domains.first(),
+ access=WorkspaceGroupSharing.READER,
+ can_compute=False,
+)
+# DCC admins.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=dcc_admin_group,
+ access=WorkspaceGroupSharing.OWNER,
+ can_compute=True,
+)
+# DCC writers.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=dcc_writer_group,
+ access=WorkspaceGroupSharing.WRITER,
+ can_compute=True,
+)
+# RC uploaders.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=rc_1_uploader_group,
+ access=WorkspaceGroupSharing.WRITER,
+ can_compute=False,
+)
-# Create a past upload cycle before QC is completed.
+# Create a past upload cycle before qc is completed.
upload_cycle = factories.UploadCycleFactory.create(
cycle=4,
- is_current=True,
+ is_past=True,
is_ready_for_compute=True,
)
-factories.UploadWorkspaceFactory.create(
+workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
research_center=rc,
workspace__name="TEST_U04_RC1",
date_qc_completed=None,
)
+# Create records as appropriate for the previous point in the cycle - current cycle after compute.
+# Auth domain.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=workspace.workspace.authorization_domains.first(),
+ access=WorkspaceGroupSharing.READER,
+ can_compute=False,
+)
+# DCC admins.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=dcc_admin_group,
+ access=WorkspaceGroupSharing.OWNER,
+ can_compute=True,
+)
+# DCC writers.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=dcc_writer_group,
+ access=WorkspaceGroupSharing.WRITER,
+ can_compute=True,
+)
+# RC uploaders.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=rc_1_uploader_group,
+ access=WorkspaceGroupSharing.WRITER,
+ can_compute=True,
+)
# Create a past upload cycle after QC is completed.
upload_cycle = factories.UploadCycleFactory.create(
cycle=5,
- is_current=True,
+ is_past=True,
is_ready_for_compute=True,
)
-factories.UploadWorkspaceFactory.create(
+workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
research_center=rc,
workspace__name="TEST_U05_RC1",
date_qc_completed=timezone.now(),
)
+# Create records as appropriate for the previous point in the cycle - past cycle before QC complete.
+# Auth domain.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=workspace.workspace.authorization_domains.first(),
+ access=WorkspaceGroupSharing.READER,
+ can_compute=False,
+)
+# DCC admins.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=dcc_admin_group,
+ access=WorkspaceGroupSharing.OWNER,
+ can_compute=True,
+)
+# DCC writers.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=dcc_writer_group,
+ access=WorkspaceGroupSharing.WRITER,
+ can_compute=True,
+)
# Create a past upload cycle with a combined workspace.
upload_cycle = factories.UploadCycleFactory.create(
@@ -92,7 +203,7 @@
is_past=True,
is_ready_for_compute=True,
)
-factories.UploadWorkspaceFactory.create(
+workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
research_center=rc,
workspace__name="TEST_U06_RC1",
@@ -103,3 +214,18 @@
date_completed=timezone.now(),
workspace__name="TEST_U06_COMBINED",
)
+# Create records as appropriate for the previous point in the cycle - past cycle before QC complete.
+# Auth domain.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=workspace.workspace.authorization_domains.first(),
+ access=WorkspaceGroupSharing.READER,
+ can_compute=False,
+)
+# DCC admins.
+WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace,
+ group=dcc_admin_group,
+ access=WorkspaceGroupSharing.OWNER,
+ can_compute=True,
+)
From 8df9dc58ba3181098c37d688c138436005d3a158 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 11:11:48 -0700
Subject: [PATCH 051/125] Add a BooleanIconColumn to show icons for True/False
---
gregor_django/gregor_anvil/tables.py | 46 +++++++++++++++
.../gregor_anvil/tests/test_tables.py | 57 +++++++++++++++++++
2 files changed, 103 insertions(+)
diff --git a/gregor_django/gregor_anvil/tables.py b/gregor_django/gregor_anvil/tables.py
index 236c98c2..1ba4f4ac 100644
--- a/gregor_django/gregor_anvil/tables.py
+++ b/gregor_django/gregor_anvil/tables.py
@@ -6,6 +6,52 @@
from . import models
+class BooleanIconColumn(tables.BooleanColumn):
+ """A column that renders a boolean value as an icon.
+
+ This column renders a boolean value as a bootstrap icon. By default, the icon is a green checkmark for True,
+ and nothing for False, but this can be customized by keyword arguments.
+
+ Args:
+ show_false_icon (bool): Whether to show an icon for False values.
+ true_color (str): The color of the icon for True values. (Default: green)
+ false_color (str): The color of the icon for False values. (Default: red)
+ true_icon (str): The icon to use for True values. (Default: check-circle-fill)
+ false_icon (str): The icon to use for False values. (Default: x-circle-fill)
+ """
+
+ def __init__(
+ self,
+ show_false_icon=False,
+ true_color="green",
+ false_color="red",
+ true_icon="check-circle-fill",
+ false_icon="x-circle-fill",
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self.show_false_icon = show_false_icon
+ self.true_color = true_color
+ self.false_color = false_color
+ self.true_icon = true_icon
+ self.false_icon = false_icon
+
+ def render(self, value, record, bound_column):
+ value = self._get_bool_value(record, value, bound_column)
+ if value:
+ rendered_value = format_html(
+ f""""""
+ )
+ else:
+ if self.show_false_icon:
+ rendered_value = format_html(
+ f"""""" # noqa: E501
+ )
+ else:
+ rendered_value = ""
+ return rendered_value
+
+
class AccountTable(tables.Table):
"""A custom table for `Accounts`."""
diff --git a/gregor_django/gregor_anvil/tests/test_tables.py b/gregor_django/gregor_anvil/tests/test_tables.py
index bafc2386..fb5d90c4 100644
--- a/gregor_django/gregor_anvil/tests/test_tables.py
+++ b/gregor_django/gregor_anvil/tests/test_tables.py
@@ -15,6 +15,63 @@
from . import factories
+class BooleanIconColumnTest(TestCase):
+ """Tests for the BooleanIconColumn class."""
+
+ def test_render_default(self):
+ """render method with defaults."""
+ column = tables.BooleanIconColumn()
+ value = column.render(True, None, None)
+ self.assertIn("bi-check-circle-fill", value)
+ self.assertIn("green", value)
+ value = column.render(False, None, None)
+ self.assertEqual(value, "")
+
+ def test_render_show_false_icon(self):
+ """render method with defaults."""
+ column = tables.BooleanIconColumn(show_false_icon=True)
+ value = column.render(True, None, None)
+ self.assertIn("bi-check-circle-fill", value)
+ self.assertIn("green", value)
+ value = column.render(False, None, None)
+ self.assertIn("bi-x-circle-fill", value)
+ self.assertIn("red", value)
+
+ def test_true_color(self):
+ column = tables.BooleanIconColumn(true_color="blue")
+ value = column.render(True, None, None)
+ self.assertIn("bi-check-circle-fill", value)
+ self.assertIn("blue", value)
+ value = column.render(False, None, None)
+ self.assertEqual(value, "")
+
+ def test_true_icon(self):
+ column = tables.BooleanIconColumn(true_icon="dash")
+ value = column.render(True, None, None)
+ self.assertIn("bi-dash", value)
+ self.assertIn("green", value)
+ value = column.render(False, None, None)
+ self.assertEqual(value, "")
+
+ def test_false_color(self):
+ column = tables.BooleanIconColumn(show_false_icon=True, false_color="blue")
+ value = column.render(False, None, None)
+ self.assertIn("bi-x-circle-fill", value)
+ self.assertIn("blue", value)
+ value = column.render(True, None, None)
+ self.assertIn("bi-check-circle-fill", value)
+ self.assertIn("green", value)
+
+ def test_false_icon(self):
+ column = tables.BooleanIconColumn(show_false_icon=True, false_icon="dash")
+ value = column.render(False, None, None)
+ self.assertIn("bi-dash", value)
+ self.assertIn("red", value)
+ value = column.render(True, None, None)
+ self.assertIn("bi-check-circle-fill", value)
+ self.assertIn("green", value)
+
+
class AccountTableTest(TestCase):
"""Tests for the AccountTable in this app."""
From e8006f950cd65cd3eaaf44f0a9130f97992f4dbd Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 22 Aug 2024 19:22:11 +0000
Subject: [PATCH 052/125] Bump werkzeug from 3.0.3 to 3.0.4
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.3 to 3.0.4.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.3...3.0.4)
---
updated-dependencies:
- dependency-name: werkzeug
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
requirements/dev-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt
index 16e31a22..5b6bf856 100644
--- a/requirements/dev-requirements.txt
+++ b/requirements/dev-requirements.txt
@@ -233,7 +233,7 @@ wcwidth==0.2.13
# via prompt-toolkit
websockets==12.0
# via sphinx-autobuild
-werkzeug==3.0.3
+werkzeug==3.0.4
# via -r requirements/dev-requirements.in
# The following packages are considered to be unsafe in a requirements file:
From 3fa3d217a009b601b113e49f8157acdb5183bd83 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 12:35:57 -0700
Subject: [PATCH 053/125] Use the new BooleanIconColumn in the audit tables
---
gregor_django/gregor_anvil/audit/upload_workspace_audit.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/gregor_django/gregor_anvil/audit/upload_workspace_audit.py b/gregor_django/gregor_anvil/audit/upload_workspace_audit.py
index 6d0af9d7..1bb0fb90 100644
--- a/gregor_django/gregor_anvil/audit/upload_workspace_audit.py
+++ b/gregor_django/gregor_anvil/audit/upload_workspace_audit.py
@@ -6,6 +6,7 @@
from django.db.models import Q, QuerySet
from ..models import CombinedConsortiumDataWorkspace, UploadWorkspace
+from ..tables import BooleanIconColumn
# from primed.primed_anvil.tables import BooleanIconColumn
from .base import GREGoRAudit, GREGoRAuditResult
@@ -136,7 +137,7 @@ class UploadWorkspaceAuditTable(tables.Table):
managed_group = tables.Column(linkify=True)
# is_shared = tables.Column()
access = tables.Column()
- can_compute = tables.Column()
+ can_compute = BooleanIconColumn(show_false_icon=True, null=True)
note = tables.Column()
# action = tables.Column()
action = tables.TemplateColumn(template_name="gregor_anvil/snippets/upload_workspace_audit_action_button.html")
From 25500cb4805854c9508f08f8996e2724c8d29249 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 14:27:37 -0700
Subject: [PATCH 054/125] Change is_ready_for_compute to a date field
This gives us a little more information while accomplishing the same
thing.
---
add_upload_workspace_audit_test_data.py | 8 ++---
.../audit/upload_workspace_audit.py | 4 +--
...dy_to_compute_to_date_ready_for_compute.py | 31 +++++++++++++++++++
gregor_django/gregor_anvil/models.py | 7 +++--
gregor_django/gregor_anvil/tests/factories.py | 1 +
.../gregor_anvil/tests/test_audit.py | 8 +++--
.../gregor_anvil/tests/test_models.py | 12 +++----
.../gregor_anvil/uploadcycle_detail.html | 8 ++---
8 files changed, 57 insertions(+), 22 deletions(-)
create mode 100644 gregor_django/gregor_anvil/migrations/0031_uploadcycle_change_is_ready_to_compute_to_date_ready_for_compute.py
diff --git a/add_upload_workspace_audit_test_data.py b/add_upload_workspace_audit_test_data.py
index e33783cb..01ed9ceb 100644
--- a/add_upload_workspace_audit_test_data.py
+++ b/add_upload_workspace_audit_test_data.py
@@ -29,7 +29,6 @@
upload_cycle = factories.UploadCycleFactory.create(
cycle=1,
is_future=True,
- is_ready_for_compute=False,
)
workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
@@ -41,7 +40,6 @@
upload_cycle = factories.UploadCycleFactory.create(
cycle=2,
is_current=True,
- is_ready_for_compute=False,
)
workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
@@ -83,8 +81,9 @@
upload_cycle = factories.UploadCycleFactory.create(
cycle=3,
is_current=True,
- is_ready_for_compute=True,
)
+upload_cycle.date_ready_for_compute = upload_cycle.start_date
+upload_cycle.save()
workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
research_center=rc,
@@ -124,7 +123,6 @@
upload_cycle = factories.UploadCycleFactory.create(
cycle=4,
is_past=True,
- is_ready_for_compute=True,
)
workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
@@ -166,7 +164,6 @@
upload_cycle = factories.UploadCycleFactory.create(
cycle=5,
is_past=True,
- is_ready_for_compute=True,
)
workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
@@ -201,7 +198,6 @@
upload_cycle = factories.UploadCycleFactory.create(
cycle=6,
is_past=True,
- is_ready_for_compute=True,
)
workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=upload_cycle,
diff --git a/gregor_django/gregor_anvil/audit/upload_workspace_audit.py b/gregor_django/gregor_anvil/audit/upload_workspace_audit.py
index 1bb0fb90..285a4ee2 100644
--- a/gregor_django/gregor_anvil/audit/upload_workspace_audit.py
+++ b/gregor_django/gregor_anvil/audit/upload_workspace_audit.py
@@ -304,7 +304,7 @@ def _audit_workspace_and_rc_uploader_group(self, upload_workspace, managed_group
**audit_result_args,
)
)
- elif upload_cycle.is_current and not upload_cycle.is_ready_for_compute:
+ elif upload_cycle.is_current and not upload_cycle.date_ready_for_compute:
note = self.RC_UPLOADERS_CURRENT_CYCLE_BEFORE_COMPUTE
if (
current_sharing
@@ -324,7 +324,7 @@ def _audit_workspace_and_rc_uploader_group(self, upload_workspace, managed_group
**audit_result_args,
)
)
- elif upload_cycle.is_current and upload_cycle.is_ready_for_compute:
+ elif upload_cycle.is_current and upload_cycle.date_ready_for_compute:
note = self.RC_UPLOADERS_CURRENT_CYCLE_AFTER_COMPUTE
if (
current_sharing
diff --git a/gregor_django/gregor_anvil/migrations/0031_uploadcycle_change_is_ready_to_compute_to_date_ready_for_compute.py b/gregor_django/gregor_anvil/migrations/0031_uploadcycle_change_is_ready_to_compute_to_date_ready_for_compute.py
new file mode 100644
index 00000000..309c19e5
--- /dev/null
+++ b/gregor_django/gregor_anvil/migrations/0031_uploadcycle_change_is_ready_to_compute_to_date_ready_for_compute.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.15 on 2024-08-22 21:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gregor_anvil', '0030_researchcenter_non_member_group'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='historicaluploadcycle',
+ name='is_ready_for_compute',
+ ),
+ migrations.RemoveField(
+ model_name='uploadcycle',
+ name='is_ready_for_compute',
+ ),
+ migrations.AddField(
+ model_name='historicaluploadcycle',
+ name='date_ready_for_compute',
+ field=models.DateField(blank=True, default=None, help_text='Date that this workspace was ready for RC uploaders to run compute.', null=True),
+ ),
+ migrations.AddField(
+ model_name='uploadcycle',
+ name='date_ready_for_compute',
+ field=models.DateField(blank=True, default=None, help_text='Date that this workspace was ready for RC uploaders to run compute.', null=True),
+ ),
+ ]
diff --git a/gregor_django/gregor_anvil/models.py b/gregor_django/gregor_anvil/models.py
index 64a26764..b721ad54 100644
--- a/gregor_django/gregor_anvil/models.py
+++ b/gregor_django/gregor_anvil/models.py
@@ -159,8 +159,11 @@ class UploadCycle(TimeStampedModel, models.Model):
)
start_date = models.DateField(help_text="The start date of this upload cycle.")
end_date = models.DateField(help_text="The end date of this upload cycle.")
- is_ready_for_compute = models.BooleanField(
- help_text="Boolean indicator of whether workspace writers should be able to run compute.", default=False
+ date_ready_for_compute = models.DateField(
+ help_text="Date that this workspace was ready for RC uploaders to run compute.",
+ blank=True,
+ null=True,
+ default=None,
)
note = models.TextField(blank=True, help_text="Additional notes.")
diff --git a/gregor_django/gregor_anvil/tests/factories.py b/gregor_django/gregor_anvil/tests/factories.py
index 50d8a6af..2a1f64a0 100644
--- a/gregor_django/gregor_anvil/tests/factories.py
+++ b/gregor_django/gregor_anvil/tests/factories.py
@@ -43,6 +43,7 @@ class Params:
is_past = Trait(
start_date=timezone.localdate() - timedelta(days=100),
end_date=timezone.localdate() - timedelta(days=10),
+ date_ready_for_compute=timezone.localdate() - timedelta(days=90),
)
is_current = Trait(
start_date=timezone.localdate() - timedelta(days=45),
diff --git a/gregor_django/gregor_anvil/tests/test_audit.py b/gregor_django/gregor_anvil/tests/test_audit.py
index ccf4cefc..92741654 100644
--- a/gregor_django/gregor_anvil/tests/test_audit.py
+++ b/gregor_django/gregor_anvil/tests/test_audit.py
@@ -885,7 +885,7 @@ def setUp(self):
self.upload_workspace = factories.UploadWorkspaceFactory.create(
research_center=self.research_center,
upload_cycle__is_current=True,
- upload_cycle__is_ready_for_compute=False,
+ upload_cycle__date_ready_for_compute=None,
)
self.auth_domain = self.upload_workspace.workspace.authorization_domains.get()
self.other_group = ManagedGroupFactory.create()
@@ -1443,8 +1443,12 @@ def setUp(self):
self.rc_uploader_group = ManagedGroupFactory.create()
self.research_center = factories.ResearchCenterFactory.create(uploader_group=self.rc_uploader_group)
self.upload_workspace = factories.UploadWorkspaceFactory.create(
- research_center=self.research_center, upload_cycle__is_current=True, upload_cycle__is_ready_for_compute=True
+ research_center=self.research_center,
+ upload_cycle__is_current=True,
)
+ # Set date ready for compute to a non-null value.
+ self.upload_workspace.upload_cycle.date_ready_for_compute = self.upload_workspace.upload_cycle.start_date
+ self.upload_workspace.upload_cycle.save()
self.auth_domain = self.upload_workspace.workspace.authorization_domains.get()
self.other_group = ManagedGroupFactory.create()
self.anvil_admins = ManagedGroupFactory.create(name="anvil-admins")
diff --git a/gregor_django/gregor_anvil/tests/test_models.py b/gregor_django/gregor_anvil/tests/test_models.py
index f9883337..2273b3c0 100644
--- a/gregor_django/gregor_anvil/tests/test_models.py
+++ b/gregor_django/gregor_anvil/tests/test_models.py
@@ -219,7 +219,7 @@ def test_model_saving(self):
instance.full_clean()
instance.save()
self.assertIsInstance(instance, models.UploadCycle)
- self.assertEqual(instance.is_ready_for_compute, False)
+ self.assertIsNone(instance.date_ready_for_compute)
def test_model_saving_with_note(self):
"""Creation using the model constructor and .save() works."""
@@ -410,7 +410,6 @@ def test_get_partner_upload_workspaces_full_test(self):
version=2,
date_completed=None,
)
- # import ipdb; ipdb.set_trace()
included_workspaces = upload_cycle.get_partner_upload_workspaces()
self.assertEqual(included_workspaces.count(), 2)
self.assertNotIn(workspace_1, included_workspaces)
@@ -418,13 +417,14 @@ def test_get_partner_upload_workspaces_full_test(self):
self.assertIn(workspace_3, included_workspaces)
self.assertNotIn(workspace_4, included_workspaces)
- def test_is_ready_for_compute(self):
+ def test_date_ready_for_compute(self):
"""UploadCycle is ready for compute if all PartnerUploadWorkspaces have date_completed."""
upload_cycle = factories.UploadCycleFactory.create()
- self.assertFalse(upload_cycle.is_ready_for_compute)
- upload_cycle.is_ready_for_compute = True
+ self.assertIsNone(upload_cycle.date_ready_for_compute)
+ date = timezone.localdate()
+ upload_cycle.date_ready_for_compute = date
upload_cycle.save()
- self.assertEqual(upload_cycle.is_ready_for_compute, True)
+ self.assertEqual(upload_cycle.date_ready_for_compute, date)
def test_is_current_is_past_is_future(self):
# Previous cycle.
diff --git a/gregor_django/templates/gregor_anvil/uploadcycle_detail.html b/gregor_django/templates/gregor_anvil/uploadcycle_detail.html
index a93dea16..e86c51eb 100644
--- a/gregor_django/templates/gregor_anvil/uploadcycle_detail.html
+++ b/gregor_django/templates/gregor_anvil/uploadcycle_detail.html
@@ -17,11 +17,11 @@
Start date: {{ object.start_date }}
End date: {{ object.end_date }}
- Ready for compute:
- {% if object.is_ready_for_compute %}
- Yes
+ Date ready for compute:
+ {% if object.date_ready_for_compute %}
+ {{ object.date_ready_for_compute }}
{% else %}
- No
+ —
{% endif %}
From a4c33f1a7458d14b395896d19f76daeb9c5aef34 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 14:43:13 -0700
Subject: [PATCH 055/125] Change date_qc_completed to a DateField
We don't need the timestamp and date fields are easier to fill in.
---
...oadworkspace_date_qc_completed_and_more.py | 23 +++++++++++++++++++
gregor_django/gregor_anvil/models.py | 2 +-
.../gregor_anvil/tests/test_audit.py | 8 ++-----
.../gregor_anvil/tests/test_views.py | 4 ++--
4 files changed, 28 insertions(+), 9 deletions(-)
create mode 100644 gregor_django/gregor_anvil/migrations/0032_alter_historicaluploadworkspace_date_qc_completed_and_more.py
diff --git a/gregor_django/gregor_anvil/migrations/0032_alter_historicaluploadworkspace_date_qc_completed_and_more.py b/gregor_django/gregor_anvil/migrations/0032_alter_historicaluploadworkspace_date_qc_completed_and_more.py
new file mode 100644
index 00000000..17ef5e04
--- /dev/null
+++ b/gregor_django/gregor_anvil/migrations/0032_alter_historicaluploadworkspace_date_qc_completed_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.15 on 2024-08-22 21:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gregor_anvil', '0031_uploadcycle_change_is_ready_to_compute_to_date_ready_for_compute'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='historicaluploadworkspace',
+ name='date_qc_completed',
+ field=models.DateField(blank=True, default=None, help_text='Date that QC was completed for this workspace. If null, QC is not complete.', null=True),
+ ),
+ migrations.AlterField(
+ model_name='uploadworkspace',
+ name='date_qc_completed',
+ field=models.DateField(blank=True, default=None, help_text='Date that QC was completed for this workspace. If null, QC is not complete.', null=True),
+ ),
+ ]
diff --git a/gregor_django/gregor_anvil/models.py b/gregor_django/gregor_anvil/models.py
index b721ad54..791555ec 100644
--- a/gregor_django/gregor_anvil/models.py
+++ b/gregor_django/gregor_anvil/models.py
@@ -233,7 +233,7 @@ class UploadWorkspace(TimeStampedModel, BaseWorkspaceData):
upload_cycle = models.ForeignKey(UploadCycle, on_delete=models.PROTECT)
"""The UploadCycle associated with this workspace."""
- date_qc_completed = models.DateTimeField(
+ date_qc_completed = models.DateField(
help_text="Date that QC was completed for this workspace. If null, QC is not complete.",
blank=True,
null=True,
diff --git a/gregor_django/gregor_anvil/tests/test_audit.py b/gregor_django/gregor_anvil/tests/test_audit.py
index 92741654..e0de8ba9 100644
--- a/gregor_django/gregor_anvil/tests/test_audit.py
+++ b/gregor_django/gregor_anvil/tests/test_audit.py
@@ -2576,9 +2576,7 @@ def setUp(self):
self.upload_workspace = factories.UploadWorkspaceFactory.create(
research_center=self.research_center,
upload_cycle__is_past=True,
- date_qc_completed=fake.date_time_this_year(
- before_now=True, after_now=False, tzinfo=timezone.get_current_timezone()
- ),
+ date_qc_completed=fake.date_this_year(before_today=True, after_today=False),
)
self.auth_domain = self.upload_workspace.workspace.authorization_domains.get()
self.other_group = ManagedGroupFactory.create()
@@ -3148,9 +3146,7 @@ def setUp(self):
self.upload_workspace = factories.UploadWorkspaceFactory.create(
research_center=self.research_center,
upload_cycle__is_past=True,
- date_qc_completed=fake.date_time_this_year(
- before_now=True, after_now=False, tzinfo=timezone.get_current_timezone()
- ),
+ date_qc_completed=fake.date_this_year(before_today=True, after_today=False),
)
self.auth_domain = self.upload_workspace.workspace.authorization_domains.get()
# Create a corresponding combined workspace.
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index 106477ef..f7b2102b 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -2432,7 +2432,7 @@ def test_context_needs_action_table_stop_sharing(self):
self.upload_workspace.upload_cycle.start_date = timezone.now() - timedelta(days=20)
self.upload_workspace.upload_cycle.end_date = timezone.now() - timedelta(days=10)
self.upload_workspace.upload_cycle.save()
- self.upload_workspace.date_qc_completed = timezone.now() - timedelta(days=1)
+ self.upload_workspace.date_qc_completed = timezone.localdate() - timedelta(days=1)
self.upload_workspace.save()
group = acm_factories.ManagedGroupFactory.create()
rc = self.upload_workspace.research_center
@@ -2776,7 +2776,7 @@ def test_context_needs_action_table_stop_sharing(self):
upload_workspace = factories.UploadWorkspaceFactory.create(
upload_cycle=self.upload_cycle,
research_center__uploader_group=group,
- date_qc_completed=timezone.now() - timedelta(days=1),
+ date_qc_completed=timezone.localdate() - timedelta(days=1),
)
self.upload_cycle.start_date = timezone.now() - timedelta(days=20)
self.upload_cycle.end_date = timezone.now() - timedelta(days=10)
From b2f458dbc57b58d57a6226312fdaa259f1a3548b Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 14:47:28 -0700
Subject: [PATCH 056/125] Change date_completed to a DateField for the combined
workspace
The timestamp is not necessary and it's easier to fill in a date
field.
---
...umdataworkspace_date_completed_and_more.py | 23 +++++++++++++++++++
gregor_django/gregor_anvil/models.py | 2 +-
.../gregor_anvil/tests/test_audit.py | 6 ++---
3 files changed, 27 insertions(+), 4 deletions(-)
create mode 100644 gregor_django/gregor_anvil/migrations/0033_alter_combinedconsortiumdataworkspace_date_completed_and_more.py
diff --git a/gregor_django/gregor_anvil/migrations/0033_alter_combinedconsortiumdataworkspace_date_completed_and_more.py b/gregor_django/gregor_anvil/migrations/0033_alter_combinedconsortiumdataworkspace_date_completed_and_more.py
new file mode 100644
index 00000000..ffb73a3e
--- /dev/null
+++ b/gregor_django/gregor_anvil/migrations/0033_alter_combinedconsortiumdataworkspace_date_completed_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.15 on 2024-08-22 21:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gregor_anvil', '0032_alter_historicaluploadworkspace_date_qc_completed_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='combinedconsortiumdataworkspace',
+ name='date_completed',
+ field=models.DateField(blank=True, default=None, help_text='Date that data preparation in this workspace was completed.', null=True),
+ ),
+ migrations.AlterField(
+ model_name='historicalcombinedconsortiumdataworkspace',
+ name='date_completed',
+ field=models.DateField(blank=True, default=None, help_text='Date that data preparation in this workspace was completed.', null=True),
+ ),
+ ]
diff --git a/gregor_django/gregor_anvil/models.py b/gregor_django/gregor_anvil/models.py
index 791555ec..8cd5ffec 100644
--- a/gregor_django/gregor_anvil/models.py
+++ b/gregor_django/gregor_anvil/models.py
@@ -298,7 +298,7 @@ class CombinedConsortiumDataWorkspace(TimeStampedModel, BaseWorkspaceData):
"""A model to track a workspace that has data combined from multiple upload workspaces."""
upload_cycle = models.ForeignKey(UploadCycle, on_delete=models.PROTECT)
- date_completed = models.DateTimeField(
+ date_completed = models.DateField(
help_text="Date that data preparation in this workspace was completed.",
blank=True,
null=True,
diff --git a/gregor_django/gregor_anvil/tests/test_audit.py b/gregor_django/gregor_anvil/tests/test_audit.py
index e0de8ba9..173db996 100644
--- a/gregor_django/gregor_anvil/tests/test_audit.py
+++ b/gregor_django/gregor_anvil/tests/test_audit.py
@@ -7,7 +7,6 @@
from anvil_consortium_manager.tests.factories import ManagedGroupFactory, WorkspaceGroupSharingFactory
from django.conf import settings
from django.test import TestCase, override_settings
-from django.utils import timezone
from faker import Faker
from ..audit import upload_workspace_audit
@@ -3152,8 +3151,9 @@ def setUp(self):
# Create a corresponding combined workspace.
self.combined_workspace = factories.CombinedConsortiumDataWorkspaceFactory.create(
upload_cycle=self.upload_workspace.upload_cycle,
- date_completed=fake.date_time_this_year(
- before_now=True, after_now=False, tzinfo=timezone.get_current_timezone()
+ date_completed=fake.date_this_year(
+ before_today=True,
+ after_today=False,
),
)
self.other_group = ManagedGroupFactory.create()
From aac2753ad7e935f22db1c3a06169d1bc8b11ce41 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 15:07:04 -0700
Subject: [PATCH 057/125] Add second exception to try/except block
The AnVILGroupNotFound exception is returned when you try to share
a workspace with a group, but the group does not exist in AnVIL.
The view that resolves the audit should handle this like an API
error (even though the return code is success - 200 - it is not
actually a success). Without this exception in the try/except block,
the htmx functionality did not return either "Handled!" or "Error!",
which is confusing to the user.
---
.../gregor_anvil/tests/test_views.py | 39 +++++++++++++++++++
gregor_django/gregor_anvil/views.py | 3 +-
2 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index f7b2102b..33d79c67 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -3930,3 +3930,42 @@ def test_post_new_stop_sharing_anvil_api_error_htmx(self):
# No messages were added.
messages = [m.message for m in get_messages(response.wsgi_request)]
self.assertEqual(len(messages), 0)
+
+ def test_post_new_share_as_writer_group_not_found_on_anvil_htmx(self):
+ group = acm_factories.ManagedGroupFactory.create()
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ workspace__billing_project__name="test-bp",
+ workspace__name="test-ws",
+ upload_cycle__is_future=True,
+ research_center__uploader_group=group,
+ )
+ acls = [
+ {
+ "email": group.email,
+ "accessLevel": "WRITER",
+ "canShare": False,
+ "canCompute": False,
+ }
+ ]
+ self.anvil_response_mock.add(
+ responses.PATCH,
+ self.api_client.rawls_entry_point + "/api/workspaces/test-bp/test-ws/acl?inviteUsersNotFound=false",
+ # Successful error code, but with usersNotFound
+ status=200,
+ json={"invitesSent": [], "usersNotFound": acls, "usersUpdated": []},
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuditResolve.htmx_error)
+ # The sharing object was not deleted.
+ self.assertEqual(acm_models.WorkspaceGroupSharing.objects.count(), 0)
+ # sharing.refresh_from_db()
+ # self.assertEqual(sharing.created, date_created)
+ # self.assertEqual(sharing.modified, date_created)
+ # No messages were added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index 37f2ba70..6790f5bb 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -3,6 +3,7 @@
AnVILConsortiumManagerStaffEditRequired,
AnVILConsortiumManagerStaffViewRequired,
)
+from anvil_consortium_manager.exceptions import AnVILGroupNotFound
from anvil_consortium_manager.models import Account, ManagedGroup, Workspace, WorkspaceGroupSharing
from django.contrib import messages
from django.contrib.auth import get_user_model
@@ -343,7 +344,7 @@ def form_valid(self, form):
sharing.full_clean()
sharing.save()
sharing.anvil_create_or_update()
- except AnVILAPIError as e:
+ except (AnVILAPIError, AnVILGroupNotFound) as e:
if self.request.htmx:
return HttpResponse(self.htmx_error)
else:
From e7e4b28d9b3df10e8a15655b26a551c9847d1983 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 15:58:49 -0700
Subject: [PATCH 058/125] Add explanation template text for UploadWorkspace
audits
---
.../upload_workspace_audit_explanation.html | 48 ++++++++++++++++++-
.../gregor_anvil/upload_workspace_audit.html | 12 +++--
.../upload_workspace_audit_resolve.html | 4 +-
3 files changed, 58 insertions(+), 6 deletions(-)
diff --git a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_audit_explanation.html b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_audit_explanation.html
index dd6d86a4..d53023d6 100644
--- a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_audit_explanation.html
+++ b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_audit_explanation.html
@@ -1 +1,47 @@
-XXX
+
+
+
+
+
+
+
+
+
+ This audit checks that workspace sharing is appropriate for the current point in the upload cycle.
+ Sharing with the following groups are checked:
+
+
The authorization domain of the workspace
+
GREGOR_DCC_ADMINS
+
GREGOR_DCC_WRITERS
+
The upload group for the Research Center associated with this upload workspace
+
Any additional groups that the workspace is shared with
+
+ Note that groups associated with AnVIL (e.g., anvil-admins, anvil-devs) are ignored by the audit.
+
+
The audit result categories are explained below.
+
+
+
Verified includes the following:
+
+
The workspace is shared with a group with appropriate permissions.
+
The workspace is not shared with a group that it should not be shared with.
- Auditing sharing for {% if object %}{{ object }}{% else %}all upload workspaces{% endif %}.
+
+ Auditing workspace sharing for {{ object }}.
+ All records in the "Needs action" table should be handled by clicking on the action button.
+ Any records in the "Errors" table should be reported.
+
+ {% include "gregor_anvil/snippets/upload_workspace_audit_explanation.html" %}
+
Audit results
-{% include "gregor_anvil/snippets/upload_workspace_audit_explanation.html" %}
-
{% include "__audit_tables.html" with verified_table=verified_table needs_action_table=needs_action_table errors_table=errors_table %}
{% endblock content %}
diff --git a/gregor_django/templates/gregor_anvil/upload_workspace_audit_resolve.html b/gregor_django/templates/gregor_anvil/upload_workspace_audit_resolve.html
index 7ee9c159..7cdcd7d0 100644
--- a/gregor_django/templates/gregor_anvil/upload_workspace_audit_resolve.html
+++ b/gregor_django/templates/gregor_anvil/upload_workspace_audit_resolve.html
@@ -14,11 +14,13 @@
+{% endblock action_buttons %}
From 6cfb57070fd649e335f64ad7e70ec816f992ce1a Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 16:26:45 -0700
Subject: [PATCH 060/125] Subclass the UploadCycleForm forms for creating and
updating
We want different fields to be avilable when updating vs. creating
an UploadCycle. Split the form into subclasses specific to each
action.
---
gregor_django/gregor_anvil/forms.py | 16 +++++
.../gregor_anvil/tests/test_forms.py | 67 ++++++++++++++++++-
.../gregor_anvil/tests/test_views.py | 2 +-
gregor_django/gregor_anvil/views.py | 2 +-
4 files changed, 82 insertions(+), 5 deletions(-)
diff --git a/gregor_django/gregor_anvil/forms.py b/gregor_django/gregor_anvil/forms.py
index 76990a07..cf864f6e 100644
--- a/gregor_django/gregor_anvil/forms.py
+++ b/gregor_django/gregor_anvil/forms.py
@@ -19,15 +19,31 @@ class Meta:
"cycle",
"start_date",
"end_date",
+ "date_ready_for_compute",
"note",
)
widgets = {
"start_date": CustomDateInput(),
"end_date": CustomDateInput(),
+ "date_ready_for_compute": CustomDateInput(),
}
+class UploadCycleCreateForm(UploadCycleForm):
+ """Form to create an UploadCycle object."""
+
+ class Meta(UploadCycleForm.Meta):
+ exclude = ("date_ready_for_compute",) # noqa: DJ006
+
+
+class UploadCycleUpdateForm(UploadCycleForm):
+ """Form to update an UploadCycle object."""
+
+ class Meta(UploadCycleForm.Meta):
+ exclude = ("cycle",) # noqa: DJ006
+
+
class UploadWorkspaceForm(forms.ModelForm):
"""Form for a UploadWorkspace object."""
diff --git a/gregor_django/gregor_anvil/tests/test_forms.py b/gregor_django/gregor_anvil/tests/test_forms.py
index 61ea420c..e29858f5 100644
--- a/gregor_django/gregor_anvil/tests/test_forms.py
+++ b/gregor_django/gregor_anvil/tests/test_forms.py
@@ -9,10 +9,10 @@
from . import factories
-class UploadCycleForm(TestCase):
- """Tests for the UploadCycleForm class."""
+class UploadCycleCreateFormTest(TestCase):
+ """Tests for the UploadCycleCreateForm class."""
- form_class = forms.UploadCycleForm
+ form_class = forms.UploadCycleCreateForm
def test_valid(self):
"""Form is valid with necessary input."""
@@ -78,6 +78,67 @@ def test_valid_note(self):
self.assertTrue(form.is_valid())
+class UploadCycleUpdateFormTest(TestCase):
+ """Tests for the UploadCycleUpdateForm class."""
+
+ form_class = forms.UploadCycleUpdateForm
+
+ def test_valid(self):
+ """Form is valid with necessary input."""
+ form_data = {
+ "start_date": date.today(),
+ "end_date": date.today() + timedelta(days=1),
+ }
+ form = self.form_class(data=form_data)
+ self.assertTrue(form.is_valid())
+
+ def test_invalid_missing_start_date(self):
+ """Form is invalid when missing start_date."""
+ form_data = {
+ # "start_date": date.today(),
+ "end_date": date.today() + timedelta(days=1),
+ }
+ form = self.form_class(data=form_data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertIn("start_date", form.errors)
+ self.assertEqual(len(form.errors["start_date"]), 1)
+ self.assertIn("required", form.errors["start_date"][0])
+
+ def test_invalid_missing_end_date(self):
+ """Form is invalid when missing cycle."""
+ form_data = {
+ "start_date": date.today(),
+ # "end_date": date.today() + timedelta(days=1),
+ }
+ form = self.form_class(data=form_data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertIn("end_date", form.errors)
+ self.assertEqual(len(form.errors["end_date"]), 1)
+ self.assertIn("required", form.errors["end_date"][0])
+
+ def test_valid_note(self):
+ """Form is valid with a note."""
+ form_data = {
+ "start_date": date.today(),
+ "end_date": date.today() + timedelta(days=1),
+ "note": "my test note",
+ }
+ form = self.form_class(data=form_data)
+ self.assertTrue(form.is_valid())
+
+ def test_valid_date_ready_for_compute(self):
+ """Form is valid with date_ready_for_compute."""
+ form_data = {
+ "start_date": date.today(),
+ "end_date": date.today() + timedelta(days=60),
+ "date_ready_for_compute": date.today() + timedelta(days=10),
+ }
+ form = self.form_class(data=form_data)
+ self.assertTrue(form.is_valid())
+
+
class UploadWorkspaceFormTest(TestCase):
"""Tests for the UploadWorkspace class."""
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index 5dd2bdfc..93e1a388 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -731,7 +731,7 @@ def test_has_form_in_context(self):
self.client.force_login(self.user)
response = self.client.get(self.get_url())
self.assertTrue("form" in response.context_data)
- self.assertIsInstance(response.context_data["form"], forms.UploadCycleForm)
+ self.assertIsInstance(response.context_data["form"], forms.UploadCycleCreateForm)
def test_can_create_an_object(self):
"""Posting valid data to the form creates an object."""
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index 6790f5bb..191eb3d2 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -105,7 +105,7 @@ class UploadCycleCreate(AnVILConsortiumManagerStaffEditRequired, SuccessMessageM
"""View to create a new UploadCycle object."""
model = models.UploadCycle
- form_class = forms.UploadCycleForm
+ form_class = forms.UploadCycleCreateForm
success_message = "Successfully created Upload Cycle."
From d798e99a7cf8f1f4a2dbfff93d8f28e064ccd103 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Thu, 22 Aug 2024 16:40:01 -0700
Subject: [PATCH 061/125] Add clean check for date_ready_for_compute
In the UploadCycle clean method, verify that date_ready_for_compute
is between the start and end dates.
---
gregor_django/gregor_anvil/models.py | 6 ++++
.../gregor_anvil/tests/test_models.py | 31 +++++++++++++++++++
2 files changed, 37 insertions(+)
diff --git a/gregor_django/gregor_anvil/models.py b/gregor_django/gregor_anvil/models.py
index 8cd5ffec..2e4499c2 100644
--- a/gregor_django/gregor_anvil/models.py
+++ b/gregor_django/gregor_anvil/models.py
@@ -187,6 +187,12 @@ def clean(self):
# End date must be after start date.
if self.start_date and self.end_date and self.start_date >= self.end_date:
raise ValidationError("end_date must be after start_date!")
+ # date_ready_for_compute must be after start_date
+ if self.start_date and self.date_ready_for_compute and self.start_date > self.date_ready_for_compute:
+ raise ValidationError("date_ready_for_compute must be after start_date!")
+ # date_ready_for_compute must be before end_date
+ if self.end_date and self.date_ready_for_compute and self.end_date < self.date_ready_for_compute:
+ raise ValidationError("date_ready_for_compute must be before end_date!")
def get_partner_upload_workspaces(self):
"""Return a queryset of PartnerUploadWorkspace objects that are included in this upload cycle.
diff --git a/gregor_django/gregor_anvil/tests/test_models.py b/gregor_django/gregor_anvil/tests/test_models.py
index 2273b3c0..21cb31c6 100644
--- a/gregor_django/gregor_anvil/tests/test_models.py
+++ b/gregor_django/gregor_anvil/tests/test_models.py
@@ -303,8 +303,39 @@ def test_start_date_equal_to_end_date(self):
self.assertEqual(len(e.exception.message_dict), 1)
self.assertIn(NON_FIELD_ERRORS, e.exception.message_dict)
self.assertEqual(len(e.exception.error_dict[NON_FIELD_ERRORS]), 1)
+ self.assertIn("end_date", e.exception.message_dict[NON_FIELD_ERRORS][0])
self.assertIn("after start_date", e.exception.message_dict[NON_FIELD_ERRORS][0])
+ def test_start_date_after_date_ready_for_compute(self):
+ today = date.today()
+ instance = factories.UploadCycleFactory.build(
+ start_date=today + timedelta(days=1),
+ end_date=today + timedelta(days=10),
+ date_ready_for_compute=today,
+ )
+ with self.assertRaises(ValidationError) as e:
+ instance.full_clean()
+ self.assertEqual(len(e.exception.message_dict), 1)
+ self.assertIn(NON_FIELD_ERRORS, e.exception.message_dict)
+ self.assertEqual(len(e.exception.error_dict[NON_FIELD_ERRORS]), 1)
+ self.assertIn("date_ready_for_compute", e.exception.message_dict[NON_FIELD_ERRORS][0])
+ self.assertIn("after start_date", e.exception.message_dict[NON_FIELD_ERRORS][0])
+
+ def test_date_ready_for_compute_after_end_date(self):
+ today = date.today()
+ instance = factories.UploadCycleFactory.build(
+ start_date=today,
+ end_date=today + timedelta(days=10),
+ date_ready_for_compute=today + timedelta(days=11),
+ )
+ with self.assertRaises(ValidationError) as e:
+ instance.full_clean()
+ self.assertEqual(len(e.exception.message_dict), 1)
+ self.assertIn(NON_FIELD_ERRORS, e.exception.message_dict)
+ self.assertEqual(len(e.exception.error_dict[NON_FIELD_ERRORS]), 1)
+ self.assertIn("date_ready_for_compute", e.exception.message_dict[NON_FIELD_ERRORS][0])
+ self.assertIn("before end_date", e.exception.message_dict[NON_FIELD_ERRORS][0])
+
def test_get_partner_upload_workspaces_no_date_completed(self):
"""PartnerUploadWorkspace with no date_completed is not included."""
upload_cycle = factories.UploadCycleFactory.create()
From 0db27a56aeb18a62e3a6a65bcbb5bf0d4f764db4 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Fri, 23 Aug 2024 10:25:18 -0700
Subject: [PATCH 062/125] Add view to update an Upload Cycle
---
gregor_django/gregor_anvil/forms.py | 14 +-
.../gregor_anvil/tests/test_views.py | 162 ++++++++++++++++++
gregor_django/gregor_anvil/urls.py | 1 +
gregor_django/gregor_anvil/views.py | 11 +-
.../gregor_anvil/uploadcycle_form.html | 8 +-
5 files changed, 192 insertions(+), 4 deletions(-)
diff --git a/gregor_django/gregor_anvil/forms.py b/gregor_django/gregor_anvil/forms.py
index cf864f6e..f936330c 100644
--- a/gregor_django/gregor_anvil/forms.py
+++ b/gregor_django/gregor_anvil/forms.py
@@ -34,14 +34,24 @@ class UploadCycleCreateForm(UploadCycleForm):
"""Form to create an UploadCycle object."""
class Meta(UploadCycleForm.Meta):
- exclude = ("date_ready_for_compute",) # noqa: DJ006
+ fields = (
+ "cycle",
+ "start_date",
+ "end_date",
+ "note",
+ )
class UploadCycleUpdateForm(UploadCycleForm):
"""Form to update an UploadCycle object."""
class Meta(UploadCycleForm.Meta):
- exclude = ("cycle",) # noqa: DJ006
+ fields = (
+ "start_date",
+ "end_date",
+ "date_ready_for_compute",
+ "note",
+ )
class UploadWorkspaceForm(forms.ModelForm):
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index 93e1a388..bde09aa8 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -834,6 +834,168 @@ def test_post_blank_data(self):
self.assertEqual(models.UploadCycle.objects.count(), 0)
+class UploadCycleUpdateTest(TestCase):
+ def setUp(self):
+ """Set up test class."""
+ self.factory = RequestFactory()
+ # 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)
+ )
+ # Data for forms
+ self.start_date = date.today()
+ self.end_date = self.start_date + timedelta(days=10)
+
+ def get_url(self, *args):
+ """Get the url for the view being tested."""
+ return reverse("gregor_anvil:upload_cycles:update", args=args)
+
+ def get_view(self):
+ """Return the view being tested."""
+ return views.UploadCycleUpdate.as_view()
+
+ def test_view_redirect_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(1))
+ self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(1))
+
+ def test_status_code_with_user_permission(self):
+ """Returns successful response code."""
+ obj = factories.UploadCycleFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(obj.cycle))
+ self.assertEqual(response.status_code, 200)
+
+ def test_access_with_view_permission(self):
+ """Raises permission denied if user has only view permission."""
+ user_with_view_perm = User.objects.create_user(username="test-other", password="test-other")
+ user_with_view_perm.user_permissions.add(
+ Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME)
+ )
+ request = self.factory.get(self.get_url(1))
+ request.user = user_with_view_perm
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request, pk=1)
+
+ def test_access_without_user_permission(self):
+ """Raises permission denied if user has no permissions."""
+ user_no_perms = User.objects.create_user(username="test-none", password="test-none")
+ request = self.factory.get(self.get_url(1))
+ request.user = user_no_perms
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request, pk=1)
+
+ def test_has_form_in_context(self):
+ """Response includes a form."""
+ obj = factories.UploadCycleFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(obj.cycle))
+ self.assertTrue("form" in response.context_data)
+ self.assertIsInstance(response.context_data["form"], forms.UploadCycleUpdateForm)
+
+ def test_can_update_an_object(self):
+ """Posting valid data to the form creates an object."""
+ obj = factories.UploadCycleFactory.create(is_current=True)
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(obj.pk),
+ {
+ "start_date": obj.start_date,
+ "end_date": obj.end_date,
+ "date_ready_for_compute": timezone.localdate(),
+ "note": "",
+ },
+ )
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(models.UploadCycle.objects.count(), 1)
+ obj.refresh_from_db()
+ self.assertEqual(obj.date_ready_for_compute, timezone.localdate())
+ # History is added.
+ self.assertEqual(obj.history.count(), 2)
+ self.assertEqual(obj.history.latest().history_type, "~")
+
+ def test_success_message(self):
+ """Response includes a success message if successful."""
+ obj = factories.UploadCycleFactory.create(is_current=True)
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(obj.cycle),
+ {
+ "start_date": obj.start_date,
+ "end_date": obj.end_date,
+ "date_ready_for_compute": timezone.localdate(),
+ "note": "",
+ },
+ )
+ self.assertEqual(response.status_code, 302)
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertEqual(views.UploadCycleUpdate.success_message, str(messages[0]))
+
+ def test_redirects_to_object_detail(self):
+ """After successfully creating an object, view redirects to the object's detail page."""
+ obj = factories.UploadCycleFactory.create(is_current=True)
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(obj.cycle),
+ {
+ "start_date": obj.start_date,
+ "end_date": obj.end_date,
+ "date_ready_for_compute": timezone.localdate(),
+ "note": "",
+ },
+ )
+ self.assertRedirects(response, obj.get_absolute_url())
+
+ def test_object_does_not_exist(self):
+ """Raises 404 when object doesn't exist."""
+ request = self.factory.get(self.get_url(1))
+ request.user = self.user
+ with self.assertRaises(Http404):
+ self.get_view()(request, pk=1)
+
+ def test_invalid_input(self):
+ """Posting invalid data does not create an object."""
+ obj = factories.UploadCycleFactory.create(is_current=True)
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(obj.cycle),
+ {"start_date": self.start_date, "end_date": self.end_date, "date_ready_for_compute": "foo"},
+ )
+ self.assertEqual(response.status_code, 200)
+ form = response.context_data["form"]
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors.keys()), 1)
+ self.assertIn("date_ready_for_compute", form.errors.keys())
+ self.assertEqual(len(form.errors["date_ready_for_compute"]), 1)
+ obj.refresh_from_db()
+ self.assertIsNone(obj.date_ready_for_compute)
+
+ def test_post_blank_data_ready_for_compute(self):
+ """Can successfully post blank data for date_ready_for_compute."""
+ obj = factories.UploadCycleFactory.create(is_current=True)
+ start_date = obj.start_date
+ end_date = obj.end_date
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(obj.cycle),
+ {
+ "start_date": start_date + timedelta(days=1),
+ "end_date": end_date + timedelta(days=1),
+ },
+ )
+ self.assertEqual(response.status_code, 302)
+ obj.refresh_from_db()
+ self.assertEqual(obj.start_date, start_date + timedelta(days=1))
+ self.assertEqual(obj.end_date, end_date + timedelta(days=1))
+ self.assertIsNone(obj.date_ready_for_compute)
+
+
class UploadCycleDetailTest(TestCase):
"""Tests for the UploadCycle view."""
diff --git a/gregor_django/gregor_anvil/urls.py b/gregor_django/gregor_anvil/urls.py
index eaad0a61..7bcc1f5a 100644
--- a/gregor_django/gregor_anvil/urls.py
+++ b/gregor_django/gregor_anvil/urls.py
@@ -33,6 +33,7 @@
[
path("/", views.UploadCycleDetail.as_view(), name="detail"),
path("new/", views.UploadCycleCreate.as_view(), name="new"),
+ path("/update/", views.UploadCycleUpdate.as_view(), name="update"),
path("", views.UploadCycleList.as_view(), name="list"),
],
"upload_cycles",
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index 191eb3d2..a4b53925 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -13,7 +13,7 @@
from django.forms import Form
from django.http import Http404, HttpResponse
from django.utils.translation import gettext_lazy as _
-from django.views.generic import CreateView, DetailView, FormView, TemplateView
+from django.views.generic import CreateView, DetailView, FormView, TemplateView, UpdateView
from django_tables2 import MultiTableMixin, SingleTableView
from gregor_django.users.tables import UserTable
@@ -109,6 +109,15 @@ class UploadCycleCreate(AnVILConsortiumManagerStaffEditRequired, SuccessMessageM
success_message = "Successfully created Upload Cycle."
+class UploadCycleUpdate(AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, UpdateView):
+ """View to create a new UploadCycle object."""
+
+ model = models.UploadCycle
+ slug_field = "cycle"
+ form_class = forms.UploadCycleUpdateForm
+ success_message = "Successfully updated Upload Cycle."
+
+
class UploadCycleDetail(AnVILConsortiumManagerStaffViewRequired, MultiTableMixin, DetailView):
"""View to show details about an `UploadCycle`."""
diff --git a/gregor_django/templates/gregor_anvil/uploadcycle_form.html b/gregor_django/templates/gregor_anvil/uploadcycle_form.html
index 59cfe860..5a2d3581 100644
--- a/gregor_django/templates/gregor_anvil/uploadcycle_form.html
+++ b/gregor_django/templates/gregor_anvil/uploadcycle_form.html
@@ -5,7 +5,13 @@
{% block content %}
-
Add a new Upload Cycle
+
+ {% if object %}
+ Update Upload Cycle {{ object }}
+ {% else %}
+ Add a new Upload Cycle
+ {% endif %}
+
From f391c28ff8dcb35476ab99323c1ac5e7bbea6f28 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Mon, 26 Aug 2024 16:03:13 -0700
Subject: [PATCH 086/125] Add view to run auth domain audit for a single upload
workspace
---
.../upload_workspace_auth_domain_audit.py | 8 +-
.../gregor_anvil/tests/test_views.py | 380 +++++++++++++++++-
gregor_django/gregor_anvil/urls.py | 17 +-
gregor_django/gregor_anvil/views.py | 40 +-
...rkspace_auth_domain_audit_explanation.html | 29 ++
.../upload_workspace_auth_domain_audit.html | 25 ++
6 files changed, 490 insertions(+), 9 deletions(-)
create mode 100644 gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html
create mode 100644 gregor_django/templates/gregor_anvil/upload_workspace_auth_domain_audit.html
diff --git a/gregor_django/gregor_anvil/audit/upload_workspace_auth_domain_audit.py b/gregor_django/gregor_anvil/audit/upload_workspace_auth_domain_audit.py
index 7a3f6416..2026bec9 100644
--- a/gregor_django/gregor_anvil/audit/upload_workspace_auth_domain_audit.py
+++ b/gregor_django/gregor_anvil/audit/upload_workspace_auth_domain_audit.py
@@ -115,10 +115,10 @@ class UploadWorkspaceAuthDomainAuditTable(tables.Table):
# is_shared = tables.Column()
role = tables.Column()
note = tables.Column()
- # action = tables.Column()
- action = tables.TemplateColumn(
- template_name="gregor_anvil/snippets/upload_workspace_auth_domain_audit_action_button.html"
- )
+ action = tables.Column()
+ # action = tables.TemplateColumn(
+ # template_name="gregor_anvil/snippets/upload_workspace_auth_domain_audit_action_button.html"
+ # )
class Meta:
attrs = {"class": "table align-middle"}
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index 99f30ded..216f6621 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -25,7 +25,7 @@
from gregor_django.users.tests.factories import UserFactory
from .. import forms, models, tables, views
-from ..audit import upload_workspace_sharing_audit
+from ..audit import upload_workspace_auth_domain_audit, upload_workspace_sharing_audit
from . import factories
# from .utils import AnVILAPIMockTestMixin
@@ -4169,3 +4169,381 @@ def test_post_new_share_as_writer_group_not_found_on_anvil_htmx(self):
# No messages were added.
messages = [m.message for m in get_messages(response.wsgi_request)]
self.assertEqual(len(messages), 0)
+
+
+class UploadWorkspaceAuthDomainAuditByWorkspaceTest(AnVILAPIMockTestMixin, TestCase):
+ """Tests for the UploadWorkspaceAuthDomainAuditByWorkspace view."""
+
+ def setUp(self):
+ """Set up test class."""
+ super().setUp()
+ self.factory = RequestFactory()
+ # 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)
+ )
+ self.user.user_permissions.add(
+ Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME)
+ )
+ self.upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+
+ def get_url(self, *args):
+ """Get the url for the view being tested."""
+ return reverse(
+ "gregor_anvil:audit:upload_workspaces:auth_domains:by_upload_workspace",
+ args=args,
+ )
+
+ def get_view(self):
+ """Return the view being tested."""
+ return views.UploadWorkspaceAuthDomainAuditByWorkspace.as_view()
+
+ def test_view_redirect_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("foo", "bar"))
+ self.assertRedirects(
+ response,
+ resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo", "bar"),
+ )
+
+ def test_status_code_with_user_permission_view(self):
+ """Returns successful response code if the user has view permission."""
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertEqual(response.status_code, 200)
+
+ def test_access_without_user_permission(self):
+ """Raises permission denied if user has no permissions."""
+ user_no_perms = User.objects.create_user(username="test-none", password="test-none")
+ request = self.factory.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ request.user = user_no_perms
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(
+ request,
+ billing_project_slug=self.upload_workspace.workspace.billing_project.name,
+ workspace_slug=self.upload_workspace.workspace.name,
+ )
+
+ def test_invalid_billing_project_name(self):
+ """Raises a 404 error with an invalid object billing project."""
+ request = self.factory.get(self.get_url("foo", self.upload_workspace.workspace.name))
+ request.user = self.user
+ with self.assertRaises(Http404):
+ self.get_view()(
+ request,
+ billing_project_slug="foo",
+ workspace_slug=self.upload_workspace.workspace.name,
+ )
+
+ def test_invalid_workspace_name(self):
+ """Raises a 404 error with an invalid workspace name."""
+ request = self.factory.get(self.get_url(self.upload_workspace.workspace.billing_project.name, "foo"))
+ request.user = self.user
+ with self.assertRaises(Http404):
+ self.get_view()(
+ request,
+ billing_project_slug=self.upload_workspace.workspace.billing_project.name,
+ workspace_slug="foo",
+ )
+
+ def test_context_audit_results(self):
+ """The audit_results exists in the context."""
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertIn("audit_results", response.context_data)
+ audit_results = response.context_data["audit_results"]
+ self.assertIsInstance(
+ audit_results,
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit,
+ )
+ self.assertTrue(audit_results.completed)
+ self.assertEqual(audit_results.queryset.count(), 1)
+ self.assertIn(self.upload_workspace, audit_results.queryset)
+
+ def test_context_audit_results_does_not_include_other_workspaces(self):
+ """The audit_results does not include other workspaces."""
+ other_workspace = factories.UploadWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ audit_results = response.context_data["audit_results"]
+ self.assertEqual(audit_results.queryset.count(), 1)
+ self.assertNotIn(other_workspace, audit_results.queryset)
+
+ def test_context_verified_table_access(self):
+ """verified_table shows a record when audit has verified access."""
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=self.upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertIn("verified_table", response.context_data)
+ table = response.context_data["verified_table"]
+ self.assertIsInstance(
+ table,
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.upload_workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), acm_models.GroupGroupMembership.ADMIN)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_ADMINS,
+ )
+ self.assertEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_verified_table_no_access(self):
+ """verified_table shows a record when audit has verifiednotmember."""
+ self.upload_workspace.upload_cycle.start_date = timezone.localdate() - timedelta(days=20)
+ self.upload_workspace.upload_cycle.start_date = timezone.localdate() - timedelta(days=10)
+ self.upload_workspace.upload_cycle.save()
+ # Create and share a combined workspace.
+ factories.CombinedConsortiumDataWorkspaceFactory(
+ upload_cycle=self.upload_workspace.upload_cycle,
+ date_completed=timezone.localdate() - timedelta(days=1),
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertIn("verified_table", response.context_data)
+ table = response.context_data["verified_table"]
+ self.assertIsInstance(
+ table,
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.upload_workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), None)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_AFTER_COMBINED,
+ )
+ self.assertEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_needs_action_table_add_member(self):
+ """needs_action_table shows a record when audit finds that access needs to be granted."""
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertIn("needs_action_table", response.context_data)
+ table = response.context_data["needs_action_table"]
+ self.assertIsInstance(
+ table,
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.upload_workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertIsNone(table.rows[0].get_cell_value("role"))
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_BEFORE_COMBINED,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_needs_action_table_add_admin(self):
+ """needs_action_table shows a record when audit finds that access needs to be granted."""
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertIn("needs_action_table", response.context_data)
+ table = response.context_data["needs_action_table"]
+ self.assertIsInstance(
+ table,
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.upload_workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertIsNone(table.rows[0].get_cell_value("role"))
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_ADMINS,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_needs_action_table_remove(self):
+ """needs_action_table shows a record when audit finds that access needs to be removed."""
+ self.upload_workspace.upload_cycle.start_date = timezone.localdate() - timedelta(days=20)
+ self.upload_workspace.upload_cycle.start_date = timezone.localdate() - timedelta(days=10)
+ self.upload_workspace.upload_cycle.save()
+ # Create and share a combined workspace.
+ factories.CombinedConsortiumDataWorkspaceFactory(
+ upload_cycle=self.upload_workspace.upload_cycle,
+ date_completed=timezone.localdate() - timedelta(days=1),
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=self.upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertIn("needs_action_table", response.context_data)
+ table = response.context_data["needs_action_table"]
+ self.assertIsInstance(
+ table,
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.upload_workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), acm_models.GroupGroupMembership.MEMBER)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_AFTER_COMBINED,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_error_table_remove(self):
+ """error table shows a record when audit finds that access needs to be removed."""
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=self.upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertIn("errors_table", response.context_data)
+ table = response.context_data["errors_table"]
+ self.assertIsInstance(
+ table,
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.upload_workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), acm_models.GroupGroupMembership.MEMBER)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.OTHER_GROUP,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_errors_table_change_to_member(self):
+ """needs action table shows a record when audit finds that access needs to be removed."""
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=self.upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertIn("errors_table", response.context_data)
+ table = response.context_data["errors_table"]
+ self.assertIsInstance(
+ table,
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.upload_workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), acm_models.GroupGroupMembership.ADMIN)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_BEFORE_COMBINED,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+ def test_context_needs_action_table_change_to_admin(self):
+ """error table shows a record when audit finds that access needs to be removed."""
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=self.upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Check the table in the context.
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ self.upload_workspace.workspace.billing_project.name,
+ self.upload_workspace.workspace.name,
+ )
+ )
+ self.assertIn("needs_action_table", response.context_data)
+ table = response.context_data["needs_action_table"]
+ self.assertIsInstance(
+ table,
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditTable,
+ )
+ self.assertEqual(len(table.rows), 1)
+ self.assertEqual(table.rows[0].get_cell_value("workspace"), self.upload_workspace)
+ self.assertEqual(table.rows[0].get_cell_value("managed_group"), group)
+ self.assertEqual(table.rows[0].get_cell_value("role"), acm_models.GroupGroupMembership.MEMBER)
+ self.assertEqual(
+ table.rows[0].get_cell_value("note"),
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_ADMINS,
+ )
+ self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
diff --git a/gregor_django/gregor_anvil/urls.py b/gregor_django/gregor_anvil/urls.py
index ac259fda..d79381f1 100644
--- a/gregor_django/gregor_anvil/urls.py
+++ b/gregor_django/gregor_anvil/urls.py
@@ -68,17 +68,28 @@
"sharing",
)
-upload_workspace_sharing_audit_patterns = (
+upload_workspace_auth_domain_audit_patterns = (
+ [
+ path(
+ "//",
+ views.UploadWorkspaceAuthDomainAuditByWorkspace.as_view(),
+ name="by_upload_workspace",
+ ),
+ ],
+ "auth_domains",
+)
+
+upload_workspace_audit_patterns = (
[
path("sharing/", include(upload_workspace_sharing_audit_patterns)),
- # path("auth_domain/", include(upload_workspace_auth_domain_audit_patterns)),
+ path("auth_domain/", include(upload_workspace_auth_domain_audit_patterns)),
],
"upload_workspaces",
)
audit_patterns = (
[
- path("upload_workspaces/", include(upload_workspace_sharing_audit_patterns)),
+ path("upload_workspaces/", include(upload_workspace_audit_patterns)),
],
"audit",
)
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index e402a6d3..78896730 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -19,7 +19,7 @@
from gregor_django.users.tables import UserTable
from . import forms, models, tables
-from .audit import upload_workspace_sharing_audit
+from .audit import upload_workspace_auth_domain_audit, upload_workspace_sharing_audit
User = get_user_model()
@@ -366,3 +366,41 @@ def form_valid(self, form):
return HttpResponse(self.htmx_success)
else:
return super().form_valid(form)
+
+
+class UploadWorkspaceAuthDomainAuditByWorkspace(AnVILConsortiumManagerStaffEditRequired, DetailView):
+ """View to audit UploadWorkspace sharing for a specific UploadWorkspace."""
+
+ template_name = "gregor_anvil/upload_workspace_auth_domain_audit.html"
+ model = models.UploadWorkspace
+
+ def get_object(self, queryset=None):
+ """Look up the UploadWorkspace by billing project and name."""
+ # Filter the queryset based on kwargs.
+ billing_project_slug = self.kwargs.get("billing_project_slug", None)
+ workspace_slug = self.kwargs.get("workspace_slug", None)
+ queryset = models.UploadWorkspace.objects.filter(
+ workspace__billing_project__name=billing_project_slug,
+ workspace__name=workspace_slug,
+ )
+ try:
+ # Get the single item from the filtered queryset
+ obj = queryset.get()
+ except queryset.model.DoesNotExist:
+ raise Http404(
+ _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name}
+ )
+ return obj
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ # Run the audit.
+ audit = upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit(
+ queryset=self.model.objects.filter(pk=self.object.pk)
+ )
+ audit.run_audit()
+ context["verified_table"] = audit.get_verified_table()
+ context["errors_table"] = audit.get_errors_table()
+ context["needs_action_table"] = audit.get_needs_action_table()
+ context["audit_results"] = audit
+ return context
diff --git a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html
new file mode 100644
index 00000000..5243aa21
--- /dev/null
+++ b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+ This audit checks that auth domain membership is appropriate for the current point in the upload cycle.
+ Membership for the following groups are checked:
+
+
GREGOR_DCC_ADMINS
+
GREGOR_DCC_WRITERS
+
GREGOR_DCC_MEMBERS
+
The upload group for the Research Center associated with this upload workspace
+
The member group for the Research Center associated with this upload workspace
+
The non-member group for the Research Center associated with this upload workspace
+
Any additional groups that the workspace is shared with
+
+ Note that groups associated with AnVIL (e.g., anvil-admins, anvil-devs) are ignored by the audit.
+
+
+ Auditing workspace auth domain membership for {{ object }}.
+ All records in the "Needs action" table should be handled by clicking on the action button.
+ Any records in the "Errors" table should be reported.
+
+ {% include "gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html" %}
+
+
+
+
Audit results
+
+{% include "__audit_tables.html" with verified_table=verified_table needs_action_table=needs_action_table errors_table=errors_table %}
+
+{% endblock content %}
From b83bbba0d0a21a8177171b69a3912a9e064a1f28 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Tue, 27 Aug 2024 13:46:38 -0700
Subject: [PATCH 087/125] Add a view to resolve auth domain audits
---
.../gregor_anvil/tests/test_views.py | 1336 +++++++++++++++++
gregor_django/gregor_anvil/urls.py | 7 +-
gregor_django/gregor_anvil/views.py | 122 +-
...rkspace_auth_domain_audit_explanation.html | 23 +-
...d_workspace_auth_domain_audit_resolve.html | 44 +
...pload_workspace_sharing_audit_resolve.html | 2 +-
6 files changed, 1523 insertions(+), 11 deletions(-)
create mode 100644 gregor_django/templates/gregor_anvil/upload_workspace_auth_domain_audit_resolve.html
diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py
index 216f6621..8de12849 100644
--- a/gregor_django/gregor_anvil/tests/test_views.py
+++ b/gregor_django/gregor_anvil/tests/test_views.py
@@ -4547,3 +4547,1339 @@ def test_context_needs_action_table_change_to_admin(self):
upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_ADMINS,
)
self.assertNotEqual(table.rows[0].get_cell_value("action"), "—")
+
+
+class UploadWorkspaceAuthDomainAuditResolveTest(AnVILAPIMockTestMixin, TestCase):
+ """Tests for the UploadWorkspaceAuthDomainAuditResolve view."""
+
+ def setUp(self):
+ """Set up test class."""
+ super().setUp()
+ self.factory = RequestFactory()
+ # 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)
+ )
+ self.user.user_permissions.add(
+ Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME)
+ )
+
+ def get_url(self, *args):
+ """Get the url for the view being tested."""
+ return reverse(
+ "gregor_anvil:audit:upload_workspaces:auth_domains:resolve",
+ args=args,
+ )
+
+ def get_view(self):
+ """Return the view being tested."""
+ return views.UploadWorkspaceAuthDomainAuditResolve.as_view()
+
+ def test_view_redirect_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("foo", "bar", "foobar"))
+ self.assertRedirects(
+ response,
+ resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo", "bar", "foobar"),
+ )
+
+ def test_status_code_with_user_permission_staff_edit(self):
+ """Returns successful response code if the user has staff edit permission."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+
+ def test_status_code_with_user_permission_staff_view(self):
+ """Returns 403 response code if the user has staff view permission."""
+ user_view = User.objects.create_user(username="test-view", password="test-view")
+ user_view.user_permissions.add(
+ Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME)
+ )
+ self.client.force_login(self.user)
+ request = self.factory.get(self.get_url("foo", "bar", "foobar"))
+ request.user = user_view
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
+ def test_status_code_with_user_permission_view(self):
+ """Returns forbidden response code if the user has view permission."""
+ user = User.objects.create_user(username="test-none", password="test-none")
+ user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME))
+ request = self.factory.get(self.get_url("foo", "bar", "foobar"))
+ request.user = user
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
+ def test_access_without_user_permission(self):
+ """Raises permission denied if user has no permissions."""
+ user_no_perms = User.objects.create_user(username="test-none", password="test-none")
+ request = self.factory.get(self.get_url("foo", "bar", "foobar"))
+ request.user = user_no_perms
+ with self.assertRaises(PermissionDenied):
+ self.get_view()(request)
+
+ def test_get_billing_project_does_not_exist(self):
+ """Raises a 404 error with an invalid billing project."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url("foo", upload_workspace.workspace.name, group.name))
+ self.assertEqual(response.status_code, 404)
+
+ def test_get_workspace_name_does_not_exist(self):
+ """Raises a 404 error with an invalid billing project."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(upload_workspace.workspace.billing_project.name, "foo", group.name))
+ self.assertEqual(response.status_code, 404)
+
+ def test_get_group_does_not_exist(self):
+ """get request raises a 404 error with an non-existent email."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(
+ upload_workspace.workspace.billing_project.name,
+ upload_workspace.workspace.name,
+ "foo",
+ )
+ )
+ self.assertEqual(response.status_code, 404)
+
+ def test_get_context_audit_result(self):
+ """The audit_results exists in the context."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ self.assertIsInstance(
+ response.context_data["audit_result"],
+ upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAuditResult,
+ )
+
+ def test_get_verified_member(self):
+ """Get request with VerifiedMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, upload_workspace_auth_domain_audit.VerifiedMember)
+ self.assertEqual(audit_result.workspace, upload_workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_BEFORE_COMBINED
+ )
+
+ def test_get_verified_admin(self):
+ """Get request with VerifiedAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, upload_workspace_auth_domain_audit.VerifiedAdmin)
+ self.assertEqual(audit_result.workspace, upload_workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_ADMINS
+ )
+
+ def test_get_verified_not_member(self):
+ """Get request with VerifiedNotMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, upload_workspace_auth_domain_audit.VerifiedNotMember)
+ self.assertEqual(audit_result.workspace, upload_workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.OTHER_GROUP
+ )
+
+ def test_get_add_member(self):
+ """Get request with AddMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, upload_workspace_auth_domain_audit.AddMember)
+ self.assertEqual(audit_result.workspace, upload_workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_BEFORE_COMBINED
+ )
+
+ def test_get_add_admin(self):
+ """Get request with AddAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, upload_workspace_auth_domain_audit.AddAdmin)
+ self.assertEqual(audit_result.workspace, upload_workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_ADMINS
+ )
+
+ def test_get_change_to_member(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, upload_workspace_auth_domain_audit.ChangeToMember)
+ self.assertEqual(audit_result.workspace, upload_workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_BEFORE_COMBINED
+ )
+
+ def test_get_change_to_admin(self):
+ """Get request with ChangeToAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, upload_workspace_auth_domain_audit.ChangeToAdmin)
+ self.assertEqual(audit_result.workspace, upload_workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.DCC_ADMINS
+ )
+
+ def test_get_remove(self):
+ """Get request with ChangeToAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertIn("audit_result", response.context_data)
+ audit_result = response.context_data["audit_result"]
+ self.assertIsInstance(audit_result, upload_workspace_auth_domain_audit.Remove)
+ self.assertEqual(audit_result.workspace, upload_workspace)
+ self.assertEqual(audit_result.managed_group, group)
+ self.assertEqual(
+ audit_result.note, upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit.OTHER_GROUP
+ )
+
+ def test_post_billing_project_does_not_exist(self):
+ """Raises a 404 error with an invalid billing project."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.post(self.get_url("foo", upload_workspace.workspace.name, group.name))
+ self.assertEqual(response.status_code, 404)
+
+ def test_post_workspace_name_does_not_exist(self):
+ """Raises a 404 error with an invalid billing project."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.post(self.get_url(upload_workspace.workspace.billing_project.name, "foo", group.name))
+ self.assertEqual(response.status_code, 404)
+
+ def test_post_group_does_not_exist(self):
+ """post request raises a 404 error with an non-existent email."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(
+ upload_workspace.workspace.billing_project.name,
+ upload_workspace.workspace.name,
+ "foo",
+ )
+ )
+ self.assertEqual(response.status_code, 404)
+
+ def test_post_verified_member(self):
+ """Get request with VerifiedMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, upload_workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_post_verified_admin(self):
+ """Get request with VerifiedAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, upload_workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_post_verified_not_member(self):
+ """Get request with VerifiedNotMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, upload_workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_add_member(self):
+ """Get request with AddMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, upload_workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership = acm_models.GroupGroupMembership.objects.get(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ )
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+
+ def test_post_add_admin(self):
+ """Get request with AddAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ self.client.force_login(self.user)
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, upload_workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership = acm_models.GroupGroupMembership.objects.get(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ )
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+
+ def test_post_change_to_member(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, upload_workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertGreater(membership.modified, membership.created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+
+ def test_post_change_to_admin(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, upload_workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertGreater(membership.modified, membership.created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+
+ def test_post_remove_admin(self):
+ """Post request with Remove result for an admin membership."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, upload_workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_remove_member(self):
+ """Post request with Remove result for a member membership."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertRedirects(response, upload_workspace.get_absolute_url())
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_htmx_verified_member(self):
+ """Get request with VerifiedMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_post_htmx_verified_admin(self):
+ """Get request with VerifiedAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(upload_cycle__is_future=True)
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+
+ def test_post_htmx_verified_not_member(self):
+ """Get request with VerifiedNotMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create()
+ group = acm_factories.ManagedGroupFactory.create()
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_htmx_add_member(self):
+ """Get request with AddMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership = acm_models.GroupGroupMembership.objects.get(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ )
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+
+ def test_post_htmx_add_admin(self):
+ """Get request with AddAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ self.client.force_login(self.user)
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership = acm_models.GroupGroupMembership.objects.get(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ )
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+
+ def test_post_htmx_change_to_member(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertGreater(membership.modified, membership.created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+
+ def test_post_htmx_change_to_admin(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertGreater(membership.modified, membership.created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+
+ def test_post_htmx_remove_admin(self):
+ """Post request with Remove result for an admin membership."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_htmx_remove_member(self):
+ """Post request with Remove result for a member membership."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_success)
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+
+ def test_post_api_error_add_member(self):
+ """Get request with AddMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # No memberships were created.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_add_admin(self):
+ """Get request with AddAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ self.client.force_login(self.user)
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # No memberships were created.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_change_to_member_error_on_put_call(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_change_to_member_error_on_delete_call(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_change_to_admin_error_on_delete_call(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_change_to_admin_error_on_put_call(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_remove_admin(self):
+ """Post request with Remove result for an admin membership."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_api_error_remove_member(self):
+ """Post request with Remove result for a member membership."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name)
+ )
+ self.assertEqual(response.status_code, 200)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # Error message was added.
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 1)
+ self.assertIn("AnVIL API Error", str(messages[0]))
+
+ def test_post_htmx_api_error_add_member(self):
+ """Get request with AddMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # No memberships were created.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+ # No messages
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_htmx_api_error_add_admin(self):
+ """Get request with AddAdmin result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ self.client.force_login(self.user)
+ # Add the mocked API response.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # No memberships were created.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 0)
+ # No messages
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_htmx_api_error_change_to_member_error_on_put_call(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # No messages
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_htmx_api_error_change_to_member_error_on_delete_call(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name="GREGOR_DCC_WRITERS")
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # No messages
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_htmx_api_error_change_to_admin_error_on_delete_call(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # No messages
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_htmx_api_error_change_to_admin_error_on_put_call(self):
+ """Get request with ChangeToMember result."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create(name=settings.ANVIL_DCC_ADMINS_GROUP_NAME)
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ # Add the mocked API responses - one to create and one to delete.
+ # Note that the auth domain group is created automatically by the factory using the workspace name.
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # No messages
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_htmx_api_error_remove_admin(self):
+ """Post request with Remove result for an admin membership."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.ADMIN,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/admin/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.ADMIN)
+ # No messages
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
+
+ def test_post_htmx_api_error_remove_member(self):
+ """Post request with Remove result for a member membership."""
+ upload_workspace = factories.UploadWorkspaceFactory.create(
+ upload_cycle__is_future=True, workspace__name="test-ws"
+ )
+ group = acm_factories.ManagedGroupFactory.create()
+ date_created = timezone.now() - timedelta(weeks=3)
+ with freeze_time(date_created):
+ membership = acm_factories.GroupGroupMembershipFactory.create(
+ parent_group=upload_workspace.workspace.authorization_domains.first(),
+ child_group=group,
+ role=acm_models.GroupGroupMembership.MEMBER,
+ )
+ self.anvil_response_mock.add(
+ responses.DELETE,
+ self.api_client.sam_entry_point + f"/api/groups/v1/auth_test-ws/member/{group.name}@firecloud.org",
+ status=500,
+ json=ErrorResponseFactory().response,
+ )
+ self.client.force_login(self.user)
+ header = {"HTTP_HX-Request": "true"}
+ response = self.client.post(
+ self.get_url(upload_workspace.workspace.billing_project.name, upload_workspace.workspace.name, group.name),
+ **header,
+ )
+ self.assertEqual(response.content.decode(), views.UploadWorkspaceAuthDomainAuditResolve.htmx_error)
+ # The membership was not updated.
+ self.assertEqual(acm_models.GroupGroupMembership.objects.count(), 1)
+ membership.refresh_from_db()
+ self.assertEqual(membership.created, date_created)
+ self.assertEqual(membership.modified, date_created)
+ self.assertEqual(membership.role, acm_models.GroupGroupMembership.MEMBER)
+ # No messages
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertEqual(len(messages), 0)
diff --git a/gregor_django/gregor_anvil/urls.py b/gregor_django/gregor_anvil/urls.py
index d79381f1..8e260949 100644
--- a/gregor_django/gregor_anvil/urls.py
+++ b/gregor_django/gregor_anvil/urls.py
@@ -50,7 +50,7 @@
[
# path("all/", views.UploadWorkspaceSharingAuditAll.as_view(), name="all"),
path(
- "resolve///",
+ "resolve////",
views.UploadWorkspaceSharingAuditResolve.as_view(),
name="resolve",
),
@@ -70,6 +70,11 @@
upload_workspace_auth_domain_audit_patterns = (
[
+ path(
+ "resolve////",
+ views.UploadWorkspaceAuthDomainAuditResolve.as_view(),
+ name="resolve",
+ ),
path(
"//",
views.UploadWorkspaceAuthDomainAuditByWorkspace.as_view(),
diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py
index 78896730..5ddd052b 100644
--- a/gregor_django/gregor_anvil/views.py
+++ b/gregor_django/gregor_anvil/views.py
@@ -4,7 +4,13 @@
AnVILConsortiumManagerStaffViewRequired,
)
from anvil_consortium_manager.exceptions import AnVILGroupNotFound
-from anvil_consortium_manager.models import Account, ManagedGroup, Workspace, WorkspaceGroupSharing
+from anvil_consortium_manager.models import (
+ Account,
+ GroupGroupMembership,
+ ManagedGroup,
+ Workspace,
+ WorkspaceGroupSharing,
+)
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.messages.views import SuccessMessageMixin
@@ -404,3 +410,117 @@ def get_context_data(self, **kwargs):
context["needs_action_table"] = audit.get_needs_action_table()
context["audit_results"] = audit
return context
+
+
+class UploadWorkspaceAuthDomainAuditResolve(AnVILConsortiumManagerStaffEditRequired, FormView):
+ """View to resolve UploadWorkspace auth domain audit results."""
+
+ form_class = Form
+ template_name = "gregor_anvil/upload_workspace_auth_domain_audit_resolve.html"
+ htmx_success = """ Handled!"""
+ htmx_error = """ Error!"""
+
+ def get_upload_workspace(self):
+ """Look up the UploadWorkspace by billing project and name."""
+ # Filter the queryset based on kwargs.
+ billing_project_slug = self.kwargs.get("billing_project_slug", None)
+ workspace_slug = self.kwargs.get("workspace_slug", None)
+ queryset = models.UploadWorkspace.objects.filter(
+ workspace__billing_project__name=billing_project_slug,
+ workspace__name=workspace_slug,
+ )
+ try:
+ # Get the single item from the filtered queryset
+ obj = queryset.get()
+ except queryset.model.DoesNotExist:
+ raise Http404(
+ _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name}
+ )
+ return obj
+
+ def get_managed_group(self, queryset=None):
+ """Look up the ManagedGroup by name."""
+ try:
+ obj = ManagedGroup.objects.get(name=self.kwargs.get("managed_group_slug", None))
+ except ManagedGroup.DoesNotExist:
+ raise Http404("No ManagedGroups found matching the query")
+ return obj
+
+ def get_audit_result(self):
+ audit = upload_workspace_auth_domain_audit.UploadWorkspaceAuthDomainAudit()
+ # No way to set the group queryset, since it is dynamically determined by the workspace.
+ audit.audit_workspace_and_group(self.upload_workspace, self.managed_group)
+ # Set to completed, because we are just running this one specific check.
+ audit.completed = True
+ return audit.get_all_results()[0]
+
+ def get(self, request, *args, **kwargs):
+ self.upload_workspace = self.get_upload_workspace()
+ self.managed_group = self.get_managed_group()
+ self.audit_result = self.get_audit_result()
+ return super().get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ self.upload_workspace = self.get_upload_workspace()
+ self.managed_group = self.get_managed_group()
+ self.audit_result = self.get_audit_result()
+ return super().post(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["upload_workspace"] = self.upload_workspace
+ context["managed_group"] = self.managed_group
+ context["audit_result"] = self.audit_result
+ return context
+
+ def get_success_url(self):
+ return self.upload_workspace.get_absolute_url()
+
+ def form_valid(self, form):
+ # Handle the result.
+ try:
+ with transaction.atomic():
+ # Set up the membership instance.
+ if self.audit_result.current_membership_instance:
+ membership = self.audit_result.current_membership_instance
+ else:
+ membership = GroupGroupMembership(
+ parent_group=self.upload_workspace.workspace.authorization_domains.first(),
+ child_group=self.managed_group,
+ )
+ # Now process the result.
+ if isinstance(self.audit_result, upload_workspace_auth_domain_audit.VerifiedMember):
+ pass
+ elif isinstance(self.audit_result, upload_workspace_auth_domain_audit.VerifiedAdmin):
+ pass
+ elif isinstance(self.audit_result, upload_workspace_auth_domain_audit.VerifiedNotMember):
+ pass
+ elif isinstance(self.audit_result, upload_workspace_auth_domain_audit.Remove):
+ membership.anvil_delete()
+ membership.delete()
+ else:
+ if isinstance(self.audit_result, upload_workspace_auth_domain_audit.ChangeToMember):
+ membership.anvil_delete()
+ membership.role = GroupGroupMembership.MEMBER
+ elif isinstance(self.audit_result, upload_workspace_auth_domain_audit.ChangeToAdmin):
+ membership.anvil_delete()
+ membership.role = GroupGroupMembership.ADMIN
+ else:
+ if isinstance(self.audit_result, upload_workspace_auth_domain_audit.AddMember):
+ membership.role = GroupGroupMembership.MEMBER
+ elif isinstance(self.audit_result, upload_workspace_auth_domain_audit.AddAdmin):
+ membership.role = GroupGroupMembership.ADMIN
+ membership.full_clean()
+ membership.save()
+ membership.anvil_create()
+ except (AnVILAPIError, AnVILGroupNotFound) as e:
+ if self.request.htmx:
+ return HttpResponse(self.htmx_error)
+ else:
+ messages.error(self.request, "AnVIL API Error: " + str(e))
+ return super().form_invalid(form)
+ # Otherwise, the audit resolution succeeded.
+ if self.request.htmx:
+ return HttpResponse(self.htmx_success)
+ else:
+ return super().form_valid(form)
diff --git a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html
index 5243aa21..edd9af6c 100644
--- a/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html
+++ b/gregor_django/templates/gregor_anvil/snippets/upload_workspace_auth_domain_audit_explanation.html
@@ -10,15 +10,22 @@
This audit checks that auth domain membership is appropriate for the current point in the upload cycle.
- Membership for the following groups are checked:
+ Membership is expected to be as follows.
-
GREGOR_DCC_ADMINS
-
GREGOR_DCC_WRITERS
-
GREGOR_DCC_MEMBERS
-
The upload group for the Research Center associated with this upload workspace
-
The member group for the Research Center associated with this upload workspace
-
The non-member group for the Research Center associated with this upload workspace
-
Any additional groups that the workspace is shared with
+
Before the combined workspace is completed:
+
+
GREGOR_DCC_ADMINS (as admin)
+
GREGOR_DCC_WRITERS
+
GREGOR_DCC_MEMBERS
+
The upload group for the Research Center associated with this upload workspace
+
The member group for the Research Center associated with this upload workspace
+
The non-member group for the Research Center associated with this upload workspace
+
+
After the combined workspace is completed:
+
+
GREGOR_DCC_ADMINS (as admin)
+
GREGOR_ALL
+
Note that groups associated with AnVIL (e.g., anvil-admins, anvil-devs) are ignored by the audit.
From a4ccef09d9cc8b24536271274e481d79a4e197c0 Mon Sep 17 00:00:00 2001
From: Adrienne Stilp
Date: Wed, 28 Aug 2024 16:01:28 -0700
Subject: [PATCH 112/125] Fix template object for audit results
Accidentally had an old data_access_audit in the template, so some
records were not getting rendered.
---
gregor_django/templates/gregor_anvil/email_audit_report.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gregor_django/templates/gregor_anvil/email_audit_report.html b/gregor_django/templates/gregor_anvil/email_audit_report.html
index fb1f74c4..fe112c9e 100644
--- a/gregor_django/templates/gregor_anvil/email_audit_report.html
+++ b/gregor_django/templates/gregor_anvil/email_audit_report.html
@@ -34,7 +34,7 @@