diff --git a/add_cdsa_example_data.py b/add_cdsa_example_data.py index 105233ba..cbb34f33 100644 --- a/add_cdsa_example_data.py +++ b/add_cdsa_example_data.py @@ -70,6 +70,7 @@ signed_agreement__version=v10, study_site=cc, ) +cdsa_1001.signed_agreement.representative.study_sites.add(cc) GroupGroupMembershipFactory.create(parent_group=cdsa_group, child_group=cdsa_1001.signed_agreement.anvil_access_group) cdsa_1002 = factories.MemberAgreementFactory.create( @@ -81,6 +82,7 @@ signed_agreement__version=v10, study_site=cardinal, ) +cdsa_1002.signed_agreement.representative.study_sites.add(cardinal) GroupGroupMembershipFactory.create(parent_group=cdsa_group, child_group=cdsa_1002.signed_agreement.anvil_access_group) cdsa_1003 = factories.MemberAgreementFactory.create( diff --git a/primed/primed_anvil/migrations/0007_studysite_member_group.py b/primed/primed_anvil/migrations/0007_studysite_member_group.py new file mode 100644 index 00000000..ff038f61 --- /dev/null +++ b/primed/primed_anvil/migrations/0007_studysite_member_group.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.13 on 2024-07-10 20:21 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('anvil_consortium_manager', '0019_accountuserarchive'), + ('primed_anvil', '0006_studysite_drupal_node_id'), + ] + + operations = [ + migrations.AddField( + model_name='studysite', + name='member_group', + field=models.ForeignKey(blank=True, help_text='The AnVIL Managed Group associated with this site.', null=True, on_delete=django.db.models.deletion.PROTECT, to='anvil_consortium_manager.managedgroup'), + ), + ] diff --git a/primed/primed_anvil/migrations/0008_alter_studysite_member_group.py b/primed/primed_anvil/migrations/0008_alter_studysite_member_group.py new file mode 100644 index 00000000..e8f20eaa --- /dev/null +++ b/primed/primed_anvil/migrations/0008_alter_studysite_member_group.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.13 on 2024-07-10 20:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('anvil_consortium_manager', '0019_accountuserarchive'), + ('primed_anvil', '0007_studysite_member_group'), + ] + + operations = [ + migrations.AlterField( + model_name='studysite', + name='member_group', + field=models.OneToOneField(blank=True, help_text='The AnVIL Managed Group associated with this site.', null=True, on_delete=django.db.models.deletion.PROTECT, to='anvil_consortium_manager.managedgroup'), + ), + ] diff --git a/primed/primed_anvil/models.py b/primed/primed_anvil/models.py index 87389d1b..ad8727e3 100644 --- a/primed/primed_anvil/models.py +++ b/primed/primed_anvil/models.py @@ -1,3 +1,4 @@ +from anvil_consortium_manager.models import ManagedGroup from django.conf import settings from django.db import models from django.urls import reverse @@ -37,6 +38,15 @@ class StudySite(TimeStampedModel, models.Model): full_name = models.CharField(max_length=255) """The full name of the Study Sites.""" + member_group = models.OneToOneField( + ManagedGroup, + on_delete=models.PROTECT, + help_text="The AnVIL Managed Group associated with this site.", + null=True, + blank=True, + unique=True, + ) + drupal_node_id = models.IntegerField(blank=True, null=True) """Reference node ID for entity in drupal""" diff --git a/primed/primed_anvil/tables.py b/primed/primed_anvil/tables.py index 29a79d8d..94b583e3 100644 --- a/primed/primed_anvil/tables.py +++ b/primed/primed_anvil/tables.py @@ -156,8 +156,11 @@ def __init__(self, *args, **kwargs): super().__init__(*args, extra_columns=extra_columns, **kwargs) -class UserAccountSingleGroupMembershipTable(tables.Table): - """A table with users and info about whether they are members of a group.""" +class UserAccountTable(tables.Table): + """A table for `User`s with `Account` information.""" + + name = tables.Column(linkify=True) + account = tables.Column(linkify=True, verbose_name="AnVIL account") class Meta: model = User @@ -167,8 +170,13 @@ class Meta: ) order_by = ("name",) - name = tables.Column(linkify=lambda record: record.get_absolute_url()) - account = tables.Column(verbose_name="AnVIL account") + +class UserAccountSingleGroupMembershipTable(UserAccountTable): + """A table with users and info about whether they are members of a group.""" + + class Meta(UserAccountTable.Meta): + pass + is_group_member = tables.BooleanColumn(verbose_name="Has access?", default=False) def __init__(self, *args, managed_group=None, **kwargs): diff --git a/primed/primed_anvil/tests/test_models.py b/primed/primed_anvil/tests/test_models.py index b4f249c6..0672b0c0 100644 --- a/primed/primed_anvil/tests/test_models.py +++ b/primed/primed_anvil/tests/test_models.py @@ -1,5 +1,6 @@ """Tests of models in the `primed_anvil` app.""" +from anvil_consortium_manager.tests.factories import ManagedGroupFactory from django.core.exceptions import ValidationError from django.db.utils import IntegrityError from django.test import TestCase @@ -48,6 +49,7 @@ class StudySiteTest(TestCase): def test_model_saving(self): """Creation using the model constructor and .save() works.""" instance = models.StudySite(full_name="Test name", short_name="TEST") + instance.full_clean() instance.save() self.assertIsInstance(instance, models.StudySite) @@ -72,6 +74,24 @@ def test_unique_short_name(self): with self.assertRaises(IntegrityError): instance2.save() + def test_can_set_members_group(self): + member_group = ManagedGroupFactory.create() + instance = models.StudySite(full_name="Test name", short_name="TEST", member_group=member_group) + instance.full_clean() + instance.save() + self.assertIsInstance(instance, models.StudySite) + + def test_same_member_group_different_sites(self): + member_group = ManagedGroupFactory.create() + factories.StudySiteFactory.create(member_group=member_group) + instance = factories.StudySiteFactory.build(member_group=member_group) + with self.assertRaises(ValidationError) as e: + instance.full_clean() + self.assertEqual(len(e.exception.message_dict), 1) + self.assertIn("member_group", e.exception.message_dict) + self.assertEqual(len(e.exception.message_dict["member_group"]), 1) + self.assertIn("Study site with this Member group already exists.", e.exception.message_dict["member_group"][0]) + class AvailableDataTest(TestCase): """Tests for the AvailableData model.""" diff --git a/primed/primed_anvil/tests/test_tables.py b/primed/primed_anvil/tests/test_tables.py index a539ada3..3a892c15 100644 --- a/primed/primed_anvil/tests/test_tables.py +++ b/primed/primed_anvil/tests/test_tables.py @@ -255,6 +255,35 @@ def test_render_not_workspace(self): column.render(None, "foo", None) +class UserAccountTableTest(TestCase): + """Tests for the UserAccountTable class.""" + + def setUp(self): + self.managed_group = ManagedGroupFactory.create() + + def test_row_count_with_no_objects(self): + table = tables.UserAccountTable(User.objects.all()) + self.assertEqual(len(table.rows), 0) + + def test_row_count_with_one_object(self): + UserFactory.create() + table = tables.UserAccountTable(User.objects.all()) + self.assertEqual(len(table.rows), 1) + + def test_row_count_with_two_objects(self): + UserFactory.create_batch(2) + table = tables.UserAccountTable(User.objects.all()) + self.assertEqual(len(table.rows), 2) + + def test_ordering(self): + """Users are ordered alphabetically by name""" + user_foo = UserFactory.create(name="Foo") + user_bar = UserFactory.create(name="Bar") + table = tables.UserAccountTable(User.objects.all()) + self.assertEqual(table.data[0], user_bar) + self.assertEqual(table.data[1], user_foo) + + class UserAccountSingleGroupMembershipTableTest(TestCase): """Tests for the UserAccountSingleGroupMembershipTable class.""" diff --git a/primed/primed_anvil/tests/test_views.py b/primed/primed_anvil/tests/test_views.py index 22f929b9..f47e8717 100644 --- a/primed/primed_anvil/tests/test_views.py +++ b/primed/primed_anvil/tests/test_views.py @@ -3,6 +3,7 @@ from anvil_consortium_manager import models as acm_models from anvil_consortium_manager.tests.factories import ( AccountFactory, + GroupAccountMembershipFactory, ManagedGroupFactory, WorkspaceGroupSharingFactory, ) @@ -17,13 +18,13 @@ from django.test import RequestFactory, TestCase from django.urls import reverse -from primed.cdsa.tables import CDSAWorkspaceStaffTable, CDSAWorkspaceUserTable +from primed.cdsa.tables import CDSAWorkspaceStaffTable, CDSAWorkspaceUserTable, MemberAgreementTable from primed.cdsa.tests.factories import ( CDSAWorkspaceFactory, DataAffiliateAgreementFactory, MemberAgreementFactory, ) -from primed.dbgap.tables import dbGaPWorkspaceStaffTable, dbGaPWorkspaceUserTable +from primed.dbgap.tables import dbGaPApplicationTable, dbGaPWorkspaceStaffTable, dbGaPWorkspaceUserTable from primed.dbgap.tests.factories import ( dbGaPApplicationFactory, dbGaPStudyAccessionFactory, @@ -843,6 +844,22 @@ def test_view_status_code_with_invalid_pk(self): with self.assertRaises(Http404): self.get_view()(request, pk=obj.pk + 1) + def test_table_classes(self): + """Table classes are correct.""" + obj = self.model_factory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.pk)) + self.assertIn("tables", response.context_data) + self.assertEqual(len(response.context_data["tables"]), 4) + self.assertIsInstance(response.context_data["tables"][0], tables.UserAccountTable) + self.assertEqual(len(response.context_data["tables"][0].data), 0) + self.assertIsInstance(response.context_data["tables"][1], dbGaPApplicationTable) + self.assertEqual(len(response.context_data["tables"][1].data), 0) + self.assertIsInstance(response.context_data["tables"][2], MemberAgreementTable) + self.assertEqual(len(response.context_data["tables"][2].data), 0) + self.assertIsInstance(response.context_data["tables"][3], tables.AccountTable) + self.assertEqual(len(response.context_data["tables"][3].data), 0) + def test_site_user_table(self): """Contains a table of site users with the correct users.""" obj = self.model_factory.create() @@ -883,6 +900,45 @@ def test_cdsa_table(self): self.assertIn(site_cdsa, table.data) self.assertNotIn(other_cdsa, table.data) + def test_site_user_table_when_member_group_is_set(self): + """The site user table is the correct class when the member group is set.""" + member_group = ManagedGroupFactory.create() + obj = self.model_factory.create(member_group=member_group) + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.pk)) + table = response.context_data["tables"][0] + self.assertIsInstance(table, tables.UserAccountSingleGroupMembershipTable) + self.assertEqual(table.managed_group, member_group) + + def test_member_group_table(self): + member_group = ManagedGroupFactory.create() + obj = self.model_factory.create(member_group=member_group) + account = AccountFactory.create(verified=True) + GroupAccountMembershipFactory.create(account=account, group=member_group) + other_account = AccountFactory.create(verified=True) + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.pk)) + table = response.context_data["tables"][3] + self.assertEqual(len(table.rows), 1) + self.assertIn(account, table.data) + self.assertNotIn(other_account, table.data) + + def test_member_table_group_not_set(self): + obj = self.model_factory.create() + AccountFactory.create(verified=True) + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.pk)) + table = response.context_data["tables"][3] + self.assertEqual(len(table.rows), 0) + + def test_link_to_member_group(self): + """Response includes a link to the member group if it exists.""" + member_group = ManagedGroupFactory.create() + obj = self.model_factory.create(member_group=member_group) + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.pk)) + self.assertContains(response, member_group.get_absolute_url()) + class StudySiteListTest(TestCase): """Tests for the StudySiteList view.""" diff --git a/primed/primed_anvil/views.py b/primed/primed_anvil/views.py index 0811ec9c..7717e667 100644 --- a/primed/primed_anvil/views.py +++ b/primed/primed_anvil/views.py @@ -5,7 +5,7 @@ AnVILConsortiumManagerStaffViewRequired, AnVILConsortiumManagerViewRequired, ) -from anvil_consortium_manager.models import AnVILProjectManagerAccess, Workspace +from anvil_consortium_manager.models import Account, AnVILProjectManagerAccess, Workspace from dal import autocomplete from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin @@ -33,7 +33,6 @@ OpenAccessWorkspaceStaffTable, OpenAccessWorkspaceUserTable, ) -from primed.users.tables import UserTable from . import filters, helpers, models, tables @@ -124,19 +123,25 @@ class StudySiteDetail(AnVILConsortiumManagerStaffViewRequired, MultiTableMixin, """View to show details about a `StudySite`.""" model = models.StudySite - tables = [ - UserTable, - dbGaPApplicationTable, - MemberAgreementTable, - ] - # def get_table(self): - # return UserTable(User.objects.filter(study_sites=self.object)) - def get_tables_data(self): + def get_tables(self): user_qs = User.objects.filter(study_sites=self.object) + if self.object.member_group: + user_table = tables.UserAccountSingleGroupMembershipTable(user_qs, managed_group=self.object.member_group) + else: + user_table = tables.UserAccountTable(user_qs, exclude="study_sites") dbgap_qs = dbGaPApplication.objects.filter(principal_investigator__study_sites=self.object) cdsa_qs = MemberAgreement.objects.filter(study_site=self.object) - return [user_qs, dbgap_qs, cdsa_qs] + if self.object.member_group: + account_qs = Account.objects.filter(groupaccountmembership__group=self.object.member_group) + else: + account_qs = Account.objects.none() + return [ + user_table, + dbGaPApplicationTable(dbgap_qs), + MemberAgreementTable(cdsa_qs), + tables.AccountTable(account_qs, exclude=("number_groups")), + ] class StudySiteList(AnVILConsortiumManagerStaffViewRequired, SingleTableView): diff --git a/primed/templates/primed_anvil/studysite_detail.html b/primed/templates/primed_anvil/studysite_detail.html index 04bd8cde..5f702061 100644 --- a/primed/templates/primed_anvil/studysite_detail.html +++ b/primed/templates/primed_anvil/studysite_detail.html @@ -6,6 +6,13 @@
Study site user list only contains those site users who have created an account on this website.
- {% render_table tables.0 %} ++ This table shows Accounts in the member group for this Study Site. +
+ {% render_table tables.3 %} ++ This table shows users who are associated with this Study Site. +
+ {% render_table tables.0 %} +