Skip to content

Commit

Permalink
Merge pull request #648 from UW-GAC/feature/associate-study-sites-wit…
Browse files Browse the repository at this point in the history
…h-members-groups

Associate study sites with members groups
  • Loading branch information
amstilp authored Jul 10, 2024
2 parents 1059698 + bcf962c commit 54f9201
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 20 deletions.
2 changes: 2 additions & 0 deletions add_cdsa_example_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
20 changes: 20 additions & 0 deletions primed/primed_anvil/migrations/0007_studysite_member_group.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
Original file line number Diff line number Diff line change
@@ -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'),
),
]
10 changes: 10 additions & 0 deletions primed/primed_anvil/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"""

Expand Down
16 changes: 12 additions & 4 deletions primed/primed_anvil/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
20 changes: 20 additions & 0 deletions primed/primed_anvil/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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."""
Expand Down
29 changes: 29 additions & 0 deletions primed/primed_anvil/tests/test_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
60 changes: 58 additions & 2 deletions primed/primed_anvil/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from anvil_consortium_manager import models as acm_models
from anvil_consortium_manager.tests.factories import (
AccountFactory,
GroupAccountMembershipFactory,
ManagedGroupFactory,
WorkspaceGroupSharingFactory,
)
Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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."""
Expand Down
27 changes: 16 additions & 11 deletions primed/primed_anvil/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -33,7 +33,6 @@
OpenAccessWorkspaceStaffTable,
OpenAccessWorkspaceUserTable,
)
from primed.users.tables import UserTable

from . import filters, helpers, models, tables

Expand Down Expand Up @@ -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):
Expand Down
54 changes: 51 additions & 3 deletions primed/templates/primed_anvil/studysite_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
<dl class="row">
<dt class="col-sm-2">Full name</dt> <dd class="col-sm-10">{{ object.full_name }}</dd>
<dt class="col-sm-2">Short name</dt> <dd class="col-sm-10">{{ object.short_name }}</dd>
<dt class="col-sm-2">Member group</dt> <dd class="col-sm-10">
{% if object.member_group %}
<a href="{{ object.member_group.get_absolute_url }}">{{ object.member_group }}</a>
{% else %}
&mdash;
{% endif %}
</dd>
</dl>
{% endblock panel %}

Expand Down Expand Up @@ -44,9 +51,50 @@ <h2 class="accordion-header" id="workspaces-heading-CDSA">
</div>
</div>

<h3>Investigators</h3>

<h3>Study Site Users</h3>
<p class='alert alert-warning'>Study site user list only contains those site users who have created an account on this website.</p>
{% render_table tables.0 %}
<div class="my-3">
<div class="accordion" id="accordionMembers">
<div class="accordion-item">
<h2 class="accordion-header" id="headingMembersOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseMembersOne" aria-expanded="false" aria-controls="collapseMembersOne">
<span class="fa-solid fa-cloud-arrow-down mx-2"></span>
View consortium members with data access
<span class="badge mx-2 bg-secondary pill"> {{ tables.1.rows|length }}</span>
</button>
</h2>
<div id="collapseMembersOne" class="accordion-collapse collapse" aria-labelledby="headingMembersOne" data-bs-parent="#accordionMembers">
<div class="accordion-body">
<p>
This table shows Accounts in the member group for this Study Site.
</p>
{% render_table tables.3 %}
</div>
</div>
</div>
</div>
</div>

<div class="my-3">
<div class="accordion" id="accordionStudySiteUsers">
<div class="accordion-item">
<h2 class="accordion-header" id="headingStudySiteUsersOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseStudySiteUsersOne" aria-expanded="false" aria-controls="collapseStudySiteUsersOne">
<span class="fa-solid fa-user mx-2"></span>
Study Site members
<span class="badge mx-2 bg-secondary pill"> {{ tables.0.rows|length }}</span>
</button>
</h2>
<div id="collapseStudySiteUsersOne" class="accordion-collapse collapse" aria-labelledby="headingStudySiteUsersOne" data-bs-parent="#accordionStudySiteUsers">
<div class="accordion-body">
<p>
This table shows users who are associated with this Study Site.
</p>
{% render_table tables.0 %}
</div>
</div>
</div>
</div>
</div>

{% endblock after_panel %}

0 comments on commit 54f9201

Please sign in to comment.