From 244d8155a17c2e4ef06a6c17d303a5efadac73dc Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 16:03:11 -0800 Subject: [PATCH 01/13] Add a PartnerUploadWorkspaceModel --- .../0018_partneruploadworkspace_and_more.py | 186 ++++++++++++++++++ gregor_django/gregor_anvil/models.py | 29 +++ gregor_django/gregor_anvil/tests/factories.py | 13 ++ .../gregor_anvil/tests/test_models.py | 113 +++++++++++ 4 files changed, 341 insertions(+) create mode 100644 gregor_django/gregor_anvil/migrations/0018_partneruploadworkspace_and_more.py diff --git a/gregor_django/gregor_anvil/migrations/0018_partneruploadworkspace_and_more.py b/gregor_django/gregor_anvil/migrations/0018_partneruploadworkspace_and_more.py new file mode 100644 index 00000000..9db4b3b8 --- /dev/null +++ b/gregor_django/gregor_anvil/migrations/0018_partneruploadworkspace_and_more.py @@ -0,0 +1,186 @@ +# Generated by Django 4.2.8 on 2023-12-15 23:55 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields +import simple_history.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("anvil_consortium_manager", "0015_add_new_permissions"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("gregor_anvil", "0017_remove_releaseworkspace_upload_workspaces"), + ] + + operations = [ + migrations.CreateModel( + name="PartnerUploadWorkspace", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ( + "version", + models.IntegerField( + help_text="The version of this workspace for this PartnerGroup and ConsentGroup.", + validators=[django.core.validators.MinValueValidator(1)], + ), + ), + ( + "date_completed", + models.DateField( + blank=True, + help_text="The date when uploads to this workspace and data validation were completed.", + null=True, + ), + ), + ( + "consent_group", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="gregor_anvil.consentgroup", + ), + ), + ( + "partner_group", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="gregor_anvil.partnergroup", + ), + ), + ( + "workspace", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="anvil_consortium_manager.workspace", + ), + ), + ], + ), + migrations.CreateModel( + name="HistoricalPartnerUploadWorkspace", + fields=[ + ( + "id", + models.BigIntegerField( + auto_created=True, blank=True, db_index=True, verbose_name="ID" + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ( + "version", + models.IntegerField( + help_text="The version of this workspace for this PartnerGroup and ConsentGroup.", + validators=[django.core.validators.MinValueValidator(1)], + ), + ), + ( + "date_completed", + models.DateField( + blank=True, + help_text="The date when uploads to this workspace and data validation were completed.", + null=True, + ), + ), + ("history_id", models.AutoField(primary_key=True, serialize=False)), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ( + "consent_group", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="gregor_anvil.consentgroup", + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "partner_group", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="gregor_anvil.partnergroup", + ), + ), + ( + "workspace", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="anvil_consortium_manager.workspace", + ), + ), + ], + options={ + "verbose_name": "historical partner upload workspace", + "verbose_name_plural": "historical partner upload workspaces", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.AddConstraint( + model_name="partneruploadworkspace", + constraint=models.UniqueConstraint( + fields=("partner_group", "consent_group", "version"), + name="unique_partner_upload_workspace_data", + ), + ), + ] diff --git a/gregor_django/gregor_anvil/models.py b/gregor_django/gregor_anvil/models.py index e502df65..e29dcdca 100644 --- a/gregor_django/gregor_anvil/models.py +++ b/gregor_django/gregor_anvil/models.py @@ -137,6 +137,35 @@ class Meta: ] +class PartnerUploadWorkspace(TimeStampedModel, BaseWorkspaceData): + """A model to track additional data about a partner workspace.""" + + partner_group = models.ForeignKey(PartnerGroup, on_delete=models.PROTECT) + """The PartnerGroup providing data for this Workspace.""" + + consent_group = models.ForeignKey(ConsentGroup, on_delete=models.PROTECT) + """The ConsentGroup associated with this workspace.""" + + version = models.IntegerField( + validators=[MinValueValidator(1)], + help_text="The version of this workspace for this PartnerGroup and ConsentGroup.", + ) + date_completed = models.DateField( + help_text="The date when uploads to this workspace and data validation were completed.", + null=True, + blank=True, + ) + + class Meta: + constraints = [ + # Model uniqueness. + models.UniqueConstraint( + name="unique_partner_upload_workspace_data", + fields=["partner_group", "consent_group", "version"], + ), + ] + + class ExampleWorkspace(TimeStampedModel, BaseWorkspaceData): """A model to track example workspaces.""" diff --git a/gregor_django/gregor_anvil/tests/factories.py b/gregor_django/gregor_anvil/tests/factories.py index c98ace24..01841d5e 100644 --- a/gregor_django/gregor_anvil/tests/factories.py +++ b/gregor_django/gregor_anvil/tests/factories.py @@ -69,6 +69,19 @@ class Meta: django_get_or_create = ["research_center", "consent_group"] +class PartnerUploadWorkspaceFactory(DjangoModelFactory): + """A factory for the UploadWorkspace model.""" + + partner_group = SubFactory(PartnerGroupFactory) + consent_group = SubFactory(ConsentGroupFactory) + version = Faker("random_int", min=1) + workspace = SubFactory(WorkspaceFactory, workspace_type="upload") + + class Meta: + model = models.PartnerUploadWorkspace + django_get_or_create = ["partner_group", "consent_group"] + + class ExampleWorkspaceFactory(DjangoModelFactory): """A factory for the ExampleWorkspace model.""" diff --git a/gregor_django/gregor_anvil/tests/test_models.py b/gregor_django/gregor_anvil/tests/test_models.py index 1453787e..73d86a96 100644 --- a/gregor_django/gregor_anvil/tests/test_models.py +++ b/gregor_django/gregor_anvil/tests/test_models.py @@ -315,6 +315,119 @@ def test_duplicated_workspace(self): instance_2.save() +class PartnerUploadWorkspaceTest(TestCase): + """Tests for the PartnerUploadWorkspace model.""" + + def test_model_saving(self): + """Creation using the model constructor and .save() works.""" + partner_group = factories.PartnerGroupFactory.create() + consent_group = factories.ConsentGroupFactory.create() + workspace = WorkspaceFactory.create() + instance = models.PartnerUploadWorkspace( + partner_group=partner_group, + consent_group=consent_group, + workspace=workspace, + version=1, + ) + instance.save() + self.assertIsInstance(instance, models.PartnerUploadWorkspace) + + def test_str_method(self): + """The custom __str__ method returns the correct string.""" + instance = factories.PartnerUploadWorkspaceFactory.create() + instance.save() + self.assertIsInstance(instance.__str__(), str) + self.assertEqual(instance.__str__(), instance.workspace.__str__()) + + def test_date_completed(self): + instance = factories.PartnerUploadWorkspaceFactory.create( + date_completed=date.today() + ) + instance.save() + self.assertIsNotNone(instance.date_completed) + + def test_unique_constraint(self): + """Cannot save two instances with the same ResearchCenter, ConsentGroup, and version.""" + instance_1 = factories.PartnerUploadWorkspaceFactory.create() + workspace = WorkspaceFactory.create() + instance_2 = factories.PartnerUploadWorkspaceFactory.build( + partner_group=instance_1.partner_group, + consent_group=instance_1.consent_group, + workspace=workspace, + version=instance_1.version, + ) + with self.assertRaises(ValidationError) as e: + instance_2.full_clean() + self.assertEqual(len(e.exception.error_dict), 1) + self.assertIn(NON_FIELD_ERRORS, e.exception.error_dict) + self.assertEqual(len(e.exception.error_dict[NON_FIELD_ERRORS]), 1) + with self.assertRaises(IntegrityError): + instance_2.save() + + def test_different_partner_group(self): + """Can save two instances with different PartnerGroups and the same version/consent_group.""" + instance_1 = factories.PartnerUploadWorkspaceFactory.create() + partner_group = factories.PartnerGroupFactory.create() + workspace = WorkspaceFactory.create() + instance_2 = factories.PartnerUploadWorkspaceFactory.build( + partner_group=partner_group, + consent_group=instance_1.consent_group, + workspace=workspace, + version=instance_1.version, + ) + instance_2.full_clean() + instance_2.save() + self.assertEqual(models.PartnerUploadWorkspace.objects.count(), 2) + + def test_different_consent_group(self): + """Can save two instances with different ConsentGroups and the same version and partner group.""" + instance_1 = factories.PartnerUploadWorkspaceFactory.create() + consent_group = factories.ConsentGroupFactory.create() + workspace = WorkspaceFactory.create() + instance_2 = factories.PartnerUploadWorkspaceFactory.build( + partner_group=instance_1.partner_group, + consent_group=consent_group, + workspace=workspace, + version=instance_1.version, + ) + instance_2.full_clean() + instance_2.save() + self.assertEqual(models.PartnerUploadWorkspace.objects.count(), 2) + + def test_different_upload_cycle(self): + """Can save two instances models with different versions and the same partner group and consent group.""" + instance_1 = factories.PartnerUploadWorkspaceFactory.create() + workspace = WorkspaceFactory.create() + instance_2 = factories.PartnerUploadWorkspaceFactory.build( + partner_group=instance_1.partner_group, + consent_group=instance_1.consent_group, + workspace=workspace, + version=instance_1.version + 1, + ) + instance_2.full_clean() + instance_2.save() + self.assertEqual(models.PartnerUploadWorkspace.objects.count(), 2) + + def test_duplicated_workspace(self): + """One workspace cannot be associated with two PartnerUploadWorkspace models.""" + instance_1 = factories.PartnerUploadWorkspaceFactory.create() + partner_group = factories.PartnerGroupFactory.create() + consent_group = factories.ConsentGroupFactory.create() + instance_2 = factories.PartnerUploadWorkspaceFactory.build( + partner_group=partner_group, + consent_group=consent_group, + workspace=instance_1.workspace, + version=instance_1.version + 1, + ) + with self.assertRaises(ValidationError) as e: + instance_2.full_clean() + self.assertEqual(len(e.exception.error_dict), 1) + self.assertIn("workspace", e.exception.error_dict) + self.assertEqual(len(e.exception.error_dict["workspace"]), 1) + with self.assertRaises(IntegrityError): + instance_2.save() + + class ExampleWorkspaceTest(TestCase): """Tests for the ExampleWorkspace model.""" From 7c799cf30c7576400923da5bf577feaaf21ba038 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 16:08:02 -0800 Subject: [PATCH 02/13] Add a form for PartnerUploadWorkspaces --- gregor_django/gregor_anvil/forms.py | 13 +++ .../gregor_anvil/tests/test_forms.py | 98 +++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/gregor_django/gregor_anvil/forms.py b/gregor_django/gregor_anvil/forms.py index 6bbe6652..7d8d198e 100644 --- a/gregor_django/gregor_anvil/forms.py +++ b/gregor_django/gregor_anvil/forms.py @@ -41,6 +41,19 @@ class Meta: ) +class PartnerUploadWorkspaceForm(forms.ModelForm): + """Form for a PartnerUploadWorkspace object.""" + + class Meta: + model = models.PartnerUploadWorkspace + fields = ( + "partner_group", + "consent_group", + "version", + "workspace", + ) + + class ExampleWorkspaceForm(forms.ModelForm): """Form for a ExampleWorkspace object.""" diff --git a/gregor_django/gregor_anvil/tests/test_forms.py b/gregor_django/gregor_anvil/tests/test_forms.py index eef888c5..97a89b89 100644 --- a/gregor_django/gregor_anvil/tests/test_forms.py +++ b/gregor_django/gregor_anvil/tests/test_forms.py @@ -182,6 +182,104 @@ def test_invalid_duplicate_object(self): self.assertIn("already exists", non_field_errors[0]) +class PartnerUploadWorkspaceFormTest(TestCase): + """Tests for the PartnerUploadWorkspace class.""" + + form_class = forms.PartnerUploadWorkspaceForm + + def setUp(self): + """Create a workspace for use in the form.""" + self.workspace = WorkspaceFactory() + self.partner_group = factories.PartnerGroupFactory() + self.consent_group = factories.ConsentGroupFactory() + + def test_valid(self): + """Form is valid with necessary input.""" + form_data = { + "partner_group": self.partner_group, + "consent_group": self.consent_group, + "version": 1, + "workspace": self.workspace, + } + form = self.form_class(data=form_data) + self.assertTrue(form.is_valid()) + + def test_invalid_missing_partner_group(self): + """Form is invalid when missing partner_group.""" + form_data = { + # "partner_group": self.partner_group, + "consent_group": self.consent_group, + "version": 1, + "workspace": self.workspace, + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("partner_group", form.errors) + self.assertEqual(len(form.errors["partner_group"]), 1) + self.assertIn("required", form.errors["partner_group"][0]) + + def test_invalid_missing_consent_group(self): + """Form is invalid when missing consent_group.""" + form_data = { + "partner_group": self.partner_group, + # "consent_group": self.consent_group, + "version": 1, + "workspace": self.workspace, + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("consent_group", form.errors) + self.assertEqual(len(form.errors["consent_group"]), 1) + self.assertIn("required", form.errors["consent_group"][0]) + + def test_invalid_missing_version(self): + """Form is invalid when missing research_center.""" + form_data = { + "partner_group": self.partner_group, + "consent_group": self.consent_group, + # "version": 1, + "workspace": self.workspace, + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("version", form.errors) + self.assertEqual(len(form.errors["version"]), 1) + self.assertIn("required", form.errors["version"][0]) + + def test_invalid_missing_workspace(self): + """Form is invalid when missing research_center.""" + form_data = { + "partner_group": self.partner_group, + "consent_group": self.consent_group, + "version": 1, + # "workspace": self.workspace, + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("workspace", form.errors) + self.assertEqual(len(form.errors["workspace"]), 1) + self.assertIn("required", form.errors["workspace"][0]) + + def test_invalid_duplicate_object(self): + """Form is invalid with a duplicated object.""" + instance = factories.PartnerUploadWorkspaceFactory.create() + form_data = { + "partner_group": instance.partner_group, + "consent_group": instance.consent_group, + "version": instance.version, + "workspace": self.workspace, + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + non_field_errors = form.non_field_errors() + self.assertEqual(len(non_field_errors), 1) + self.assertIn("already exists", non_field_errors[0]) + + class ExampleWorkspaceFormTest(TestCase): form_class = forms.ExampleWorkspaceForm From ee0c77a99a9ae4f4933255493cbac14b7c61c5ef Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 16:10:21 -0800 Subject: [PATCH 03/13] Add a table for PartnerUploadWorkspaces --- gregor_django/gregor_anvil/tables.py | 17 +++++++++++++++ .../gregor_anvil/tests/test_tables.py | 21 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/gregor_django/gregor_anvil/tables.py b/gregor_django/gregor_anvil/tables.py index 5461e75f..652a8129 100644 --- a/gregor_django/gregor_anvil/tables.py +++ b/gregor_django/gregor_anvil/tables.py @@ -150,6 +150,23 @@ class Meta: ) +class PartnerUploadWorkspaceTable(WorkspaceConsortiumAccessTable, tables.Table): + """A table for Workspaces that includes fields from PartnerUploadWorkspace.""" + + name = tables.columns.Column(linkify=True) + + class Meta: + model = Workspace + fields = ( + "name", + "partnerworkspace__partner_group", + "partnerworkspace__consent_group", + "partnerworkspace__version", + "partnerworkspace__date_completed", + "consortium_access", + ) + + class TemplateWorkspaceTable(WorkspaceConsortiumAccessTable, tables.Table): """A table for Workspaces that includes fields from TemplateWorkspace.""" diff --git a/gregor_django/gregor_anvil/tests/test_tables.py b/gregor_django/gregor_anvil/tests/test_tables.py index 64b38ccb..3e48c5f6 100644 --- a/gregor_django/gregor_anvil/tests/test_tables.py +++ b/gregor_django/gregor_anvil/tests/test_tables.py @@ -246,6 +246,27 @@ def test_row_count_with_two_objects(self): self.assertEqual(len(table.rows), 2) +class PartnerUploadWorkspaceTableTest(TestCase): + model = Workspace + model_factory = factories.PartnerUploadWorkspaceFactory + table_class = tables.PartnerUploadWorkspaceTable + + def test_row_count_with_no_objects(self): + table = self.table_class(self.model.objects.all()) + self.assertEqual(len(table.rows), 0) + + def test_row_count_with_one_object(self): + self.model_factory.create() + table = self.table_class(self.model.objects.all()) + self.assertEqual(len(table.rows), 1) + + def test_row_count_with_two_objects(self): + # These values are coded into the model, so need to create separately. + self.model_factory.create_batch(2) + table = self.table_class(self.model.objects.all()) + self.assertEqual(len(table.rows), 2) + + class TemplateWorkspaceTableTest(TestCase): model = Workspace model_factory = factories.TemplateWorkspaceFactory From 555e9cacfa65fdc28c08f56121f7386dfb176f5f Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 16:14:48 -0800 Subject: [PATCH 04/13] Add a new adapter for the PartnerUploadWorkspace type --- gregor_django/gregor_anvil/adapters.py | 13 +++++ .../partneruploadworkspace_detail.html | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 gregor_django/templates/gregor_anvil/partneruploadworkspace_detail.html diff --git a/gregor_django/gregor_anvil/adapters.py b/gregor_django/gregor_anvil/adapters.py index 30af25e0..7c95b19a 100644 --- a/gregor_django/gregor_anvil/adapters.py +++ b/gregor_django/gregor_anvil/adapters.py @@ -57,6 +57,19 @@ def get_autocomplete_queryset(self, queryset, q, forwarded={}): return queryset +class PartnerUploadWorkspaceAdapter(BaseWorkspaceAdapter): + """Adapter for PartnerUploadWorkspaces.""" + + type = "partner_upload" + name = "Partner upload workspace" + description = "Workspaces that contain data uploaded by a Partner Group " + list_table_class = tables.PartnerUploadWorkspaceTable + workspace_data_model = models.PartnerUploadWorkspace + workspace_data_form_class = forms.PartnerUploadWorkspaceForm + workspace_form_class = WorkspaceForm + workspace_detail_template_name = "gregor_anvil/partneruploadworkspace_detail.html" + + class ExampleWorkspaceAdapter(BaseWorkspaceAdapter): """Adapter for ExampleWorkspaces.""" diff --git a/gregor_django/templates/gregor_anvil/partneruploadworkspace_detail.html b/gregor_django/templates/gregor_anvil/partneruploadworkspace_detail.html new file mode 100644 index 00000000..11727d02 --- /dev/null +++ b/gregor_django/templates/gregor_anvil/partneruploadworkspace_detail.html @@ -0,0 +1,47 @@ +{% extends "anvil_consortium_manager/workspace_detail.html" %} + +{% block workspace_data %} +
+
+
Partner Group
{{ workspace_data_object.partner_group }}
+
Consent group
{{ workspace_data_object.consent_group }}
+
Version
{{ workspace_data_object.version }}"
+
Date completed
{{ workspace_data_object.date_completed }}"
+
+{% endblock workspace_data %} + +{% block after_panel %} +
+
+
+

+ +

+
+
+ {{ workspace_data_object.consent_group.data_use_limitations }} +
+
+
+
+
+ +{{ block.super }} +{% endblock after_panel %} + +{% block action_buttons %} +{% if show_edit_links %} + {% if object.authorization_domains.first %} +

+ Share with auth domain +

+ {% else %} +

no auth domain

+ {% endif %} +{% endif %} + +{{ block.super }} +{% endblock action_buttons %} From fbf75ea6ca19ca80deeeec62c200c2911466a8f6 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 16:18:37 -0800 Subject: [PATCH 05/13] Add date_completed to PartnerUploadWorkspace form --- gregor_django/gregor_anvil/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gregor_django/gregor_anvil/forms.py b/gregor_django/gregor_anvil/forms.py index 7d8d198e..61de8b2a 100644 --- a/gregor_django/gregor_anvil/forms.py +++ b/gregor_django/gregor_anvil/forms.py @@ -50,6 +50,7 @@ class Meta: "partner_group", "consent_group", "version", + "date_completed", "workspace", ) From 20f49c4fca5db17548670f81cd7572752d518923 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 16:19:27 -0800 Subject: [PATCH 06/13] Fix PartnerUploadWorkspace table --- gregor_django/gregor_anvil/tables.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gregor_django/gregor_anvil/tables.py b/gregor_django/gregor_anvil/tables.py index 652a8129..2e7d25fd 100644 --- a/gregor_django/gregor_anvil/tables.py +++ b/gregor_django/gregor_anvil/tables.py @@ -154,15 +154,16 @@ class PartnerUploadWorkspaceTable(WorkspaceConsortiumAccessTable, tables.Table): """A table for Workspaces that includes fields from PartnerUploadWorkspace.""" name = tables.columns.Column(linkify=True) + partneruploadworkspace__partner_group = tables.columns.Column(linkify=True) class Meta: model = Workspace fields = ( "name", - "partnerworkspace__partner_group", - "partnerworkspace__consent_group", - "partnerworkspace__version", - "partnerworkspace__date_completed", + "partneruploadworkspace__partner_group", + "partneruploadworkspace__consent_group", + "partneruploadworkspace__version", + "partneruploadworkspace__date_completed", "consortium_access", ) From fcb3878263fd8391878e5a63b71adc4023a1e3c3 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 16:21:19 -0800 Subject: [PATCH 07/13] Register the PartnerUploadWorkspace in the admin --- gregor_django/gregor_anvil/admin.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/gregor_django/gregor_anvil/admin.py b/gregor_django/gregor_anvil/admin.py index cc46b622..9c86d243 100644 --- a/gregor_django/gregor_anvil/admin.py +++ b/gregor_django/gregor_anvil/admin.py @@ -98,6 +98,30 @@ class UploadWorkspaceAdmin(SimpleHistoryAdmin): ) +@admin.register(models.PartnerUploadWorkspace) +class PartnerUploadWorkspaceAdmin(SimpleHistoryAdmin): + """Admin class for the PartnerUploadWorkspace model.""" + + list_display = ( + "id", + "workspace", + "partner_group", + "consent_group", + "version", + ) + list_filter = ( + "partner_group", + "consent_group", + "version", + ) + sortable_by = ( + "id", + "workspace", + "partner_group", + "version", + ) + + @admin.register(models.ExampleWorkspace) class ExampleWorkspaceAdmin(SimpleHistoryAdmin): """Admin class for the ExampleWorkspace model.""" From 02fefa785db7d13efce0f6739292ce2cdb84c2d7 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 16:21:43 -0800 Subject: [PATCH 08/13] Register the PartnerUploadWorkspace adapter with ACM --- config/settings/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings/base.py b/config/settings/base.py index e32190f0..c66ddc03 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -372,6 +372,7 @@ "gregor_django.gregor_anvil.adapters.ExampleWorkspaceAdapter", "gregor_django.gregor_anvil.adapters.TemplateWorkspaceAdapter", "gregor_django.gregor_anvil.adapters.UploadWorkspaceAdapter", + "gregor_django.gregor_anvil.adapters.PartnerUploadWorkspaceAdapter", "gregor_django.gregor_anvil.adapters.CombinedConsortiumDataWorkspaceAdapter", "gregor_django.gregor_anvil.adapters.ReleaseWorkspaceAdapter", "gregor_django.gregor_anvil.adapters.DCCProcessingWorkspaceAdapter", From 91ecf1e8bf7aa5791207aa1bd8de98f49caa584c Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 16:37:11 -0800 Subject: [PATCH 09/13] Add PartnerUploadWorkspaces to the UploadCycle detail page This table should show the set of PartnerUploadWorkspaces that are contributing to a given upload cycle. The logic is not quite right in the view yet, but it's enough to show adding the table. --- .../gregor_anvil/tests/test_views.py | 23 ++++++++++++++++++- gregor_django/gregor_anvil/views.py | 5 ++++ .../partneruploadworkspace_detail.html | 4 ++-- .../gregor_anvil/uploadcycle_detail.html | 5 ++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/gregor_django/gregor_anvil/tests/test_views.py b/gregor_django/gregor_anvil/tests/test_views.py index 99186128..fd7f62dc 100644 --- a/gregor_django/gregor_anvil/tests/test_views.py +++ b/gregor_django/gregor_anvil/tests/test_views.py @@ -786,7 +786,7 @@ def test_table_classes(self): self.client.force_login(self.user) response = self.client.get(self.get_url(obj.cycle)) self.assertIn("tables", response.context_data) - self.assertEqual(len(response.context_data["tables"]), 5) + self.assertEqual(len(response.context_data["tables"]), 6) self.assertIsInstance( response.context_data["tables"][0], tables.UploadWorkspaceTable ) @@ -803,6 +803,9 @@ def test_table_classes(self): self.assertIsInstance( response.context_data["tables"][4], tables.DCCProcessedDataWorkspaceTable ) + self.assertIsInstance( + response.context_data["tables"][5], tables.PartnerUploadWorkspaceTable + ) def test_upload_workspace_table(self): """Contains a table of UploadWorkspaces from this upload cycle.""" @@ -866,6 +869,24 @@ def test_dcc_processed_data_workspace_table(self): self.assertIn(workspace.workspace, table.data) self.assertNotIn(other_workspace.workspace, table.data) + def test_partner_upload_workspace_table(self): + """Contains a table of PartnerUploadWorkspaces for this upload cycle.""" + obj = self.model_factory.create() + obj.end_date + # Make sure the partner upload workspace has an end date before the end of this upload cycle. + workspace = factories.PartnerUploadWorkspaceFactory.create( + date_completed=obj.end_date - timedelta(days=1) + ) + other_workspace = factories.PartnerUploadWorkspaceFactory.create( + date_completed=obj.end_date + timedelta(days=1) + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.cycle)) + table = response.context_data["tables"][5] + self.assertEqual(len(table.rows), 1) + self.assertIn(workspace.workspace, table.data) + self.assertNotIn(other_workspace.workspace, table.data) + class UploadCycleListTest(TestCase): """Tests for the UploadCycleList view.""" diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py index 62eeaa93..385092cb 100644 --- a/gregor_django/gregor_anvil/views.py +++ b/gregor_django/gregor_anvil/views.py @@ -90,6 +90,7 @@ class UploadCycleDetail( tables.ReleaseWorkspaceTable, tables.DCCProcessingWorkspaceTable, tables.DCCProcessedDataWorkspaceTable, + tables.PartnerUploadWorkspaceTable, ] def get_tables_data(self): @@ -108,12 +109,16 @@ def get_tables_data(self): dcc_processed_data_workspace_qs = Workspace.objects.filter( dccprocesseddataworkspace__upload_cycle=self.object, ) + partner_workspace_qs = Workspace.objects.filter( + partneruploadworkspace__date_completed__lte=self.object.end_date, + ) return [ upload_workspace_qs, combined_workspace_qs, release_workspace_qs, dcc_processing_workspace_qs, dcc_processed_data_workspace_qs, + partner_workspace_qs, ] diff --git a/gregor_django/templates/gregor_anvil/partneruploadworkspace_detail.html b/gregor_django/templates/gregor_anvil/partneruploadworkspace_detail.html index 11727d02..2f17783d 100644 --- a/gregor_django/templates/gregor_anvil/partneruploadworkspace_detail.html +++ b/gregor_django/templates/gregor_anvil/partneruploadworkspace_detail.html @@ -5,8 +5,8 @@
Partner Group
{{ workspace_data_object.partner_group }}
Consent group
{{ workspace_data_object.consent_group }}
-
Version
{{ workspace_data_object.version }}"
-
Date completed
{{ workspace_data_object.date_completed }}"
+
Version
{{ workspace_data_object.version }}
+
Date completed
{{ workspace_data_object.date_completed }}
{% endblock workspace_data %} diff --git a/gregor_django/templates/gregor_anvil/uploadcycle_detail.html b/gregor_django/templates/gregor_anvil/uploadcycle_detail.html index 91a89d2a..9f421e63 100644 --- a/gregor_django/templates/gregor_anvil/uploadcycle_detail.html +++ b/gregor_django/templates/gregor_anvil/uploadcycle_detail.html @@ -45,4 +45,9 @@

DCC processed data workspaces

{% render_table tables.4 %} +

Partner upload workspaces

+
+ {% render_table tables.5 %} +
+ {% endblock after_panel %} From 300d0c684b65f145568d6593d08c53cc77ebed9c Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 17:10:25 -0800 Subject: [PATCH 10/13] Remove django_get_or_create from factories This was included in error, and was not doing what I thought. It was causing the wrong objects to be created/obtained in factories. --- gregor_django/gregor_anvil/tests/factories.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gregor_django/gregor_anvil/tests/factories.py b/gregor_django/gregor_anvil/tests/factories.py index 01841d5e..ebc453ec 100644 --- a/gregor_django/gregor_anvil/tests/factories.py +++ b/gregor_django/gregor_anvil/tests/factories.py @@ -66,7 +66,6 @@ class UploadWorkspaceFactory(DjangoModelFactory): class Meta: model = models.UploadWorkspace - django_get_or_create = ["research_center", "consent_group"] class PartnerUploadWorkspaceFactory(DjangoModelFactory): @@ -79,7 +78,6 @@ class PartnerUploadWorkspaceFactory(DjangoModelFactory): class Meta: model = models.PartnerUploadWorkspace - django_get_or_create = ["partner_group", "consent_group"] class ExampleWorkspaceFactory(DjangoModelFactory): From 44614fae1825482a1733fd371c6cda2105b6cf29 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Fri, 15 Dec 2023 17:12:06 -0800 Subject: [PATCH 11/13] Add model method to get partner workspaces for an upload cycle Because the partner workspaces are not linked directly to an upload cycle, we need to use some logic to get the partner workspaces that should be included in an upload cycle. For a given research center and consent group, only the PartnerUploadWorkspace with the highest version and with a date completed less than the upload cycle end date will be included for that upload cycle. Use this method in the UploadCycleDetail view instead of filtering partner workspaces directly. --- gregor_django/gregor_anvil/models.py | 20 +++ .../gregor_anvil/tests/test_models.py | 117 ++++++++++++++++++ gregor_django/gregor_anvil/views.py | 3 +- 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/gregor_django/gregor_anvil/models.py b/gregor_django/gregor_anvil/models.py index e29dcdca..22eefa79 100644 --- a/gregor_django/gregor_anvil/models.py +++ b/gregor_django/gregor_anvil/models.py @@ -113,6 +113,26 @@ def get_absolute_url(self): """Return the absolute url for this object.""" return reverse("gregor_anvil:upload_cycles:detail", args=[self.cycle]) + def get_partner_upload_workspaces(self): + """Return a queryset of PartnerUploadWorkspace objects that are included in this upload cycle. + + For a given PartnerGroup and ConsentGroup, the workspace with the highest version that also has a date_completed + that is before the end_date of this upload cycle is included. + """ + qs = PartnerUploadWorkspace.objects.filter( + date_completed__lte=self.end_date + ).order_by("-version") + # This is not ideal, but we can't use .distinct on fields. + pks_to_keep = [] + for partner_group in PartnerGroup.objects.all(): + for consent_group in ConsentGroup.objects.all(): + instance = qs.filter( + partner_group=partner_group, consent_group=consent_group + ).first() + if instance: + pks_to_keep.append(instance.pk) + return qs.filter(pk__in=pks_to_keep) + class UploadWorkspace(TimeStampedModel, BaseWorkspaceData): """A model to track additional data about an upload workspace.""" diff --git a/gregor_django/gregor_anvil/tests/test_models.py b/gregor_django/gregor_anvil/tests/test_models.py index 73d86a96..2e56ecf9 100644 --- a/gregor_django/gregor_anvil/tests/test_models.py +++ b/gregor_django/gregor_anvil/tests/test_models.py @@ -208,6 +208,123 @@ def test_start_date_equal_to_end_date(self): self.assertEqual(len(e.exception.error_dict[NON_FIELD_ERRORS]), 1) self.assertIn("after start_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() + factories.PartnerUploadWorkspaceFactory.create(date_completed=None) + included_workspaces = upload_cycle.get_partner_upload_workspaces() + self.assertEqual(included_workspaces.count(), 0) + + def test_get_partner_uplod_workspaces_with_date_completed(self): + """PartnerUploadWorkspace with date_completed before UploadCycle end_date is included.""" + upload_cycle = factories.UploadCycleFactory.create() + workspace = factories.PartnerUploadWorkspaceFactory.create( + date_completed=upload_cycle.end_date - timedelta(days=4) + ) + included_workspaces = upload_cycle.get_partner_upload_workspaces() + self.assertEqual(included_workspaces.count(), 1) + self.assertIn(workspace, included_workspaces) + + def test_get_partner_upload_workspaces_with_date_completed_after_end_date(self): + """PartnerUploadWorkspace with date_completed after UploadCycle end_date is not included.""" + upload_cycle = factories.UploadCycleFactory.create() + factories.PartnerUploadWorkspaceFactory.create( + date_completed=upload_cycle.end_date + timedelta(days=4) + ) + included_workspaces = upload_cycle.get_partner_upload_workspaces() + self.assertEqual(included_workspaces.count(), 0) + + def test_get_partner_upload_workspaces_with_date_completed_equal_to_end_date(self): + """PartnerUploadWorkspace with date_completed equal to UploadCycle end_date is included.""" + upload_cycle = factories.UploadCycleFactory.create() + workspace = factories.PartnerUploadWorkspaceFactory.create( + date_completed=upload_cycle.end_date + ) + included_workspaces = upload_cycle.get_partner_upload_workspaces() + self.assertEqual(included_workspaces.count(), 1) + self.assertIn(workspace, included_workspaces) + + def test_get_partner_upload_workspaces_higher_versions_with_date_completed(self): + """Only the highest version is included when two PartnerUploadWorkspaces have date_completed.""" + upload_cycle = factories.UploadCycleFactory.create() + workspace_1 = factories.PartnerUploadWorkspaceFactory.create( + version=1, date_completed=upload_cycle.end_date - timedelta(days=4) + ) + workspace_2 = factories.PartnerUploadWorkspaceFactory.create( + partner_group=workspace_1.partner_group, + consent_group=workspace_1.consent_group, + version=2, + date_completed=upload_cycle.end_date - timedelta(days=3), + ) + included_workspaces = upload_cycle.get_partner_upload_workspaces() + self.assertEqual(included_workspaces.count(), 1) + self.assertNotIn(workspace_1, included_workspaces) + self.assertIn(workspace_2, included_workspaces) + + def test_get_partner_upload_workspaces_higher_version_no_date_completed(self): + """PartnerUploadWorkspaces with higher versions and no date_completed are not included.""" + upload_cycle = factories.UploadCycleFactory.create() + workspace_1 = factories.PartnerUploadWorkspaceFactory.create( + version=1, date_completed=upload_cycle.end_date - timedelta(days=4) + ) + workspace_2 = factories.PartnerUploadWorkspaceFactory.create( + partner_group=workspace_1.partner_group, + consent_group=workspace_1.consent_group, + version=2, + date_completed=None, + ) + included_workspaces = upload_cycle.get_partner_upload_workspaces() + self.assertEqual(included_workspaces.count(), 1) + self.assertIn(workspace_1, included_workspaces) + self.assertNotIn(workspace_2, included_workspaces) + + def test_get_partner_upload_workspaces_different_partner_groups(self): + """PartnerUploadWorkspaces with different PartnerGroups are both included.""" + upload_cycle = factories.UploadCycleFactory.create() + workspace_1 = factories.PartnerUploadWorkspaceFactory.create( + version=1, date_completed=upload_cycle.end_date - timedelta(days=4) + ) + workspace_2 = factories.PartnerUploadWorkspaceFactory.create( + consent_group=workspace_1.consent_group, + version=2, + date_completed=upload_cycle.end_date - timedelta(days=3), + ) + included_workspaces = upload_cycle.get_partner_upload_workspaces() + self.assertEqual(included_workspaces.count(), 2) + self.assertIn(workspace_1, included_workspaces) + self.assertIn(workspace_2, included_workspaces) + + def test_get_partner_upload_workspaces_different_consent_groups(self): + """PartnerUploadWorkspaces with different ConsentGroups are both included.""" + + def test_get_partner_upload_workspaces_full_test(self): + upload_cycle = factories.UploadCycleFactory.create() + workspace_1 = factories.PartnerUploadWorkspaceFactory.create( + version=1, date_completed=upload_cycle.end_date - timedelta(days=4) + ) + workspace_2 = factories.PartnerUploadWorkspaceFactory.create( + partner_group=workspace_1.partner_group, + consent_group=workspace_1.consent_group, + version=2, + date_completed=upload_cycle.end_date - timedelta(days=3), + ) + workspace_3 = factories.PartnerUploadWorkspaceFactory.create( + version=1, date_completed=upload_cycle.end_date - timedelta(days=2) + ) + workspace_4 = factories.PartnerUploadWorkspaceFactory.create( + partner_group=workspace_3.partner_group, + consent_group=workspace_3.consent_group, + 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) + self.assertIn(workspace_2, included_workspaces) + self.assertIn(workspace_3, included_workspaces) + self.assertNotIn(workspace_4, included_workspaces) + class UploadWorkspaceTest(TestCase): """Tests for the UploadWorkspace model.""" diff --git a/gregor_django/gregor_anvil/views.py b/gregor_django/gregor_anvil/views.py index 385092cb..0b8e7b8b 100644 --- a/gregor_django/gregor_anvil/views.py +++ b/gregor_django/gregor_anvil/views.py @@ -109,8 +109,9 @@ def get_tables_data(self): dcc_processed_data_workspace_qs = Workspace.objects.filter( dccprocesseddataworkspace__upload_cycle=self.object, ) + partner_workspaces = self.object.get_partner_upload_workspaces() partner_workspace_qs = Workspace.objects.filter( - partneruploadworkspace__date_completed__lte=self.object.end_date, + partneruploadworkspace__in=partner_workspaces ) return [ upload_workspace_qs, From 5951b9bf62f08996a2e65d6e8791fb41a262e4cc Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Mon, 18 Dec 2023 10:11:07 -0800 Subject: [PATCH 12/13] Fix workspace_type in PartnerUploadWorkspaceFactory --- gregor_django/gregor_anvil/tests/factories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gregor_django/gregor_anvil/tests/factories.py b/gregor_django/gregor_anvil/tests/factories.py index ebc453ec..df647b5f 100644 --- a/gregor_django/gregor_anvil/tests/factories.py +++ b/gregor_django/gregor_anvil/tests/factories.py @@ -74,7 +74,7 @@ class PartnerUploadWorkspaceFactory(DjangoModelFactory): partner_group = SubFactory(PartnerGroupFactory) consent_group = SubFactory(ConsentGroupFactory) version = Faker("random_int", min=1) - workspace = SubFactory(WorkspaceFactory, workspace_type="upload") + workspace = SubFactory(WorkspaceFactory, workspace_type="partner_upload") class Meta: model = models.PartnerUploadWorkspace From bf78a59b422f72f93b8ec2171544d22a55ac7cad Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Mon, 18 Dec 2023 10:34:21 -0800 Subject: [PATCH 13/13] Aesthetic updates to the date_completed form field Modify the PartnerUploadWorkspaceForm date_completed field to have clearer help text for the user and to use a calendar picker widget for selecting the date. --- gregor_django/gregor_anvil/forms.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gregor_django/gregor_anvil/forms.py b/gregor_django/gregor_anvil/forms.py index 61de8b2a..5bb88dea 100644 --- a/gregor_django/gregor_anvil/forms.py +++ b/gregor_django/gregor_anvil/forms.py @@ -53,6 +53,12 @@ class Meta: "date_completed", "workspace", ) + help_texts = { + "date_completed": "Do not select a date until validation has been completed in this workspace.", + } + widgets = { + "date_completed": CustomDateInput(), + } class ExampleWorkspaceForm(forms.ModelForm):