diff --git a/.env.dist b/.env.dist
index 518ad98a..b9e98292 100644
--- a/.env.dist
+++ b/.env.dist
@@ -23,3 +23,8 @@ DJANGO_EMAIL_PORT=
DJANGO_EMAIL_HOST_USER=
DJANGO_EMAIL_HOST_PASSWORD=
DJANGO_EMAIL_USE_TLS=
+
+# drupal api
+DRUPAL_API_CLIENT_ID=
+DRUPAL_API_CLIENT_SECRET=
+DRUPAL_API_REL_PATH=
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f8a8fd23..4570f6cf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -50,7 +50,7 @@ jobs:
# ahead of planned upgrades we can add versions as
# needed
python-version: [3.8]
- mariadb-version: ["10.4"]
+ mariadb-version: ["10.4", "10.5"]
services:
mysql:
diff --git a/add_cdsa_example_data.py b/add_cdsa_example_data.py
index 44ef9d69..da5c0058 100644
--- a/add_cdsa_example_data.py
+++ b/add_cdsa_example_data.py
@@ -8,14 +8,23 @@
ManagedGroupFactory,
)
from django.conf import settings
+from django.core.management import call_command
from primed.cdsa.tests import factories
-from primed.duo.tests.factories import DataUseModifierFactory, DataUsePermissionFactory
+from primed.duo.models import DataUseModifier, DataUsePermission
from primed.primed_anvil.models import Study, StudySite
from primed.primed_anvil.tests.factories import StudyFactory, StudySiteFactory
from primed.users.models import User
from primed.users.tests.factories import UserFactory
+# Load duos
+call_command("load_duo")
+
+# create the CDSA auth group
+cdsa_group = ManagedGroupFactory.create(name=settings.ANVIL_CDSA_GROUP_NAME)
+# Add PRIMED ADMINS group
+cc_admins_group = ManagedGroupFactory.create(name=settings.ANVIL_CC_ADMINS_GROUP_NAME)
+
# Create major versions
major_version = factories.AgreementMajorVersionFactory.create(version=1)
@@ -28,11 +37,8 @@
)
# Create a couple signed CDSAs.
-dup = DataUsePermissionFactory.create(abbreviation="GRU")
-dum = DataUseModifierFactory.create(abbreviation="NPU")
-
-# create the CDSA auth group
-cdsa_group = ManagedGroupFactory.create(name=settings.ANVIL_CDSA_GROUP_NAME)
+dup = DataUsePermission.objects.get(abbreviation="GRU")
+dum = DataUseModifier.objects.get(abbreviation="NPU")
# Create some study sites.
StudySiteFactory.create(short_name="CC", full_name="Coordinating Center")
@@ -47,7 +53,7 @@
signed_agreement__representative=UserFactory.create(name="Ken Rice"),
signed_agreement__signing_institution="UW",
signed_agreement__representative_role="Contact PI",
- signed_agreement__is_primary=True,
+ is_primary=True,
signed_agreement__version=v10,
study_site=StudySite.objects.get(short_name="CC"),
)
@@ -60,7 +66,7 @@
signed_agreement__representative=UserFactory.create(name="Sally Adebamowo"),
signed_agreement__signing_institution="UM",
signed_agreement__representative_role="Contact PI",
- signed_agreement__is_primary=True,
+ is_primary=True,
signed_agreement__version=v10,
study_site=StudySite.objects.get(short_name="CARDINAL"),
)
@@ -73,7 +79,7 @@
signed_agreement__representative=UserFactory.create(name="Bamidele Tayo"),
signed_agreement__signing_institution="Loyola",
signed_agreement__representative_role="Co-PI",
- signed_agreement__is_primary=False,
+ is_primary=False,
signed_agreement__version=v10,
study_site=StudySite.objects.get(short_name="CARDINAL"),
)
@@ -86,7 +92,7 @@
signed_agreement__representative=UserFactory.create(name="Brackie Mitchell"),
signed_agreement__signing_institution="UM",
signed_agreement__representative_role="Co-I",
- signed_agreement__is_primary=False,
+ is_primary=False,
signed_agreement__version=v11,
study_site=StudySite.objects.get(short_name="CARDINAL"),
)
@@ -113,6 +119,8 @@
signed_agreement__signing_institution="UW",
study=Study.objects.get(short_name="MESA"),
signed_agreement__version=v10,
+ additional_limitations="This data can only be used for testing the app.",
+ requires_study_review=True,
)
GroupGroupMembershipFactory.create(
parent_group=cdsa_group, child_group=cdsa_1006.signed_agreement.anvil_access_group
@@ -123,7 +131,7 @@
signed_agreement__representative=UserFactory.create(name="Wendy"),
signed_agreement__signing_institution="JHU",
signed_agreement__representative_role="Field Center PI",
- signed_agreement__is_primary=False,
+ is_primary=False,
study=Study.objects.get(short_name="MESA"),
signed_agreement__version=v10,
)
@@ -136,7 +144,7 @@
signed_agreement__representative=UserFactory.create(name="Jerry"),
signed_agreement__signing_institution="Lundquist",
signed_agreement__representative_role="Analysis Center PI",
- signed_agreement__is_primary=False,
+ is_primary=False,
study=Study.objects.get(short_name="MESA"),
signed_agreement__version=v10,
)
@@ -219,5 +227,17 @@
workspace__name="DEMO_PRIMED_CDSA_MESA_2",
study=Study.objects.get(short_name="MESA"),
data_use_permission=dup,
+ additional_limitations="Additional limitations for workspace.",
)
cdsa_workspace_2.data_use_modifiers.add(dum)
+
+
+# Add a workspace with no primary cdsa.
+cdsa_workspace_3 = factories.CDSAWorkspaceFactory.create(
+ workspace__billing_project__name="demo-primed-cdsa",
+ workspace__name="DEMO_PRIMED_CDSA_ARIC_1",
+ study=Study.objects.create(
+ short_name="ARIC", full_name="Atherosclerosis Risk in Communities"
+ ),
+ data_use_permission=dup,
+)
diff --git a/add_data_prep_example_data.py b/add_data_prep_example_data.py
new file mode 100644
index 00000000..76136f9b
--- /dev/null
+++ b/add_data_prep_example_data.py
@@ -0,0 +1,44 @@
+# Temporary script to create some test data.
+# Run with: python manage.py shell < add_data_prep_example_data.py
+
+
+from primed.cdsa.tests.factories import CDSAWorkspaceFactory
+from primed.dbgap.tests.factories import dbGaPWorkspaceFactory
+from primed.miscellaneous_workspaces.tests import factories
+from primed.primed_anvil.tests.factories import StudyFactory
+
+# Create a dbGaP workspace.
+fhs = StudyFactory.create(short_name="FHS", full_name="Framingham Heart Study")
+workspace_dbgap = dbGaPWorkspaceFactory.create(
+ dbgap_study_accession__dbgap_phs=7,
+ dbgap_study_accession__studies=[fhs],
+ dbgap_version=33,
+ dbgap_participant_set=12,
+ dbgap_consent_code=1,
+ dbgap_consent_abbreviation="HMB",
+ workspace__name="DBGAP_FHS_v33_p12_HMB",
+)
+
+# Create a data prep workspace.
+workspace_dbgap_prep = factories.DataPrepWorkspaceFactory.create(
+ target_workspace=workspace_dbgap.workspace,
+ workspace__name="DBGAP_FHS_v33_p12_HMB_PREP",
+)
+
+
+# Create a CDSA workspace.
+workspace_cdsa = CDSAWorkspaceFactory.create(
+ study__short_name="MESA",
+ workspace__name="CDSA_MESA_HMB",
+)
+
+# Create a data prep workspace.
+factories.DataPrepWorkspaceFactory.create(
+ target_workspace=workspace_cdsa.workspace,
+ workspace__name="CDSA_MESA_HMB_PREP_1",
+ is_active=False,
+)
+workspace_cdsa_prep = factories.DataPrepWorkspaceFactory.create(
+ target_workspace=workspace_cdsa.workspace,
+ workspace__name="CDSA_MESA_HMB_PREP_2",
+)
diff --git a/config/settings/base.py b/config/settings/base.py
index 9710eaeb..01092f24 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -85,6 +85,7 @@
"simple_history",
"dbbackup",
"django_htmx",
+ "constance",
]
LOCAL_APPS = [
@@ -205,6 +206,7 @@
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
+ "constance.context_processors.config",
"primed.utils.context_processors.settings_context",
],
},
@@ -368,6 +370,17 @@
# https://django-tables2.readthedocs.io/en/latest/pages/custom-rendering.html?highlight=django_tables2_template#available-templates
DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5.html"
+# django-constance
+# ------------------------------------------------------------------------------
+CONSTANCE_CONFIG = {
+ "ANNOUNCEMENT_TEXT": ("", "Site-wide announcement message", str),
+}
+
+CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend"
+CONSTANCE_IGNORE_ADMIN_VERSION_CHECK = True
+# CONSTANCE_DATABASE_CACHE_BACKEND = "default"
+CONSTANCE_DATABASE_CACHE_AUTOFILL_TIMEOUT = None
+
# django-anvil-consortium-manager
# ------------------------------------------------------------------------------
ANVIL_API_SERVICE_ACCOUNT_FILE = env("ANVIL_API_SERVICE_ACCOUNT_FILE")
@@ -390,3 +403,13 @@
# Specify the subject for AnVIL account verification emails.
ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = "Verify your AnVIL account email"
ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL = "primedconsortium@uw.edu"
+
+DRUPAL_API_CLIENT_ID = env("DRUPAL_API_CLIENT_ID", default="")
+DRUPAL_API_CLIENT_SECRET = env("DRUPAL_API_CLIENT_SECRET", default="")
+DRUPAL_API_REL_PATH = env("DRUPAL_API_REL_PATH", default="mockapi")
+DRUPAL_DATA_AUDIT_DEACTIVATE_USERS = env(
+ "DRUPAL_DATA_AUDIT_DEACTIVATE_USERS", default=False
+)
+DRUPAL_DATA_AUDIT_REMOVE_USER_SITES = env(
+ "DRUPAL_DATA_AUDIT_REMOVE_USER_SITES", default=False
+)
diff --git a/primed/cdsa/adapters.py b/primed/cdsa/adapters.py
index e4409ddd..a4767906 100644
--- a/primed/cdsa/adapters.py
+++ b/primed/cdsa/adapters.py
@@ -1,5 +1,8 @@
from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter
from anvil_consortium_manager.forms import WorkspaceForm
+from anvil_consortium_manager.models import Workspace
+
+from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable
from . import forms, models, tables
@@ -18,3 +21,22 @@ class CDSAWorkspaceAdapter(BaseWorkspaceAdapter):
workspace_data_model = models.CDSAWorkspace
workspace_data_form_class = forms.CDSAWorkspaceForm
workspace_detail_template_name = "cdsa/cdsaworkspace_detail.html"
+
+ def get_extra_detail_context_data(self, workspace, request):
+ extra_context = {}
+ associated_data_prep = Workspace.objects.filter(
+ dataprepworkspace__target_workspace=workspace
+ )
+ extra_context["associated_data_prep_workspaces"] = DataPrepWorkspaceUserTable(
+ associated_data_prep
+ )
+ extra_context["data_prep_active"] = associated_data_prep.filter(
+ dataprepworkspace__is_active=True
+ ).exists()
+ # Get the primary CDSA for this study, assuming it exists.
+ try:
+ extra_context["primary_cdsa"] = workspace.cdsaworkspace.get_primary_cdsa()
+ except models.DataAffiliateAgreement.DoesNotExist:
+ extra_context["primary_cdsa"] = None
+
+ return extra_context
diff --git a/primed/cdsa/admin.py b/primed/cdsa/admin.py
index cdc71f4d..79abf632 100644
--- a/primed/cdsa/admin.py
+++ b/primed/cdsa/admin.py
@@ -46,13 +46,13 @@ class SignedAgreement(SimpleHistoryAdmin):
"cc_id",
"representative",
"type",
- "is_primary",
+ # "is_primary",
"date_signed",
"version",
)
list_filter = (
"type",
- "is_primary",
+ # "is_primary",
"version",
"status",
)
@@ -79,7 +79,7 @@ class MemberAgreementAdmin(SimpleHistoryAdmin):
)
list_filter = (
"study_site",
- "signed_agreement__is_primary",
+ # "signed_agreement__is_primary",
"signed_agreement__status",
)
@@ -94,7 +94,7 @@ class DataAffiliateAgreementAdmin(SimpleHistoryAdmin):
)
list_filter = (
"study",
- "signed_agreement__is_primary",
+ # "signed_agreement__is_primary",
"signed_agreement__status",
)
@@ -108,7 +108,7 @@ class NonDataAffiliateAgreementAdmin(SimpleHistoryAdmin):
"affiliation",
)
list_filter = (
- "signed_agreement__is_primary",
+ # "signed_agreement__is_primary",
"signed_agreement__status",
)
diff --git a/primed/cdsa/audit/signed_agreement_audit.py b/primed/cdsa/audit/signed_agreement_audit.py
index 929783c4..669df722 100644
--- a/primed/cdsa/audit/signed_agreement_audit.py
+++ b/primed/cdsa/audit/signed_agreement_audit.py
@@ -119,9 +119,6 @@ class SignedAgreementAccessAudit(PRIMEDAudit):
PRIMARY_NOT_ACTIVE = "Primary agreement for this CDSA is not active."
# Other errors
- ERROR_NON_DATA_AFFILIATE_COMPONENT = (
- "Non-data affiliate agreements must be primary."
- )
ERROR_OTHER_CASE = "Signed Agreement did not match any expected situations."
results_table_class = SignedAgreementAccessAuditTable
@@ -218,23 +215,15 @@ def _audit_component_agreement(self, signed_agreement):
if hasattr(signed_agreement, "memberagreement"):
# Member
primary_qs = models.SignedAgreement.objects.filter(
- is_primary=True,
+ memberagreement__is_primary=True,
memberagreement__study_site=signed_agreement.memberagreement.study_site,
)
elif hasattr(signed_agreement, "dataaffiliateagreement"):
# Data affiliate
primary_qs = models.SignedAgreement.objects.filter(
- is_primary=True,
+ dataaffiliateagreement__is_primary=True,
dataaffiliateagreement__study=signed_agreement.dataaffiliateagreement.study,
)
- elif hasattr(signed_agreement, "nondataaffiliateagreement"):
- self.errors.append(
- OtherError(
- signed_agreement=signed_agreement,
- note=self.ERROR_NON_DATA_AFFILIATE_COMPONENT,
- )
- )
- return
primary_exists = primary_qs.exists()
primary_active = primary_qs.filter(
status=models.SignedAgreement.StatusChoices.ACTIVE,
@@ -320,7 +309,8 @@ def _audit_component_agreement(self, signed_agreement):
) # pragma: no cover
def _audit_signed_agreement(self, signed_agreement):
- if signed_agreement.is_primary:
+ agreement_type = signed_agreement.get_agreement_type()
+ if not hasattr(agreement_type, "is_primary") or agreement_type.is_primary:
self._audit_primary_agreement(signed_agreement)
else:
self._audit_component_agreement(signed_agreement)
diff --git a/primed/cdsa/audit/workspace_audit.py b/primed/cdsa/audit/workspace_audit.py
index 2e329859..e81d8287 100644
--- a/primed/cdsa/audit/workspace_audit.py
+++ b/primed/cdsa/audit/workspace_audit.py
@@ -153,7 +153,7 @@ def _audit_workspace(self, workspace):
child_group=self.anvil_cdsa_group,
).exists()
primary_qs = models.DataAffiliateAgreement.objects.filter(
- study=workspace.study, signed_agreement__is_primary=True
+ study=workspace.study, is_primary=True
)
primary_exists = primary_qs.exists()
diff --git a/primed/cdsa/forms.py b/primed/cdsa/forms.py
index cfa854d2..5ec7d116 100644
--- a/primed/cdsa/forms.py
+++ b/primed/cdsa/forms.py
@@ -22,12 +22,6 @@ class Meta:
class SignedAgreementForm(Bootstrap5MediaFormMixin, forms.ModelForm):
"""Form for a SignedAgreement object."""
- is_primary = forms.TypedChoiceField(
- coerce=lambda x: x == "True",
- choices=((True, "Primary"), (False, "Component")),
- widget=forms.RadioSelect,
- label="Agreement type",
- )
version = forms.ModelChoiceField(
queryset=models.AgreementVersion.objects.filter(major_version__is_valid=True)
)
@@ -41,7 +35,6 @@ class Meta:
"signing_institution",
"version",
"date_signed",
- "is_primary",
)
widgets = {
"representative": autocomplete.ModelSelect2(
@@ -63,20 +56,38 @@ class Meta:
class MemberAgreementForm(forms.ModelForm):
+ is_primary = forms.TypedChoiceField(
+ coerce=lambda x: x == "True",
+ choices=((True, "Primary"), (False, "Component")),
+ widget=forms.RadioSelect,
+ label="Agreement type",
+ )
+
class Meta:
model = models.MemberAgreement
fields = (
"signed_agreement",
"study_site",
+ "is_primary",
)
class DataAffiliateAgreementForm(Bootstrap5MediaFormMixin, forms.ModelForm):
+ is_primary = forms.TypedChoiceField(
+ coerce=lambda x: x == "True",
+ choices=((True, "Primary"), (False, "Component")),
+ widget=forms.RadioSelect,
+ label="Agreement type",
+ )
+
class Meta:
model = models.DataAffiliateAgreement
fields = (
"signed_agreement",
"study",
+ "is_primary",
+ "additional_limitations",
+ "requires_study_review",
)
widgets = {
"study": autocomplete.ModelSelect2(
@@ -114,7 +125,7 @@ class Meta:
"data_use_permission",
"data_use_modifiers",
"disease_term",
- "data_use_limitations",
+ "additional_limitations",
"gsr_restricted",
"acknowledgments",
"available_data",
diff --git a/primed/cdsa/helpers.py b/primed/cdsa/helpers.py
index 5f613969..ffc6a416 100644
--- a/primed/cdsa/helpers.py
+++ b/primed/cdsa/helpers.py
@@ -13,7 +13,7 @@ def get_study_records_table():
"""Return the queryset for study records."""
qs = models.DataAffiliateAgreement.objects.filter(
signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE,
- signed_agreement__is_primary=True,
+ is_primary=True,
)
return tables.StudyRecordsTable(qs)
diff --git a/primed/cdsa/migrations/0015_dataaffiliateagreement_additional_limitations_and_more.py b/primed/cdsa/migrations/0015_dataaffiliateagreement_additional_limitations_and_more.py
new file mode 100644
index 00000000..2db9518a
--- /dev/null
+++ b/primed/cdsa/migrations/0015_dataaffiliateagreement_additional_limitations_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.2.10 on 2024-03-12 21:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("cdsa", "0014_gsr_restricted_help_and_verbose"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="dataaffiliateagreement",
+ name="additional_limitations",
+ field=models.TextField(
+ blank=True,
+ help_text="Additional limitations on data use as specified in the signed CDSA.",
+ ),
+ ),
+ migrations.AddField(
+ model_name="historicaldataaffiliateagreement",
+ name="additional_limitations",
+ field=models.TextField(
+ blank=True,
+ help_text="Additional limitations on data use as specified in the signed CDSA.",
+ ),
+ ),
+ ]
diff --git a/primed/cdsa/migrations/0016_rename_data_use_limitations_cdsaworkspace_additional_limitations_and_more.py b/primed/cdsa/migrations/0016_rename_data_use_limitations_cdsaworkspace_additional_limitations_and_more.py
new file mode 100644
index 00000000..6679fa3e
--- /dev/null
+++ b/primed/cdsa/migrations/0016_rename_data_use_limitations_cdsaworkspace_additional_limitations_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.10 on 2024-03-14 18:35
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("cdsa", "0015_dataaffiliateagreement_additional_limitations_and_more"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="cdsaworkspace",
+ old_name="data_use_limitations",
+ new_name="additional_limitations",
+ ),
+ migrations.RenameField(
+ model_name="historicalcdsaworkspace",
+ old_name="data_use_limitations",
+ new_name="additional_limitations",
+ ),
+ ]
diff --git a/primed/cdsa/migrations/0017_alter_cdsaworkspace_additional_limitations_and_more.py b/primed/cdsa/migrations/0017_alter_cdsaworkspace_additional_limitations_and_more.py
new file mode 100644
index 00000000..79b56330
--- /dev/null
+++ b/primed/cdsa/migrations/0017_alter_cdsaworkspace_additional_limitations_and_more.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.2.10 on 2024-03-14 18:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ (
+ "cdsa",
+ "0016_rename_data_use_limitations_cdsaworkspace_additional_limitations_and_more",
+ ),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="cdsaworkspace",
+ name="additional_limitations",
+ field=models.TextField(
+ blank=True,
+ help_text="Additional data use limitations that cannot be captured by DUO.",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="historicalcdsaworkspace",
+ name="additional_limitations",
+ field=models.TextField(
+ blank=True,
+ help_text="Additional data use limitations that cannot be captured by DUO.",
+ ),
+ ),
+ ]
diff --git a/primed/cdsa/migrations/0018_dataaffiliateagreement_requires_study_review_and_more.py b/primed/cdsa/migrations/0018_dataaffiliateagreement_requires_study_review_and_more.py
new file mode 100644
index 00000000..de611aa7
--- /dev/null
+++ b/primed/cdsa/migrations/0018_dataaffiliateagreement_requires_study_review_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.2.10 on 2024-03-15 20:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("cdsa", "0017_alter_cdsaworkspace_additional_limitations_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="dataaffiliateagreement",
+ name="requires_study_review",
+ field=models.BooleanField(
+ default=False,
+ help_text="Indicator of whether indicates investigators need to have an approved PRIMED paper proposal where this dataset was selected and approved in order to work with data brought under this CDSA.",
+ ),
+ ),
+ migrations.AddField(
+ model_name="historicaldataaffiliateagreement",
+ name="requires_study_review",
+ field=models.BooleanField(
+ default=False,
+ help_text="Indicator of whether indicates investigators need to have an approved PRIMED paper proposal where this dataset was selected and approved in order to work with data brought under this CDSA.",
+ ),
+ ),
+ ]
diff --git a/primed/cdsa/migrations/0019_alter_signedagreement_is_primary.py b/primed/cdsa/migrations/0019_alter_signedagreement_is_primary.py
new file mode 100644
index 00000000..cc8ab57f
--- /dev/null
+++ b/primed/cdsa/migrations/0019_alter_signedagreement_is_primary.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.2.10 on 2024-03-18 17:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("cdsa", "0018_dataaffiliateagreement_requires_study_review_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="historicalsignedagreement",
+ name="is_primary",
+ field=models.BooleanField(
+ help_text="Indicator of whether this is a primary Agreement (and not a component Agreement).",
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="signedagreement",
+ name="is_primary",
+ field=models.BooleanField(
+ help_text="Indicator of whether this is a primary Agreement (and not a component Agreement).",
+ null=True,
+ ),
+ ),
+ ]
diff --git a/primed/cdsa/migrations/0020_dataaffiliateagreement_is_primary_and_more.py b/primed/cdsa/migrations/0020_dataaffiliateagreement_is_primary_and_more.py
new file mode 100644
index 00000000..181a5bac
--- /dev/null
+++ b/primed/cdsa/migrations/0020_dataaffiliateagreement_is_primary_and_more.py
@@ -0,0 +1,49 @@
+# Generated by Django 4.2.10 on 2024-03-15 22:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("cdsa", "0019_alter_signedagreement_is_primary"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="dataaffiliateagreement",
+ name="is_primary",
+ field=models.BooleanField(
+ default=False,
+ help_text="Indicator of whether this is a primary Agreement (and not a component Agreement).",
+ ),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name="historicaldataaffiliateagreement",
+ name="is_primary",
+ field=models.BooleanField(
+ default=False,
+ help_text="Indicator of whether this is a primary Agreement (and not a component Agreement).",
+ ),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name="historicalmemberagreement",
+ name="is_primary",
+ field=models.BooleanField(
+ default=False,
+ help_text="Indicator of whether this is a primary Agreement (and not a component Agreement).",
+ ),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name="memberagreement",
+ name="is_primary",
+ field=models.BooleanField(
+ default=False,
+ help_text="Indicator of whether this is a primary Agreement (and not a component Agreement).",
+ ),
+ preserve_default=False,
+ ),
+ ]
diff --git a/primed/cdsa/migrations/0021_populate_is_primary.py b/primed/cdsa/migrations/0021_populate_is_primary.py
new file mode 100644
index 00000000..11cbd17a
--- /dev/null
+++ b/primed/cdsa/migrations/0021_populate_is_primary.py
@@ -0,0 +1,60 @@
+# Generated by Django 4.2.10 on 2024-03-15 22:44
+
+from django.db import migrations
+
+def forward_populate_memberagreement_is_primary(apps, schema_editor):
+ """Populate the MemberAgreement is_primary field using is_primary from the associated SignedAgreement."""
+ MemberAgreement = apps.get_model("cdsa", "MemberAgreement")
+ for row in MemberAgreement.objects.all():
+ row.is_primary = row.signed_agreement.is_primary
+ row.save(update_fields=["is_primary"])
+
+
+def forward_populate_dataaffiliateagreement_is_primary(apps, schema_editor):
+ """Populate the DataAffiliateAgreement is_primary field using is_primary from the associated SignedAgreement."""
+ DataAffiliateAgreement = apps.get_model("cdsa", "DataAffiliateAgreement")
+ for row in DataAffiliateAgreement.objects.all():
+ row.is_primary = row.signed_agreement.is_primary
+ row.save(update_fields=["is_primary"])
+
+def backward_populate_memberagreement_is_primary(apps, schema_editor):
+ """Populate the MemberAgreement is_primary field using is_primary from the associated SignedAgreement."""
+ MemberAgreement = apps.get_model("cdsa", "MemberAgreement")
+ for row in MemberAgreement.objects.all():
+ row.signed_agreement.is_primary = row.is_primary
+ row.signed_agreement.save(update_fields=["is_primary"])
+
+def backward_populate_dataaffiliateagreement_is_primary(apps, schema_editor):
+ """Populate the DataAffiliateAgreement is_primary field using is_primary from the associated SignedAgreement."""
+ DataAffiliateAgreement = apps.get_model("cdsa", "DataAffiliateAgreement")
+ for row in DataAffiliateAgreement.objects.all():
+ row.signed_agreement.is_primary = row.is_primary
+ row.signed_agreement.save(update_fields=["is_primary"])
+
+def backward_populate_nondataaffiliateagreement_is_primary(apps, schema_editor):
+ """Set the NonDataAffiliateAgreement.signed_agreement.is_primary field to True."""
+ NonDataAffiliateAgreement = apps.get_model("cdsa", "NonDataAffiliateAgreement")
+ for row in NonDataAffiliateAgreement.objects.all():
+ row.signed_agreement.is_primary = True
+ row.signed_agreement.save(update_fields=["is_primary"])
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("cdsa", "0020_dataaffiliateagreement_is_primary_and_more"),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ forward_populate_memberagreement_is_primary,
+ reverse_code=backward_populate_memberagreement_is_primary,
+ ),
+ migrations.RunPython(
+ forward_populate_dataaffiliateagreement_is_primary,
+ reverse_code=backward_populate_dataaffiliateagreement_is_primary,
+ ),
+ migrations.RunPython(
+ migrations.RunPython.noop,
+ reverse_code=backward_populate_nondataaffiliateagreement_is_primary,
+ ),
+ ]
diff --git a/primed/cdsa/migrations/0022_remove_signedagreement_is_primary.py b/primed/cdsa/migrations/0022_remove_signedagreement_is_primary.py
new file mode 100644
index 00000000..c476b7a0
--- /dev/null
+++ b/primed/cdsa/migrations/0022_remove_signedagreement_is_primary.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.10 on 2024-03-18 17:31
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("cdsa", "0021_populate_is_primary"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="historicalsignedagreement",
+ name="is_primary",
+ ),
+ migrations.RemoveField(
+ model_name="signedagreement",
+ name="is_primary",
+ ),
+ ]
diff --git a/primed/cdsa/models.py b/primed/cdsa/models.py
index 4896a3ed..4ba3cdec 100644
--- a/primed/cdsa/models.py
+++ b/primed/cdsa/models.py
@@ -155,9 +155,6 @@ class SignedAgreement(
max_length=31,
choices=TYPE_CHOICES,
)
- is_primary = models.BooleanField(
- help_text="Indicator of whether this is a primary Agreement (and not a component Agreement).",
- )
version = models.ForeignKey(
AgreementVersion,
help_text="The version of the Agreement that was signed.",
@@ -178,16 +175,15 @@ class SignedAgreement(
def __str__(self):
return "{}".format(self.cc_id)
- def clean(self):
- if self.type == self.NON_DATA_AFFILIATE and self.is_primary is False:
- raise ValidationError(
- "Non-data affiliate agreements must be primary agreements."
- )
-
@property
def combined_type(self):
combined_type = self.get_type_display()
- if not self.is_primary:
+ if self.type == self.MEMBER and not self.get_agreement_type().is_primary:
+ combined_type = combined_type + " component"
+ elif (
+ self.type == self.DATA_AFFILIATE
+ and not self.get_agreement_type().is_primary
+ ):
combined_type = combined_type + " component"
return combined_type
@@ -250,6 +246,9 @@ class MemberAgreement(TimeStampedModel, AgreementTypeModel, models.Model):
AGREEMENT_TYPE = SignedAgreement.MEMBER
+ is_primary = models.BooleanField(
+ help_text="Indicator of whether this is a primary Agreement (and not a component Agreement).",
+ )
study_site = models.ForeignKey(
StudySite,
on_delete=models.CASCADE,
@@ -271,12 +270,27 @@ class DataAffiliateAgreement(TimeStampedModel, AgreementTypeModel, models.Model)
AGREEMENT_TYPE = SignedAgreement.DATA_AFFILIATE
+ is_primary = models.BooleanField(
+ help_text="Indicator of whether this is a primary Agreement (and not a component Agreement).",
+ )
study = models.ForeignKey(
Study,
on_delete=models.PROTECT,
help_text="Study that this agreement is associated with.",
)
anvil_upload_group = models.ForeignKey(ManagedGroup, on_delete=models.PROTECT)
+ additional_limitations = models.TextField(
+ blank=True,
+ help_text="Additional limitations on data use as specified in the signed CDSA.",
+ )
+ requires_study_review = models.BooleanField(
+ default=False,
+ help_text=(
+ "Indicator of whether indicates investigators need to have an approved PRIMED paper proposal "
+ "where this dataset was selected and approved in order to work with data brought "
+ "under this CDSA."
+ ),
+ )
def get_absolute_url(self):
return reverse(
@@ -284,6 +298,22 @@ def get_absolute_url(self):
kwargs={"cc_id": self.signed_agreement.cc_id},
)
+ def clean(self):
+ super().clean()
+ # Checks for fields only allowed for primary agreements.
+ errors = {}
+ if not self.is_primary:
+ if self.additional_limitations:
+ errors["additional_limitations"] = ValidationError(
+ "Additional limitations are only allowed for primary agreements."
+ )
+ if self.requires_study_review:
+ errors["requires_study_review"] = ValidationError(
+ "requires_study_review can only be True for primary agreements."
+ )
+ if errors:
+ raise ValidationError(errors)
+
def get_agreement_group(self):
return self.study
@@ -318,8 +348,9 @@ class CDSAWorkspace(
on_delete=models.PROTECT,
help_text="The study associated with data in this workspace.",
)
- data_use_limitations = models.TextField(
- help_text="""The full data use limitations for this workspace."""
+ additional_limitations = models.TextField(
+ help_text="""Additional data use limitations that cannot be captured by DUO.""",
+ blank=True,
)
acknowledgments = models.TextField(
help_text="Acknowledgments associated with data in this workspace."
@@ -337,3 +368,12 @@ class CDSAWorkspace(
class Meta:
verbose_name = " CDSA workspace"
verbose_name_plural = " CDSA workspaces"
+
+ def get_primary_cdsa(self):
+ """Return the primary, valid CDSA associated with this workspace."""
+ cdsa = DataAffiliateAgreement.objects.get(
+ study=self.study,
+ is_primary=True,
+ signed_agreement__status=SignedAgreement.StatusChoices.ACTIVE,
+ )
+ return cdsa
diff --git a/primed/cdsa/tables.py b/primed/cdsa/tables.py
index c5f9ab6d..f39d6988 100644
--- a/primed/cdsa/tables.py
+++ b/primed/cdsa/tables.py
@@ -6,6 +6,7 @@
Workspace,
WorkspaceGroupSharing,
)
+from django.utils.safestring import mark_safe
from primed.primed_anvil.tables import (
BooleanIconColumn,
@@ -43,9 +44,7 @@ class SignedAgreementTable(tables.Table):
verbose_name="Representative",
)
representative_role = tables.Column(verbose_name="Role")
- agreement_type = tables.Column(
- accessor="combined_type", order_by=("type", "-is_primary")
- )
+ agreement_type = tables.Column(accessor="combined_type", order_by=("type"))
number_accessors = tables.Column(
verbose_name="Number of accessors",
accessor="anvil_access_group__groupaccountmembership_set__count",
@@ -79,7 +78,7 @@ class MemberAgreementTable(tables.Table):
signed_agreement__cc_id = tables.Column(linkify=True)
study_site = tables.Column(linkify=True)
- signed_agreement__is_primary = BooleanIconColumn(verbose_name="Primary?")
+ is_primary = BooleanIconColumn(verbose_name="Primary?")
signed_agreement__representative__name = tables.Column(
linkify=lambda record: record.signed_agreement.representative.get_absolute_url(),
verbose_name="Representative",
@@ -96,7 +95,7 @@ class Meta:
fields = (
"signed_agreement__cc_id",
"study_site",
- "signed_agreement__is_primary",
+ "is_primary",
"signed_agreement__representative__name",
"signed_agreement__representative_role",
"signed_agreement__signing_institution",
@@ -113,7 +112,13 @@ class DataAffiliateAgreementTable(tables.Table):
signed_agreement__cc_id = tables.Column(linkify=True)
study = tables.Column(linkify=True)
- signed_agreement__is_primary = BooleanIconColumn(verbose_name="Primary?")
+ is_primary = BooleanIconColumn(verbose_name="Primary?")
+ requires_study_review = BooleanIconColumn(
+ verbose_name="Study review required?",
+ orderable=False,
+ true_icon="dash-circle-fill",
+ true_color="#ffc107",
+ )
signed_agreement__representative__name = tables.Column(
linkify=lambda record: record.signed_agreement.representative.get_absolute_url(),
verbose_name="Representative",
@@ -130,7 +135,8 @@ class Meta:
fields = (
"signed_agreement__cc_id",
"study",
- "signed_agreement__is_primary",
+ "is_primary",
+ "requires_study_review",
"signed_agreement__representative__name",
"signed_agreement__representative_role",
"signed_agreement__signing_institution",
@@ -178,9 +184,7 @@ class RepresentativeRecordsTable(tables.Table):
representative__name = tables.Column(verbose_name="Representative")
signing_group = tables.Column(accessor="pk", orderable=False)
- agreement_type = tables.Column(
- accessor="combined_type", order_by=("type", "-is_primary")
- )
+ agreement_type = tables.Column(accessor="combined_type", order_by=("type"))
class Meta:
model = models.SignedAgreement
@@ -295,11 +299,10 @@ def render_date_shared(self, record):
return "—"
-class CDSAWorkspaceStaffTable(tables.Table):
+class CDSAWorkspaceUserTable(tables.Table):
"""A table for the CDSAWorkspace model."""
name = tables.Column(linkify=True)
- billing_project = tables.Column(linkify=True)
cdsaworkspace__data_use_permission__abbreviation = tables.Column(
verbose_name="DUO permission",
linkify=lambda record: record.cdsaworkspace.data_use_permission.get_absolute_url(),
@@ -310,6 +313,12 @@ class CDSAWorkspaceStaffTable(tables.Table):
verbose_name="DUO modifiers",
linkify_item=True,
)
+ cdsaworkspace__requires_study_review = BooleanIconColumn(
+ verbose_name="Study review required?",
+ orderable=False,
+ true_icon="dash-circle-fill",
+ true_color="#ffc107",
+ )
cdsaworkspace__gsr_restricted = BooleanIconColumn(
orderable=False, true_icon="dash-circle-fill", true_color="#ffc107"
)
@@ -319,30 +328,31 @@ class Meta:
model = Workspace
fields = (
"name",
- "billing_project",
"cdsaworkspace__study",
"cdsaworkspace__data_use_permission__abbreviation",
"cdsaworkspace__data_use_modifiers",
+ "cdsaworkspace__requires_study_review",
"cdsaworkspace__gsr_restricted",
)
order_by = ("name",)
-
-class CDSAWorkspaceUserTable(tables.Table):
+ def render_cdsaworkspace__requires_study_review(self, record):
+ try:
+ if record.cdsaworkspace.get_primary_cdsa().requires_study_review:
+ icon = "dash-circle-fill"
+ color = "#ffc107"
+ else:
+ return ""
+ except models.DataAffiliateAgreement.DoesNotExist:
+ icon = "question-circle-fill"
+ color = "red"
+ return mark_safe(f'')
+
+
+class CDSAWorkspaceStaffTable(CDSAWorkspaceUserTable):
"""A table for the CDSAWorkspace model."""
- name = tables.Column(linkify=True)
- billing_project = tables.Column()
- cdsaworkspace__data_use_permission__abbreviation = tables.Column(
- verbose_name="DUO permission",
- )
- cdsaworkspace__study = tables.Column()
- cdsaworkspace__data_use_modifiers = tables.ManyToManyColumn(
- transform=lambda x: x.abbreviation,
- verbose_name="DUO modifiers",
- )
- cdsaworkspace__gsr_restricted = BooleanIconColumn(orderable=False)
- is_shared = WorkspaceSharedWithConsortiumColumn()
+ billing_project = tables.Column(linkify=True)
class Meta:
model = Workspace
@@ -352,6 +362,7 @@ class Meta:
"cdsaworkspace__study",
"cdsaworkspace__data_use_permission__abbreviation",
"cdsaworkspace__data_use_modifiers",
+ "cdsaworkspace__requires_study_review",
"cdsaworkspace__gsr_restricted",
)
order_by = ("name",)
diff --git a/primed/cdsa/tests/factories.py b/primed/cdsa/tests/factories.py
index 610b63b4..6d7473d9 100644
--- a/primed/cdsa/tests/factories.py
+++ b/primed/cdsa/tests/factories.py
@@ -46,8 +46,6 @@ class SignedAgreementFactory(DjangoModelFactory):
models.SignedAgreement.TYPE_CHOICES,
getter=lambda c: c[0],
)
- # Assume is_primary=True for now.
- is_primary = True
version = SubFactory(AgreementVersionFactory)
date_signed = Faker("date")
anvil_access_group = SubFactory(
@@ -69,6 +67,7 @@ class MemberAgreementFactory(DjangoModelFactory):
SignedAgreementFactory, type=models.SignedAgreement.MEMBER
)
study_site = SubFactory(StudySiteFactory)
+ is_primary = True
class Meta:
model = models.MemberAgreement
@@ -80,6 +79,7 @@ class DataAffiliateAgreementFactory(DjangoModelFactory):
SignedAgreementFactory, type=models.SignedAgreement.DATA_AFFILIATE
)
study = SubFactory(StudyFactory)
+ is_primary = True
anvil_upload_group = SubFactory(
ManagedGroupFactory,
name=LazyAttribute(
@@ -107,7 +107,6 @@ class Meta:
class CDSAWorkspaceFactory(DjangoModelFactory):
study = SubFactory(StudyFactory)
- data_use_limitations = Faker("paragraph")
acknowledgments = Faker("paragraph")
requested_by = SubFactory(UserFactory)
data_use_permission = SubFactory(DataUsePermissionFactory)
diff --git a/primed/cdsa/tests/test_audit.py b/primed/cdsa/tests/test_audit.py
index 467ff57f..ef5fc61c 100644
--- a/primed/cdsa/tests/test_audit.py
+++ b/primed/cdsa/tests/test_audit.py
@@ -322,7 +322,7 @@ def test_member_component_has_primary_in_group(self):
study_site = StudySiteFactory.create()
factories.MemberAgreementFactory.create(study_site=study_site)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False, study_site=study_site
+ is_primary=False, study_site=study_site
)
# Add the signed agreement access group to the CDSA group.
GroupGroupMembershipFactory.create(
@@ -344,7 +344,7 @@ def test_member_component_has_primary_not_in_group(self):
study_site = StudySiteFactory.create()
factories.MemberAgreementFactory.create(study_site=study_site)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False, study_site=study_site
+ is_primary=False, study_site=study_site
)
# # Add the signed agreement access group to the CDSA group.
# GroupGroupMembershipFactory.create(
@@ -366,7 +366,7 @@ def test_member_component_inactive_has_primary_in_group(self):
study_site = StudySiteFactory.create()
factories.MemberAgreementFactory.create(study_site=study_site)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study_site=study_site,
signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN,
)
@@ -390,7 +390,7 @@ def test_member_component_inactive_has_primary_not_in_group(self):
study_site = StudySiteFactory.create()
factories.MemberAgreementFactory.create(study_site=study_site)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study_site=study_site,
signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN,
)
@@ -417,7 +417,7 @@ def test_member_component_has_primary_with_invalid_version_in_group(self):
signed_agreement__version__major_version__is_valid=False,
)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False, study_site=study_site
+ is_primary=False, study_site=study_site
)
# Add the signed agreement access group to the CDSA group.
GroupGroupMembershipFactory.create(
@@ -442,7 +442,7 @@ def test_member_component_has_primary_with_invalid_version_not_in_group(self):
signed_agreement__version__major_version__is_valid=False,
)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False, study_site=study_site
+ is_primary=False, study_site=study_site
)
# # Add the signed agreement access group to the CDSA group.
# GroupGroupMembershipFactory.create(
@@ -467,7 +467,7 @@ def test_member_component_has_inactive_primary_in_group(self):
signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN,
)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False, study_site=study_site
+ is_primary=False, study_site=study_site
)
# Add the signed agreement access group to the CDSA group.
GroupGroupMembershipFactory.create(
@@ -492,7 +492,7 @@ def test_member_component_has_inactive_primary_not_in_group(self):
signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN,
)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False, study_site=study_site
+ is_primary=False, study_site=study_site
)
# # Add the signed agreement access group to the CDSA group.
# GroupGroupMembershipFactory.create(
@@ -513,7 +513,7 @@ def test_member_component_no_primary_in_group(self):
"""Member component agreement, with valid version, with no primary, in CDSA group."""
study_site = StudySiteFactory.create()
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False, study_site=study_site
+ is_primary=False, study_site=study_site
)
# Add the signed agreement access group to the CDSA group.
GroupGroupMembershipFactory.create(
@@ -534,7 +534,7 @@ def test_member_component_no_primary_not_in_group(self):
"""Member component agreement, with valid version, with no primary, not in CDSA group."""
study_site = StudySiteFactory.create()
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False, study_site=study_site
+ is_primary=False, study_site=study_site
)
# # Add the signed agreement access group to the CDSA group.
# GroupGroupMembershipFactory.create(
@@ -556,7 +556,7 @@ def test_member_component_invalid_version_has_primary_in_group(self):
study_site = StudySiteFactory.create()
factories.MemberAgreementFactory.create(study_site=study_site)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study_site=study_site,
signed_agreement__version__major_version__is_valid=False,
)
@@ -580,7 +580,7 @@ def test_member_component_invalid_version_has_primary_not_in_group(self):
study_site = StudySiteFactory.create()
factories.MemberAgreementFactory.create(study_site=study_site)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study_site=study_site,
signed_agreement__version__major_version__is_valid=False,
)
@@ -606,7 +606,7 @@ def test_member_component_invalid_version_has_primary_with_invalid_version_in_gr
study_site = StudySiteFactory.create()
factories.MemberAgreementFactory.create(study_site=study_site)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study_site=study_site,
signed_agreement__version__major_version__is_valid=False,
)
@@ -632,7 +632,7 @@ def test_member_component_invalid_version_has_primary_with_invalid_version_not_i
study_site = StudySiteFactory.create()
factories.MemberAgreementFactory.create(study_site=study_site)
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study_site=study_site,
signed_agreement__version__major_version__is_valid=False,
)
@@ -654,7 +654,7 @@ def test_member_component_invalid_version_has_primary_with_invalid_version_not_i
def test_member_component_invalid_version_no_primary_in_group(self):
"""Member component agreement, with invalid version, with no primary, in CDSA group."""
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
signed_agreement__version__major_version__is_valid=False,
)
# Add the signed agreement access group to the CDSA group.
@@ -675,7 +675,7 @@ def test_member_component_invalid_version_no_primary_in_group(self):
def test_member_component_invalid_version_no_primary_not_in_group(self):
"""Member component agreement, with invalid version, with no primary, not in CDSA group."""
this_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
signed_agreement__version__major_version__is_valid=False,
)
# # Add the signed agreement access group to the CDSA group.
@@ -814,7 +814,7 @@ def test_data_affiliate_component_has_primary_in_group(self):
study = StudyFactory.create()
factories.DataAffiliateAgreementFactory.create(study=study)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False, study=study
+ is_primary=False, study=study
)
# Add the signed agreement access group to the CDSA group.
GroupGroupMembershipFactory.create(
@@ -836,7 +836,7 @@ def test_data_affiliate_component_has_primary_not_in_group(self):
study = StudyFactory.create()
factories.DataAffiliateAgreementFactory.create(study=study)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False, study=study
+ is_primary=False, study=study
)
# # Add the signed agreement access group to the CDSA group.
# GroupGroupMembershipFactory.create(
@@ -858,7 +858,7 @@ def test_data_affiliate_component_inactive_has_primary_in_group(self):
study = StudyFactory.create()
factories.DataAffiliateAgreementFactory.create(study=study)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study=study,
signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN,
)
@@ -882,7 +882,7 @@ def test_data_affiliate_component_inactive_has_primary_not_in_group(self):
study = StudyFactory.create()
factories.DataAffiliateAgreementFactory.create(study=study)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study=study,
signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN,
)
@@ -909,7 +909,7 @@ def test_data_affiliate_component_has_primary_with_invalid_version_in_group(self
signed_agreement__version__major_version__is_valid=False,
)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False, study=study
+ is_primary=False, study=study
)
# Add the signed agreement access group to the CDSA group.
GroupGroupMembershipFactory.create(
@@ -936,7 +936,7 @@ def test_data_affiliate_component_has_primary_with_invalid_version_not_in_group(
signed_agreement__version__major_version__is_valid=False,
)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False, study=study
+ is_primary=False, study=study
)
# # Add the signed agreement access group to the CDSA group.
# GroupGroupMembershipFactory.create(
@@ -961,7 +961,7 @@ def test_data_affiliate_component_has_inactive_primary_in_group(self):
signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN,
)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False, study=study
+ is_primary=False, study=study
)
# Add the signed agreement access group to the CDSA group.
GroupGroupMembershipFactory.create(
@@ -986,7 +986,7 @@ def test_data_affiliate_component_has_inactive_primary_not_in_group(self):
signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN,
)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False, study=study
+ is_primary=False, study=study
)
# # Add the signed agreement access group to the CDSA group.
# GroupGroupMembershipFactory.create(
@@ -1007,7 +1007,7 @@ def test_data_affiliate_component_no_primary_in_group(self):
"""Member component agreement, with valid version, with no primary, in CDSA group."""
study = StudyFactory.create()
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False, study=study
+ is_primary=False, study=study
)
# Add the signed agreement access group to the CDSA group.
GroupGroupMembershipFactory.create(
@@ -1028,7 +1028,7 @@ def test_data_affiliate_component_no_primary_not_in_group(self):
"""Member component agreement, with valid version, with no primary, not in CDSA group."""
study = StudyFactory.create()
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False, study=study
+ is_primary=False, study=study
)
# # Add the signed agreement access group to the CDSA group.
# GroupGroupMembershipFactory.create(
@@ -1050,7 +1050,7 @@ def test_data_affiliate_component_invalid_version_has_primary_in_group(self):
study = StudyFactory.create()
factories.DataAffiliateAgreementFactory.create(study=study)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study=study,
signed_agreement__version__major_version__is_valid=False,
)
@@ -1074,7 +1074,7 @@ def test_data_affiliate_component_invalid_version_has_primary_not_in_group(self)
study = StudyFactory.create()
factories.DataAffiliateAgreementFactory.create(study=study)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study=study,
signed_agreement__version__major_version__is_valid=False,
)
@@ -1100,7 +1100,7 @@ def test_data_affiliate_component_invalid_version_has_primary_with_invalid_versi
study = StudyFactory.create()
factories.DataAffiliateAgreementFactory.create(study=study)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study=study,
signed_agreement__version__major_version__is_valid=False,
)
@@ -1126,7 +1126,7 @@ def test_data_affiliate_component_invalid_version_has_primary_with_invalid_versi
study = StudyFactory.create()
factories.DataAffiliateAgreementFactory.create(study=study)
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
study=study,
signed_agreement__version__major_version__is_valid=False,
)
@@ -1148,7 +1148,7 @@ def test_data_affiliate_component_invalid_version_has_primary_with_invalid_versi
def test_data_affiliate_component_invalid_version_no_primary_in_group(self):
"""Member component agreement, with invalid version, with no primary, in CDSA group."""
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
signed_agreement__version__major_version__is_valid=False,
)
# Add the signed agreement access group to the CDSA group.
@@ -1169,7 +1169,7 @@ def test_data_affiliate_component_invalid_version_no_primary_in_group(self):
def test_data_affiliate_component_invalid_version_no_primary_not_in_group(self):
"""Member component agreement, with invalid version, with no primary, not in CDSA group."""
this_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False,
+ is_primary=False,
signed_agreement__version__major_version__is_valid=False,
)
# # Add the signed agreement access group to the CDSA group.
@@ -1303,46 +1303,6 @@ def test_non_data_affiliate_primary_valid_not_active_not_in_group(self):
self.assertEqual(record.signed_agreement, this_agreement.signed_agreement)
self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT)
- def test_non_data_affiliate_component_in_cdsa_group(self):
- """Non data affiliate component agreement."""
- this_agreement = factories.NonDataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False
- )
- # Add the signed agreement access group to the CDSA group.
- GroupGroupMembershipFactory.create(
- parent_group=self.cdsa_group,
- child_group=this_agreement.signed_agreement.anvil_access_group,
- )
- cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit()
- cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement)
- self.assertEqual(len(cdsa_audit.verified), 0)
- self.assertEqual(len(cdsa_audit.needs_action), 0)
- self.assertEqual(len(cdsa_audit.errors), 1)
- record = cdsa_audit.errors[0]
- self.assertIsInstance(record, signed_agreement_audit.OtherError)
- self.assertEqual(record.signed_agreement, this_agreement.signed_agreement)
- self.assertEqual(record.note, cdsa_audit.ERROR_NON_DATA_AFFILIATE_COMPONENT)
-
- def test_non_data_affiliate_component_not_in_cdsa_group(self):
- """Non data affiliate component agreement."""
- this_agreement = factories.NonDataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False
- )
- # Do not add the signed agreement access group to the CDSA group.
- # GroupGroupMembershipFactory.create(
- # parent_group=self.cdsa_group,
- # child_group=this_agreement.signed_agreement.anvil_access_group,
- # )
- cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit()
- cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement)
- self.assertEqual(len(cdsa_audit.verified), 0)
- self.assertEqual(len(cdsa_audit.needs_action), 0)
- self.assertEqual(len(cdsa_audit.errors), 1)
- record = cdsa_audit.errors[0]
- self.assertIsInstance(record, signed_agreement_audit.OtherError)
- self.assertEqual(record.signed_agreement, this_agreement.signed_agreement)
- self.assertEqual(record.note, cdsa_audit.ERROR_NON_DATA_AFFILIATE_COMPONENT)
-
class SignedAgreementAccessAuditTableTest(TestCase):
"""Tests for the `SignedAgreementAccessAuditTable` table."""
@@ -1804,9 +1764,7 @@ def test_primary_inactive_in_auth_domain(self):
def test_component_agreement_not_in_auth_domain(self):
study = StudyFactory.create()
workspace = factories.CDSAWorkspaceFactory.create(study=study)
- factories.DataAffiliateAgreementFactory.create(
- study=study, signed_agreement__is_primary=False
- )
+ factories.DataAffiliateAgreementFactory.create(study=study, is_primary=False)
# Do not add the CDSA group to the auth domain.
# GroupGroupMembershipFactory.create(
# parent_group=workspace.workspace.authorization_domains.first(),
@@ -1826,9 +1784,7 @@ def test_component_agreement_not_in_auth_domain(self):
def test_component_agreement_in_auth_domain(self):
study = StudyFactory.create()
workspace = factories.CDSAWorkspaceFactory.create(study=study)
- factories.DataAffiliateAgreementFactory.create(
- study=study, signed_agreement__is_primary=False
- )
+ factories.DataAffiliateAgreementFactory.create(study=study, is_primary=False)
# Add the CDSA group to the auth domain.
GroupGroupMembershipFactory.create(
parent_group=workspace.workspace.authorization_domains.first(),
diff --git a/primed/cdsa/tests/test_commands.py b/primed/cdsa/tests/test_commands.py
index 916bd62f..fbef817c 100644
--- a/primed/cdsa/tests/test_commands.py
+++ b/primed/cdsa/tests/test_commands.py
@@ -68,9 +68,7 @@ def test_study_records_zero(self):
self.assertEqual(len(lines), 1)
def test_study_records_one(self):
- factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ factories.DataAffiliateAgreementFactory.create(is_primary=True)
out = StringIO()
call_command("cdsa_records", "--outdir", self.outdir, "--no-color", stdout=out)
with open(os.path.join(self.outdir, "study_records.tsv")) as f:
@@ -135,7 +133,7 @@ def test_command_output_no_records(self):
def test_command_run_audit_one_agreement_verified(self):
"""Test command output with one verified instance."""
- factories.MemberAgreementFactory.create(signed_agreement__is_primary=False)
+ factories.MemberAgreementFactory.create(is_primary=False)
out = StringIO()
call_command("run_cdsa_audit", "--no-color", stdout=out)
expected_output = (
@@ -174,9 +172,7 @@ def test_command_run_audit_one_agreement_needs_action(self):
def test_command_run_audit_one_agreement_error(self):
"""Test command output with one error instance."""
- agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False
- )
+ agreement = factories.MemberAgreementFactory.create(is_primary=False)
GroupGroupMembershipFactory.create(
parent_group=self.cdsa_group,
child_group=agreement.signed_agreement.anvil_access_group,
@@ -197,7 +193,7 @@ def test_command_run_audit_one_agreement_error(self):
def test_command_run_audit_one_agreement_verified_email(self):
"""No email is sent when there are no errors."""
- factories.MemberAgreementFactory.create(signed_agreement__is_primary=False)
+ factories.MemberAgreementFactory.create(is_primary=False)
out = StringIO()
call_command(
"run_cdsa_audit", "--no-color", email="test@example.com", stdout=out
@@ -233,9 +229,7 @@ def test_command_run_audit_one_agreement_needs_action_email(self):
def test_command_run_audit_one_agreement_error_email(self):
"""Test command output with one error instance."""
- agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False
- )
+ agreement = factories.MemberAgreementFactory.create(is_primary=False)
GroupGroupMembershipFactory.create(
parent_group=self.cdsa_group,
child_group=agreement.signed_agreement.anvil_access_group,
diff --git a/primed/cdsa/tests/test_forms.py b/primed/cdsa/tests/test_forms.py
index 7662e422..1dcea0af 100644
--- a/primed/cdsa/tests/test_forms.py
+++ b/primed/cdsa/tests/test_forms.py
@@ -35,7 +35,6 @@ def test_valid(self):
"signing_institution": "Test insitution",
"version": self.agreement_version,
"date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertTrue(form.is_valid())
@@ -49,7 +48,6 @@ def test_missing_representative(self):
"signing_institution": "Test insitution",
"version": self.agreement_version,
"date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -67,7 +65,6 @@ def test_missing_cc_id(self):
"signing_institution": "Test insitution",
"version": self.agreement_version,
"date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -85,7 +82,6 @@ def test_missing_representative_role(self):
"signing_institution": "Test insitution",
"version": self.agreement_version,
"date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -103,7 +99,6 @@ def test_missing_signing_institution(self):
# "signing_institution": "Test insitution",
"version": self.agreement_version,
"date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -121,7 +116,6 @@ def test_missing_version(self):
"signing_institution": "Test insitution",
# "version": self.agreement_version,
"date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -139,7 +133,6 @@ def test_missing_date_signed(self):
"signing_institution": "Test insitution",
"version": self.agreement_version,
# "date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -148,24 +141,6 @@ def test_missing_date_signed(self):
self.assertEqual(len(form.errors["date_signed"]), 1)
self.assertIn("required", form.errors["date_signed"][0])
- def test_missing_is_primary(self):
- """Form is invalid when missing representative_role."""
- form_data = {
- "cc_id": 1234,
- "representative": self.representative,
- "representative_role": "Test role",
- "signing_institution": "Test insitution",
- "version": self.agreement_version,
- "date_signed": "2023-01-01",
- # "is_primary": True,
- }
- form = self.form_class(data=form_data)
- self.assertFalse(form.is_valid())
- self.assertEqual(len(form.errors), 1)
- self.assertIn("is_primary", form.errors)
- self.assertEqual(len(form.errors["is_primary"]), 1)
- self.assertIn("required", form.errors["is_primary"][0])
-
def test_invalid_cc_id_zero(self):
"""Form is invalid when cc_id is zero."""
form_data = {
@@ -175,7 +150,6 @@ def test_invalid_cc_id_zero(self):
"signing_institution": "Test insitution",
"version": self.agreement_version,
"date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -194,7 +168,6 @@ def test_invalid_duplicate_object(self):
"signing_institution": "Test insitution",
"version": self.agreement_version,
"date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -213,7 +186,6 @@ def test_invalid_version(self):
"signing_institution": "Test insitution",
"version": self.agreement_version,
"date_signed": "2023-01-01",
- "is_primary": True,
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -281,6 +253,7 @@ def test_valid(self):
"""Form is valid with necessary input."""
form_data = {
"signed_agreement": self.signed_agreement,
+ "is_primary": True,
"study_site": self.study_site,
}
form = self.form_class(data=form_data)
@@ -290,6 +263,7 @@ def test_missing_signed_agreement(self):
"""Form is invalid when missing signed_agreement."""
form_data = {
# "signed_agreement": self.signed_agreement,
+ "is_primary": True,
"study_site": self.study_site,
}
form = self.form_class(data=form_data)
@@ -299,10 +273,25 @@ def test_missing_signed_agreement(self):
self.assertEqual(len(form.errors["signed_agreement"]), 1)
self.assertIn("required", form.errors["signed_agreement"][0])
+ def test_missing_is_primary(self):
+ """Form is invalid when missing study_site."""
+ form_data = {
+ "signed_agreement": self.signed_agreement,
+ # "is_primary": True,
+ "study_site": self.study_site,
+ }
+ form = self.form_class(data=form_data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertIn("is_primary", form.errors)
+ self.assertEqual(len(form.errors["is_primary"]), 1)
+ self.assertIn("required", form.errors["is_primary"][0])
+
def test_missing_study_site(self):
"""Form is invalid when missing study_site."""
form_data = {
"signed_agreement": self.signed_agreement,
+ "is_primary": True,
# "study_site": self.study_site,
}
form = self.form_class(data=form_data)
@@ -317,6 +306,7 @@ def test_invalid_signed_agreement_already_has_member_agreement(self):
obj = factories.MemberAgreementFactory.create()
form_data = {
"signed_agreement": obj.signed_agreement,
+ "is_primary": True,
"study_site": self.study_site,
}
form = self.form_class(data=form_data)
@@ -332,6 +322,7 @@ def test_invalid_signed_agreement_wrong_type(self):
)
form_data = {
"signed_agreement": obj,
+ "is_primary": True,
"study_site": self.study_site,
}
form = self.form_class(data=form_data)
@@ -357,6 +348,7 @@ def test_valid(self):
"""Form is valid with necessary input."""
form_data = {
"signed_agreement": self.signed_agreement,
+ "is_primary": True,
"study": self.study,
}
form = self.form_class(data=form_data)
@@ -366,6 +358,7 @@ def test_missing_signed_agreement(self):
"""Form is invalid when missing signed_agreement."""
form_data = {
# "signed_agreement": self.signed_agreement,
+ "is_primary": True,
"study": self.study,
}
form = self.form_class(data=form_data)
@@ -375,10 +368,25 @@ def test_missing_signed_agreement(self):
self.assertEqual(len(form.errors["signed_agreement"]), 1)
self.assertIn("required", form.errors["signed_agreement"][0])
+ def test_missing_is_primary(self):
+ """Form is invalid when missing study_site."""
+ form_data = {
+ "signed_agreement": self.signed_agreement,
+ # "is_primary": True,
+ "study": self.study,
+ }
+ form = self.form_class(data=form_data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(len(form.errors), 1)
+ self.assertIn("is_primary", form.errors)
+ self.assertEqual(len(form.errors["is_primary"]), 1)
+ self.assertIn("required", form.errors["is_primary"][0])
+
def test_missing_study(self):
"""Form is invalid when missing study."""
form_data = {
"signed_agreement": self.signed_agreement,
+ "is_primary": True,
# "study": self.study,
}
form = self.form_class(data=form_data)
@@ -393,6 +401,7 @@ def test_invalid_signed_agreement_already_has_agreement_type(self):
obj = factories.DataAffiliateAgreementFactory.create()
form_data = {
"signed_agreement": obj.signed_agreement,
+ "is_primary": True,
"study": self.study,
}
form = self.form_class(data=form_data)
@@ -408,6 +417,7 @@ def test_invalid_signed_agreement_wrong_type(self):
)
form_data = {
"signed_agreement": obj,
+ "is_primary": True,
"study": self.study,
}
form = self.form_class(data=form_data)
@@ -416,6 +426,60 @@ def test_invalid_signed_agreement_wrong_type(self):
self.assertEqual(len(form.errors["signed_agreement"]), 1)
self.assertIn("expected type", form.errors["signed_agreement"][0])
+ def test_valid_primary_with_additional_limitations(self):
+ """Form is valid with necessary input."""
+ form_data = {
+ "signed_agreement": self.signed_agreement,
+ "study": self.study,
+ "is_primary": True,
+ "additional_limitations": "test limitations",
+ }
+ form = self.form_class(data=form_data)
+ self.assertTrue(form.is_valid())
+
+ def test_invalid_component_with_additional_limitations(self):
+ """Form is valid with necessary input."""
+ form_data = {
+ "signed_agreement": self.signed_agreement,
+ "study": self.study,
+ "is_primary": False,
+ "additional_limitations": "test limitations",
+ }
+ form = self.form_class(data=form_data)
+ self.assertFalse(form.is_valid())
+ self.assertIn("additional_limitations", form.errors)
+ self.assertEqual(len(form.errors["additional_limitations"]), 1)
+ self.assertIn(
+ "only allowed for primary", form.errors["additional_limitations"][0]
+ )
+
+ def test_valid_primary_with_requires_study_review_true(self):
+ """Form is valid with necessary input."""
+ form_data = {
+ "signed_agreement": self.signed_agreement,
+ "study": self.study,
+ "is_primary": True,
+ "requires_study_review": True,
+ }
+ form = self.form_class(data=form_data)
+ self.assertTrue(form.is_valid())
+
+ def test_invalid_component_with_requires_study_review_true(self):
+ """Form is valid with necessary input."""
+ form_data = {
+ "signed_agreement": self.signed_agreement,
+ "study": self.study,
+ "is_primary": False,
+ "requires_study_review": True,
+ }
+ form = self.form_class(data=form_data)
+ self.assertFalse(form.is_valid())
+ self.assertIn("requires_study_review", form.errors)
+ self.assertEqual(len(form.errors["requires_study_review"]), 1)
+ self.assertIn(
+ "can only be True for primary", form.errors["requires_study_review"][0]
+ )
+
class NonDataAffiliateAgreementFormTest(TestCase):
"""Tests for the NonDataAffiliateAgreementForm class."""
@@ -511,7 +575,6 @@ def test_valid(self):
"study": self.study,
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
@@ -526,7 +589,6 @@ def test_valid_with_one_data_use_modifier(self):
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
"data_use_modifier": DataUseModifier.objects.all(),
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
@@ -541,7 +603,6 @@ def test_valid_with_two_data_use_modifiers(self):
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
"data_use_modifier": DataUseModifier.objects.all(),
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
@@ -555,7 +616,6 @@ def test_invalid_missing_workspace(self):
"study": self.study,
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
@@ -573,7 +633,6 @@ def test_invalid_missing_study(self):
# "study": self.study,
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
@@ -591,7 +650,6 @@ def test_invalid_missing_requested_by(self):
"study": self.study,
# "requested_by": self.requester,
"data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
@@ -609,7 +667,6 @@ def test_invalid_missing_data_use_permission(self):
"study": self.study,
"requested_by": self.requester,
# "data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
@@ -620,23 +677,20 @@ def test_invalid_missing_data_use_permission(self):
self.assertEqual(len(form.errors["data_use_permission"]), 1)
self.assertIn("required", form.errors["data_use_permission"][0])
- def test_invalid_missing_data_use_limitations(self):
+ def test_valid_additional_limitations(self):
"""Form is invalid when missing data_use_limitations."""
form_data = {
"workspace": self.workspace,
"study": self.study,
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
- # "data_use_limitations": "test limitations",
+ "additional_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
form = self.form_class(data=form_data)
- self.assertFalse(form.is_valid())
- self.assertEqual(len(form.errors), 1)
- self.assertIn("data_use_limitations", form.errors)
- self.assertEqual(len(form.errors["data_use_limitations"]), 1)
- self.assertIn("required", form.errors["data_use_limitations"][0])
+ self.assertTrue(form.is_valid())
+ self.assertEqual(form.instance.additional_limitations, "test limitations")
def test_invalid_missing_acknowledgments(self):
"""Form is invalid when missing acknowledgments."""
@@ -645,7 +699,6 @@ def test_invalid_missing_acknowledgments(self):
"study": self.study,
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
# "acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
@@ -663,7 +716,6 @@ def test_invalid_missing_gsr_restricted(self):
"study": self.study,
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
# "gsr_restricted": False,
}
@@ -682,7 +734,6 @@ def test_invalid_duplicate_workspace(self):
"study": self.study,
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"gsr_restricted": False,
}
@@ -701,7 +752,6 @@ def test_valid_one_available_data(self):
"study": self.study,
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"available_data": [available_data],
"gsr_restricted": False,
@@ -717,7 +767,6 @@ def test_valid_two_available_data(self):
"study": self.study,
"requested_by": self.requester,
"data_use_permission": self.duo_permission,
- "data_use_limitations": "test limitations",
"acknowledgments": "test acknowledgmnts",
"available_data": available_data,
"gsr_restricted": False,
diff --git a/primed/cdsa/tests/test_migrations.py b/primed/cdsa/tests/test_migrations.py
index 9eb16740..d355392c 100644
--- a/primed/cdsa/tests/test_migrations.py
+++ b/primed/cdsa/tests/test_migrations.py
@@ -5,7 +5,8 @@
from django_test_migrations.contrib.unittest_case import MigratorTestCase
import factory
-from . import factories
+from primed.primed_anvil.tests.factories import StudySiteFactory
+
class AgreementMajorVersionMigrationsTest(MigratorTestCase):
"""Tests for the migrations associated with creating the new AgreementMajorVersion model."""
@@ -100,3 +101,225 @@ def test_agreement_version_major_version_correctly_populated(self):
agreement_version.full_clean()
self.assertEqual(agreement_version.major_version, major_version)
self.assertEqual(agreement_version.minor_version, 6)
+
+
+class PopulateIsPrimaryMigrationsForwardTest(MigratorTestCase):
+ """Tests for the migrations associated with creating the new AgreementMajorVersion model."""
+
+ migrate_from = ("cdsa", "0018_dataaffiliateagreement_requires_study_review_and_more")
+ migrate_to = ("cdsa", "0022_remove_signedagreement_is_primary")
+
+ def prepare(self):
+ """Prepare some data before the migration."""
+ # Get model definition for the old state.
+ User = self.old_state.apps.get_model("users", "User")
+ ManagedGroup = self.old_state.apps.get_model("anvil_consortium_manager", "ManagedGroup")
+ StudySite = self.old_state.apps.get_model("primed_anvil", "StudySite")
+ Study = self.old_state.apps.get_model("primed_anvil", "Study")
+ AgreementMajorVersion = self.old_state.apps.get_model("cdsa", "AgreementMajorVersion")
+ AgreementVersion = self.old_state.apps.get_model("cdsa", "AgreementVersion")
+ SignedAgreement = self.old_state.apps.get_model("cdsa", "SignedAgreement")
+ MemberAgreement = self.old_state.apps.get_model("cdsa", "MemberAgreement")
+ DataAffiliateAgreement = self.old_state.apps.get_model("cdsa", "DataAffiliateAgreement")
+ NonDataAffiliateAgreement = self.old_state.apps.get_model("cdsa", "NonDataAffiliateAgreement")
+ # Populate some signed agreements.
+ agreement_version = AgreementVersion.objects.create(
+ major_version=AgreementMajorVersion.objects.create(version=1, is_valid=True),
+ minor_version=0,
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=1,
+ representative=User.objects.create_user(username="test1", password="test1"),
+ representative_role="Test role 1",
+ signing_institution="Test institution 1",
+ type="member",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess1", email="testaccess1@example.com"),
+ is_primary=True
+ )
+ self.member_agreement_1 = MemberAgreement.objects.create(
+ signed_agreement=tmp,
+ study_site=StudySite.objects.create(short_name="test1", full_name="Test Study 1"),
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=2,
+ representative=User.objects.create_user(username="test2", password="test2"),
+ representative_role="Test role 2",
+ signing_institution="Test institution 2",
+ type="member",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess2", email="testaccess2@example.com"),
+ is_primary=False
+ )
+ self.member_agreement_2 = MemberAgreement.objects.create(
+ signed_agreement=tmp,
+ study_site=StudySite.objects.get(short_name="test1"),
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=3,
+ representative=User.objects.create_user(username="test3", password="test3"),
+ representative_role="Test role 3",
+ signing_institution="Test institution 3",
+ type="data_affiliate",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess3", email="testaccess3@example.com"),
+ is_primary=True
+ )
+ self.data_affiliate_agreement_1 = DataAffiliateAgreement.objects.create(
+ signed_agreement=tmp,
+ study=Study.objects.create(short_name="test2", full_name="Test Study Site 2"),
+ anvil_upload_group=ManagedGroup.objects.create(name="testupload1", email="testupload1@example.com"),
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=4,
+ representative=User.objects.create_user(username="test4", password="test4"),
+ representative_role="Test role 4",
+ signing_institution="Test institution 4",
+ type="data_affiliate",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess4", email="testaccess4@example.com"),
+ is_primary=False
+ )
+ self.data_affiliate_agreement_2 = DataAffiliateAgreement.objects.create(
+ signed_agreement=tmp,
+ study=Study.objects.get(short_name="test2"),
+ anvil_upload_group=ManagedGroup.objects.create(name="testupload2", email="testupload2@example.com"),
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=5,
+ representative=User.objects.create_user(username="test5", password="test5"),
+ representative_role="Test role 5",
+ signing_institution="Test institution 5",
+ type="non_data_affiliate",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess5", email="testaccess5@example.com"),
+ is_primary=False
+ )
+ self.non_data_affiliate_agreement = NonDataAffiliateAgreement.objects.create(
+ signed_agreement=tmp,
+ )
+
+ def test_is_primary_correctly_populated(self):
+# import ipdb; ipdb.set_trace()
+ MemberAgreement = self.new_state.apps.get_model("cdsa", "MemberAgreement")
+ DataAffiliateAgreement = self.new_state.apps.get_model("cdsa", "DataAffiliateAgreement")
+ NonDataAffiliateAgreement = self.new_state.apps.get_model("cdsa", "NonDataAffiliateAgreement")
+ instance = MemberAgreement.objects.get(pk=self.member_agreement_1.pk)
+ self.assertEqual(instance.is_primary, True)
+ instance = MemberAgreement.objects.get(pk=self.member_agreement_2.pk)
+ self.assertEqual(instance.is_primary, False)
+ instance = DataAffiliateAgreement.objects.get(pk=self.data_affiliate_agreement_1.pk)
+ self.assertEqual(instance.is_primary, True)
+ instance = DataAffiliateAgreement.objects.get(pk=self.data_affiliate_agreement_2.pk)
+ self.assertEqual(instance.is_primary, False)
+ instance = NonDataAffiliateAgreement.objects.get(pk=self.non_data_affiliate_agreement.pk)
+ self.assertFalse(hasattr(self.non_data_affiliate_agreement, "is_primary"))
+
+
+class PopulateIsPrimaryMigrationsBackwardTest(MigratorTestCase):
+ """Tests for the migrations associated with creating the new AgreementMajorVersion model."""
+
+ migrate_from = ("cdsa", "0022_remove_signedagreement_is_primary")
+ migrate_to = ("cdsa", "0018_dataaffiliateagreement_requires_study_review_and_more")
+
+ def prepare(self):
+ """Prepare some data before the migration."""
+ # Get model definition for the old state.
+ User = self.old_state.apps.get_model("users", "User")
+ ManagedGroup = self.old_state.apps.get_model("anvil_consortium_manager", "ManagedGroup")
+ StudySite = self.old_state.apps.get_model("primed_anvil", "StudySite")
+ Study = self.old_state.apps.get_model("primed_anvil", "Study")
+ AgreementMajorVersion = self.old_state.apps.get_model("cdsa", "AgreementMajorVersion")
+ AgreementVersion = self.old_state.apps.get_model("cdsa", "AgreementVersion")
+ SignedAgreement = self.old_state.apps.get_model("cdsa", "SignedAgreement")
+ MemberAgreement = self.old_state.apps.get_model("cdsa", "MemberAgreement")
+ DataAffiliateAgreement = self.old_state.apps.get_model("cdsa", "DataAffiliateAgreement")
+ NonDataAffiliateAgreement = self.old_state.apps.get_model("cdsa", "NonDataAffiliateAgreement")
+ # Populate some signed agreements.
+ agreement_version = AgreementVersion.objects.create(
+ major_version=AgreementMajorVersion.objects.create(version=1, is_valid=True),
+ minor_version=0,
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=1,
+ representative=User.objects.create_user(username="test1", password="test1"),
+ representative_role="Test role 1",
+ signing_institution="Test institution 1",
+ type="member",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess1", email="testaccess1@example.com"),
+ )
+ self.member_agreement_1 = MemberAgreement.objects.create(
+ signed_agreement=tmp,
+ study_site=StudySite.objects.create(short_name="test1", full_name="Test Study 1"),
+ is_primary=True,
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=2,
+ representative=User.objects.create_user(username="test2", password="test2"),
+ representative_role="Test role 2",
+ signing_institution="Test institution 2",
+ type="member",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess2", email="testaccess2@example.com"),
+ )
+ self.member_agreement_2 = MemberAgreement.objects.create(
+ signed_agreement=tmp,
+ study_site=StudySite.objects.get(short_name="test1"),
+ is_primary=False,
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=3,
+ representative=User.objects.create_user(username="test3", password="test3"),
+ representative_role="Test role 3",
+ signing_institution="Test institution 3",
+ type="data_affiliate",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess3", email="testaccess3@example.com"),
+ )
+ self.data_affiliate_agreement_1 = DataAffiliateAgreement.objects.create(
+ signed_agreement=tmp,
+ study=Study.objects.create(short_name="test2", full_name="Test Study Site 2"),
+ anvil_upload_group=ManagedGroup.objects.create(name="testupload1", email="testupload1@example.com"),
+ is_primary=True,
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=4,
+ representative=User.objects.create_user(username="test4", password="test4"),
+ representative_role="Test role 4",
+ signing_institution="Test institution 4",
+ type="data_affiliate",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess4", email="testaccess4@example.com"),
+ )
+ self.data_affiliate_agreement_2 = DataAffiliateAgreement.objects.create(
+ signed_agreement=tmp,
+ study=Study.objects.get(short_name="test2"),
+ anvil_upload_group=ManagedGroup.objects.create(name="testupload2", email="testupload2@example.com"),
+ is_primary=False,
+ )
+ tmp = SignedAgreement.objects.create(
+ cc_id=5,
+ representative=User.objects.create_user(username="test5", password="test5"),
+ representative_role="Test role 5",
+ signing_institution="Test institution 5",
+ type="non_data_affiliate",
+ version=agreement_version,
+ anvil_access_group=ManagedGroup.objects.create(name="testaccess5", email="testaccess5@example.com"),
+ )
+ self.non_data_affiliate_agreement = NonDataAffiliateAgreement.objects.create(
+ signed_agreement=tmp,
+ )
+
+ def test_is_primary_correctly_populated(self):
+ SignedAgreement = self.new_state.apps.get_model("cdsa", "SignedAgreement")
+ instance = SignedAgreement.objects.get(pk=self.member_agreement_1.signed_agreement.pk)
+ self.assertEqual(instance.is_primary, True)
+ instance = SignedAgreement.objects.get(pk=self.member_agreement_2.signed_agreement.pk)
+ self.assertEqual(instance.is_primary, False)
+ instance = SignedAgreement.objects.get(pk=self.data_affiliate_agreement_1.signed_agreement.pk)
+ self.assertEqual(instance.is_primary, True)
+ instance = SignedAgreement.objects.get(pk=self.data_affiliate_agreement_2.signed_agreement.pk)
+ self.assertEqual(instance.is_primary, False)
+ instance = SignedAgreement.objects.get(pk=self.non_data_affiliate_agreement.signed_agreement.pk)
+ self.assertTrue(instance.is_primary)
diff --git a/primed/cdsa/tests/test_models.py b/primed/cdsa/tests/test_models.py
index 0e41a4f5..eb9fbe66 100644
--- a/primed/cdsa/tests/test_models.py
+++ b/primed/cdsa/tests/test_models.py
@@ -8,7 +8,7 @@
ManagedGroupFactory,
WorkspaceFactory,
)
-from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
+from django.core.exceptions import ValidationError
from django.db.models import ProtectedError
from django.db.utils import IntegrityError
from django.test import TestCase, override_settings
@@ -179,7 +179,6 @@ def test_model_saving(self):
representative_role="foo",
signing_institution="bar",
type=models.SignedAgreement.MEMBER,
- is_primary=True,
version=agreement_version,
anvil_access_group=group,
)
@@ -318,22 +317,14 @@ def test_status_field(self):
def test_get_combined_type(self):
obj = factories.MemberAgreementFactory()
self.assertEqual(obj.signed_agreement.combined_type, "Member")
- obj = factories.MemberAgreementFactory(signed_agreement__is_primary=False)
+ obj = factories.MemberAgreementFactory(is_primary=False)
self.assertEqual(obj.signed_agreement.combined_type, "Member component")
obj = factories.DataAffiliateAgreementFactory()
self.assertEqual(obj.signed_agreement.combined_type, "Data affiliate")
- obj = factories.DataAffiliateAgreementFactory(
- signed_agreement__is_primary=False
- )
+ obj = factories.DataAffiliateAgreementFactory(is_primary=False)
self.assertEqual(obj.signed_agreement.combined_type, "Data affiliate component")
obj = factories.NonDataAffiliateAgreementFactory()
self.assertEqual(obj.signed_agreement.combined_type, "Non-data affiliate")
- obj = factories.NonDataAffiliateAgreementFactory(
- signed_agreement__is_primary=False
- )
- self.assertEqual(
- obj.signed_agreement.combined_type, "Non-data affiliate component"
- )
def test_get_agreement_type(self):
obj = factories.MemberAgreementFactory()
@@ -351,25 +342,6 @@ def test_get_agreement_group(self):
obj = factories.NonDataAffiliateAgreementFactory()
self.assertEqual(obj.signed_agreement.agreement_group, obj.affiliation)
- def test_clean_non_data_affiliate_is_primary_false(self):
- """ValidationError is raised when is_primary is False for a non-data affiliate."""
- user = UserFactory.create()
- group = ManagedGroupFactory.create()
- agreement_version = factories.AgreementVersionFactory.create()
- instance = factories.SignedAgreementFactory.build(
- representative=user,
- anvil_access_group=group,
- version=agreement_version,
- type=models.SignedAgreement.NON_DATA_AFFILIATE,
- is_primary=False,
- )
- 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.message_dict[NON_FIELD_ERRORS]), 1)
- self.assertIn("primary", e.exception.message_dict[NON_FIELD_ERRORS][0])
-
def test_is_in_cdsa_group(self):
"""is_in_cdsa_group works as expected."""
obj = factories.SignedAgreementFactory.create()
@@ -414,10 +386,18 @@ def test_model_saving(self):
instance = models.MemberAgreement(
signed_agreement=signed_agreement,
study_site=study_site,
+ is_primary=True,
)
instance.save()
self.assertIsInstance(instance, models.MemberAgreement)
+ def test_is_primary(self):
+ """Creation using the model constructor and .save() works."""
+ instance = factories.MemberAgreementFactory.create(is_primary=True)
+ self.assertEqual(instance.is_primary, True)
+ instance = factories.MemberAgreementFactory.create(is_primary=False)
+ self.assertEqual(instance.is_primary, False)
+
def test_clean_incorrect_type(self):
signed_agreement = factories.SignedAgreementFactory.create(
type=models.SignedAgreement.DATA_AFFILIATE
@@ -471,6 +451,20 @@ def test_get_agreement_group(self):
class DataAffiliateAgreementTest(TestCase):
"""Tests for the DataAffiliateAgreement model."""
+ def test_defaults(self):
+ upload_group = ManagedGroupFactory.create()
+ signed_agreement = factories.SignedAgreementFactory.create(
+ type=models.SignedAgreement.DATA_AFFILIATE
+ )
+ study = StudyFactory.create()
+ instance = models.DataAffiliateAgreement(
+ signed_agreement=signed_agreement,
+ study=study,
+ anvil_upload_group=upload_group,
+ )
+ self.assertFalse(instance.requires_study_review)
+ self.assertEqual(instance.additional_limitations, "")
+
def test_model_saving(self):
"""Creation using the model constructor and .save() works."""
upload_group = ManagedGroupFactory.create()
@@ -482,10 +476,18 @@ def test_model_saving(self):
signed_agreement=signed_agreement,
study=study,
anvil_upload_group=upload_group,
+ is_primary=True,
)
instance.save()
self.assertIsInstance(instance, models.DataAffiliateAgreement)
+ def test_is_primary(self):
+ """Creation using the model constructor and .save() works."""
+ instance = factories.DataAffiliateAgreementFactory.create(is_primary=True)
+ self.assertEqual(instance.is_primary, True)
+ instance = factories.DataAffiliateAgreementFactory.create(is_primary=False)
+ self.assertEqual(instance.is_primary, False)
+
def test_clean_incorrect_type(self):
signed_agreement = factories.SignedAgreementFactory.create(
type=models.SignedAgreement.MEMBER
@@ -506,6 +508,28 @@ def test_clean_incorrect_type(self):
e.exception.error_dict["signed_agreement"][0].messages[0],
)
+ def test_clean_additional_limitations_primary(self):
+ instance = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ additional_limitations="foo bar",
+ )
+ instance.full_clean()
+
+ def test_clean_additional_limitations_not_primary(self):
+ instance = factories.DataAffiliateAgreementFactory.create(
+ is_primary=False,
+ additional_limitations="foo bar",
+ )
+ with self.assertRaises(ValidationError) as e:
+ instance.clean()
+ self.assertEqual(len(e.exception.error_dict), 1)
+ self.assertIn("additional_limitations", e.exception.error_dict)
+ self.assertEqual(len(e.exception.error_dict["additional_limitations"]), 1)
+ self.assertIn(
+ "only allowed for primary agreements",
+ e.exception.error_dict["additional_limitations"][0].message,
+ )
+
def test_str_method(self):
"""The custom __str__ method returns the correct string."""
instance = factories.DataAffiliateAgreementFactory.create()
@@ -541,6 +565,29 @@ def test_get_agreement_group(self):
instance = factories.DataAffiliateAgreementFactory.create()
self.assertEqual(instance.get_agreement_group(), instance.study)
+ def test_requires_study_review_primary(self):
+ """Can set requires_study_review"""
+ instance = factories.DataAffiliateAgreementFactory.create(
+ requires_study_review=True
+ )
+ self.assertTrue(instance.requires_study_review)
+
+ def test_requires_study_review_not_primary(self):
+ """ValidationError when trying to set requires_study_review=True for components."""
+ instance = factories.DataAffiliateAgreementFactory.create(
+ is_primary=False,
+ requires_study_review=True,
+ )
+ with self.assertRaises(ValidationError) as e:
+ instance.clean()
+ self.assertEqual(len(e.exception.error_dict), 1)
+ self.assertIn("requires_study_review", e.exception.error_dict)
+ self.assertEqual(len(e.exception.error_dict["requires_study_review"]), 1)
+ self.assertIn(
+ "can only be True for primary",
+ e.exception.error_dict["requires_study_review"][0].message,
+ )
+
class NonDataAffiliateAgreementTest(TestCase):
"""Tests for the NonDataAffiliateAgreement model."""
@@ -616,7 +663,6 @@ def test_model_saving(self):
requester = UserFactory.create()
instance = models.CDSAWorkspace(
study=study,
- data_use_limitations="test limitations",
acknowledgments="test acknowledgments",
requested_by=requester,
workspace=workspace,
@@ -657,3 +703,64 @@ def test_available_data(self):
instance.available_data.add(*available_data)
self.assertIn(available_data[0], instance.available_data.all())
self.assertIn(available_data[1], instance.available_data.all())
+
+ def test_additional_limitations(self):
+ """Can have additional_limitations set."""
+ instance = factories.CDSAWorkspaceFactory.create(additional_limitations="foo")
+ self.assertEqual(instance.additional_limitations, "foo")
+
+ def test_get_primary_cdsa(self):
+ """get_primary_cdsa returns the primary valid CDSA for the study."""
+ instance = factories.CDSAWorkspaceFactory.create()
+ agreement = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE,
+ study=instance.study,
+ )
+ self.assertEqual(instance.get_primary_cdsa(), agreement)
+
+ def test_get_primary_cdsa_not_primary(self):
+ instance = factories.CDSAWorkspaceFactory.create()
+ factories.DataAffiliateAgreementFactory.create(
+ is_primary=False,
+ signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE,
+ study=instance.study,
+ )
+ with self.assertRaises(models.DataAffiliateAgreement.DoesNotExist):
+ instance.get_primary_cdsa()
+
+ def test_get_primary_cdsa_not_active(self):
+ instance = factories.CDSAWorkspaceFactory.create()
+ factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ signed_agreement__status=models.SignedAgreement.StatusChoices.LAPSED,
+ study=instance.study,
+ )
+ with self.assertRaises(models.DataAffiliateAgreement.DoesNotExist):
+ instance.get_primary_cdsa()
+
+ def test_get_primary_cdsa_different_study(self):
+ instance = factories.CDSAWorkspaceFactory.create()
+ factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE,
+ # study=instance.study,
+ )
+ with self.assertRaises(models.DataAffiliateAgreement.DoesNotExist):
+ instance.get_primary_cdsa()
+
+ def test_get_primary_cdsa_multiple_agreements(self):
+ """get_primary_cdsa returns the primary valid CDSA for the study."""
+ instance = factories.CDSAWorkspaceFactory.create()
+ factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE,
+ study=instance.study,
+ )
+ factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE,
+ study=instance.study,
+ )
+ with self.assertRaises(models.DataAffiliateAgreement.MultipleObjectsReturned):
+ instance.get_primary_cdsa()
diff --git a/primed/cdsa/tests/test_tables.py b/primed/cdsa/tests/test_tables.py
index c7c8c9de..83f6bf55 100644
--- a/primed/cdsa/tests/test_tables.py
+++ b/primed/cdsa/tests/test_tables.py
@@ -339,15 +339,11 @@ def test_row_count_with_two_agreements_multiple_members(self):
self.assertEqual(len(table.rows), 5)
def test_includes_components(self):
- agreement_1 = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ agreement_1 = factories.MemberAgreementFactory.create(is_primary=True)
GroupAccountMembershipFactory.create(
group__signedagreement=agreement_1.signed_agreement
)
- agreement_2 = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False
- )
+ agreement_2 = factories.MemberAgreementFactory.create(is_primary=False)
GroupAccountMembershipFactory.create(
group__signedagreement=agreement_2.signed_agreement
)
@@ -476,6 +472,32 @@ def test_ordering(self):
self.assertEqual(table.data[0], instance_2.workspace)
self.assertEqual(table.data[1], instance_1.workspace)
+ def test_render_requires_study_review(self):
+ table = self.table_class(self.model.objects.all())
+ # CDSA workspace with no data_affiliate_agreement.
+ cdsa_workspace = factories.CDSAWorkspaceFactory.create()
+ self.assertIn(
+ "question-circle-fill",
+ table.render_cdsaworkspace__requires_study_review(cdsa_workspace.workspace),
+ )
+ # With a primary - no review required.
+ agreement = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ requires_study_review=False,
+ study=cdsa_workspace.study,
+ )
+ self.assertEqual(
+ "",
+ table.render_cdsaworkspace__requires_study_review(cdsa_workspace.workspace),
+ )
+ # With a primary - review required.
+ agreement.requires_study_review = True
+ agreement.save()
+ self.assertIn(
+ "dash-circle-fill",
+ table.render_cdsaworkspace__requires_study_review(cdsa_workspace.workspace),
+ )
+
class CDSAWorkspaceUserTableTest(TestCase):
"""Tests for the CDSAWorkspaceUserTable class."""
@@ -505,3 +527,29 @@ def test_ordering(self):
table = self.table_class(self.model.objects.all())
self.assertEqual(table.data[0], instance_2.workspace)
self.assertEqual(table.data[1], instance_1.workspace)
+
+ def test_render_requires_study_review(self):
+ table = self.table_class(self.model.objects.all())
+ # CDSA workspace with no data_affiliate_agreement.
+ cdsa_workspace = factories.CDSAWorkspaceFactory.create()
+ self.assertIn(
+ "question-circle-fill",
+ table.render_cdsaworkspace__requires_study_review(cdsa_workspace.workspace),
+ )
+ # With a primary - no review required.
+ agreement = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ requires_study_review=False,
+ study=cdsa_workspace.study,
+ )
+ self.assertEqual(
+ "",
+ table.render_cdsaworkspace__requires_study_review(cdsa_workspace.workspace),
+ )
+ # With a primary - review required.
+ agreement.requires_study_review = True
+ agreement.save()
+ self.assertIn(
+ "dash-circle-fill",
+ table.render_cdsaworkspace__requires_study_review(cdsa_workspace.workspace),
+ )
diff --git a/primed/cdsa/tests/test_views.py b/primed/cdsa/tests/test_views.py
index 41a2caaf..ab203d0a 100644
--- a/primed/cdsa/tests/test_views.py
+++ b/primed/cdsa/tests/test_views.py
@@ -21,7 +21,7 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.messages import get_messages
-from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied
+from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.shortcuts import resolve_url
from django.test import RequestFactory, TestCase, override_settings
@@ -30,6 +30,8 @@
from freezegun import freeze_time
from primed.duo.tests.factories import DataUseModifierFactory, DataUsePermissionFactory
+from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable
+from primed.miscellaneous_workspaces.tests.factories import DataPrepWorkspaceFactory
from primed.primed_anvil.tests.factories import (
AvailableDataFactory,
StudyFactory,
@@ -1492,7 +1494,7 @@ def test_can_create_object(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1509,7 +1511,6 @@ def test_can_create_object(self):
self.assertEqual(new_agreement.representative_role, "Test role")
self.assertEqual(new_agreement.signing_institution, "Test institution")
self.assertEqual(new_agreement.date_signed, date.fromisoformat("2023-01-01"))
- self.assertEqual(new_agreement.is_primary, True)
# Type was set correctly.
self.assertEqual(new_agreement.type, new_agreement.MEMBER)
# AnVIL group was set correctly.
@@ -1525,6 +1526,7 @@ def test_can_create_object(self):
new_agreement_type = models.MemberAgreement.objects.latest("pk")
self.assertEqual(new_agreement.memberagreement, new_agreement_type)
self.assertEqual(new_agreement_type.study_site, study_site)
+ self.assertEqual(new_agreement_type.is_primary, True)
def test_redirect_url(self):
"""Redirects to successful url."""
@@ -1556,7 +1558,7 @@ def test_redirect_url(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1597,7 +1599,7 @@ def test_success_message(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1624,7 +1626,7 @@ def test_error_missing_cc_id(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1660,7 +1662,7 @@ def test_invalid_cc_id(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1694,7 +1696,7 @@ def test_error_missing_representative(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1728,7 +1730,7 @@ def test_error_invalid_representative(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1764,7 +1766,7 @@ def test_error_missing_representative_role(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1800,7 +1802,7 @@ def test_error_missing_signing_institution(self):
# "signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1835,7 +1837,7 @@ def test_error_missing_version(self):
"signing_institution": "Test institution",
# "version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1870,7 +1872,7 @@ def test_error_invalid_version(self):
"signing_institution": "Test institution",
"version": 999,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1906,7 +1908,7 @@ def test_error_missing_date_signed(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
# "date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1942,7 +1944,7 @@ def test_error_missing_is_primary(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- # "is_primary": True,
+ # "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1957,11 +1959,14 @@ def test_error_missing_is_primary(self):
# Form has errors in the correct field.
self.assertIn("form", response.context_data)
form = response.context_data["form"]
- self.assertFalse(form.is_valid())
- self.assertEqual(len(form.errors), 1)
- self.assertIn("is_primary", form.errors)
- self.assertEqual(len(form.errors["is_primary"]), 1)
- self.assertIn("required", form.errors["is_primary"][0])
+ self.assertTrue(form.is_valid())
+ formset = response.context_data["formset"]
+ self.assertFalse(formset.is_valid())
+ self.assertFalse(formset.forms[0].is_valid())
+ self.assertEqual(len(formset.forms[0].errors), 1)
+ self.assertIn("is_primary", formset.forms[0].errors)
+ self.assertEqual(len(formset.forms[0].errors["is_primary"]), 1)
+ self.assertIn("required", formset.forms[0].errors["is_primary"][0])
def test_error_missing_memberagreement_study_site(self):
"""Form shows an error when study_site is missing."""
@@ -1977,7 +1982,7 @@ def test_error_missing_memberagreement_study_site(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -1989,10 +1994,10 @@ def test_error_missing_memberagreement_study_site(self):
# No new objects were created.
self.assertEqual(models.SignedAgreement.objects.count(), 0)
self.assertEqual(models.MemberAgreement.objects.count(), 0)
- # Form has errors in the correct field.
self.assertIn("form", response.context_data)
form = response.context_data["form"]
self.assertTrue(form.is_valid())
+ # Formset has errors in the correct field.
formset = response.context_data["formset"]
self.assertFalse(formset.is_valid())
self.assertFalse(formset.forms[0].is_valid())
@@ -2015,7 +2020,7 @@ def test_error_invalid_memberagreement_study_site(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2053,7 +2058,7 @@ def test_error_duplicate_project_id(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2114,7 +2119,7 @@ def test_creates_anvil_access_group(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2165,7 +2170,7 @@ def test_creates_anvil_groups_different_setting_access_group_prefix(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2213,7 +2218,7 @@ def test_creates_anvil_groups_different_setting_cc_admins_group_name(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2251,7 +2256,7 @@ def test_manage_group_create_api_error(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2291,7 +2296,7 @@ def test_managed_group_already_exists_in_app(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2341,7 +2346,7 @@ def test_admin_group_membership_api_error(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2510,6 +2515,29 @@ def test_change_status_button_user_has_view_perm(self):
),
)
+ def test_response_is_primary(self):
+ """Response includes info about requires_study_review."""
+ instance = factories.MemberAgreementFactory.create(
+ is_primary=True,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(instance.signed_agreement.cc_id))
+ self.assertContains(response, "Primary?")
+ self.assertContains(
+ response,
+ """
Primary?Yes """, # noqa: E501
+ html=True,
+ )
+ instance.is_primary = False
+ instance.save()
+ response = self.client.get(self.get_url(instance.signed_agreement.cc_id))
+ self.assertContains(response, "Primary?")
+ self.assertContains(
+ response,
+ """Primary?No """, # noqa: E501
+ html=True,
+ )
+
class MemberAgreementListTest(TestCase):
"""Tests for the MemberAgreementList view."""
@@ -2711,7 +2739,7 @@ def test_can_create_object(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2728,7 +2756,6 @@ def test_can_create_object(self):
self.assertEqual(new_agreement.representative_role, "Test role")
self.assertEqual(new_agreement.signing_institution, "Test institution")
self.assertEqual(new_agreement.date_signed, date.fromisoformat("2023-01-01"))
- self.assertEqual(new_agreement.is_primary, True)
# Type was set correctly.
self.assertEqual(new_agreement.type, new_agreement.DATA_AFFILIATE)
# AnVIL group was set correctly.
@@ -2743,6 +2770,7 @@ def test_can_create_object(self):
self.assertEqual(models.DataAffiliateAgreement.objects.count(), 1)
new_agreement_type = models.DataAffiliateAgreement.objects.latest("pk")
self.assertEqual(new_agreement.dataaffiliateagreement, new_agreement_type)
+ self.assertEqual(new_agreement_type.is_primary, True)
self.assertEqual(new_agreement_type.study, study)
self.assertIsInstance(new_agreement_type.anvil_upload_group, ManagedGroup)
self.assertEqual(
@@ -2792,7 +2820,7 @@ def test_redirect_url(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2846,7 +2874,7 @@ def test_success_message(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2860,6 +2888,202 @@ def test_success_message(self):
views.DataAffiliateAgreementCreate.success_message, str(messages[0])
)
+ def test_can_create_primary_with_requires_study_review(self):
+ """Can create an object."""
+ self.client.force_login(self.user)
+ representative = UserFactory.create()
+ agreement_version = factories.AgreementVersionFactory.create()
+ study = StudyFactory.create()
+ # API response to create the associated anvil_access_group.
+ self.anvil_response_mock.add(
+ responses.POST,
+ self.api_client.sam_entry_point
+ + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234",
+ status=201,
+ json={"message": "mock message"},
+ )
+ self.anvil_response_mock.add(
+ responses.POST,
+ self.api_client.sam_entry_point
+ + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234",
+ status=201,
+ json={"message": "mock message"},
+ )
+ # CC admins group membership.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point
+ + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point
+ + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org",
+ status=204,
+ )
+ response = self.client.post(
+ self.get_url(),
+ {
+ "cc_id": 1234,
+ "representative": representative.pk,
+ "representative_role": "Test role",
+ "signing_institution": "Test institution",
+ "version": agreement_version.pk,
+ "date_signed": "2023-01-01",
+ "agreementtype-0-is_primary": True,
+ "agreementtype-TOTAL_FORMS": 1,
+ "agreementtype-INITIAL_FORMS": 0,
+ "agreementtype-MIN_NUM_FORMS": 1,
+ "agreementtype-MAX_NUM_FORMS": 1,
+ "agreementtype-0-study": study.pk,
+ "agreementtype-0-requires_study_review": True,
+ },
+ )
+ self.assertEqual(response.status_code, 302)
+ # Check the agreement type.
+ self.assertEqual(models.DataAffiliateAgreement.objects.count(), 1)
+ new_agreement_type = models.DataAffiliateAgreement.objects.latest("pk")
+ self.assertTrue(new_agreement_type.requires_study_review)
+
+ def test_cannot_create_component_with_requires_study_review(self):
+ """Cannot create a component agreement with requires_study_review=True."""
+ self.client.force_login(self.user)
+ representative = UserFactory.create()
+ agreement_version = factories.AgreementVersionFactory.create()
+ study = StudyFactory.create()
+ response = self.client.post(
+ self.get_url(),
+ {
+ "cc_id": 1234,
+ "representative": representative.pk,
+ "representative_role": "Test role",
+ "signing_institution": "Test institution",
+ "version": agreement_version.pk,
+ "date_signed": "2023-01-01",
+ "agreementtype-0-is_primary": False,
+ "agreementtype-TOTAL_FORMS": 1,
+ "agreementtype-INITIAL_FORMS": 0,
+ "agreementtype-MIN_NUM_FORMS": 1,
+ "agreementtype-MAX_NUM_FORMS": 1,
+ "agreementtype-0-study": study.pk,
+ "agreementtype-0-requires_study_review": True,
+ },
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("form", response.context)
+ form = response.context_data["form"]
+ self.assertTrue(form.is_valid())
+ # Formset has errors in the correct field.
+ formset = response.context_data["formset"]
+ self.assertFalse(formset.is_valid())
+ self.assertFalse(formset.forms[0].is_valid())
+ self.assertEqual(len(formset.forms[0].errors), 1)
+ self.assertIn("requires_study_review", formset.forms[0].errors)
+ self.assertEqual(len(formset.forms[0].errors["requires_study_review"]), 1)
+ self.assertIn(
+ "can only be True for primary",
+ formset.forms[0].errors["requires_study_review"][0],
+ )
+
+ def test_can_create_primary_with_additional_limitations(self):
+ """Can create an object."""
+ self.client.force_login(self.user)
+ representative = UserFactory.create()
+ agreement_version = factories.AgreementVersionFactory.create()
+ study = StudyFactory.create()
+ # API response to create the associated anvil_access_group.
+ self.anvil_response_mock.add(
+ responses.POST,
+ self.api_client.sam_entry_point
+ + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234",
+ status=201,
+ json={"message": "mock message"},
+ )
+ self.anvil_response_mock.add(
+ responses.POST,
+ self.api_client.sam_entry_point
+ + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234",
+ status=201,
+ json={"message": "mock message"},
+ )
+ # CC admins group membership.
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point
+ + "/api/groups/v1/TEST_PRIMED_CDSA_ACCESS_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org",
+ status=204,
+ )
+ self.anvil_response_mock.add(
+ responses.PUT,
+ self.api_client.sam_entry_point
+ + "/api/groups/v1/TEST_PRIMED_CDSA_UPLOAD_1234/admin/TEST_PRIMED_CC_ADMINS@firecloud.org",
+ status=204,
+ )
+ response = self.client.post(
+ self.get_url(),
+ {
+ "cc_id": 1234,
+ "representative": representative.pk,
+ "representative_role": "Test role",
+ "signing_institution": "Test institution",
+ "version": agreement_version.pk,
+ "date_signed": "2023-01-01",
+ "agreementtype-0-is_primary": True,
+ "agreementtype-TOTAL_FORMS": 1,
+ "agreementtype-INITIAL_FORMS": 0,
+ "agreementtype-MIN_NUM_FORMS": 1,
+ "agreementtype-MAX_NUM_FORMS": 1,
+ "agreementtype-0-study": study.pk,
+ "agreementtype-0-additional_limitations": "Test limitations",
+ },
+ )
+ self.assertEqual(response.status_code, 302)
+ # Check the agreement type.
+ self.assertEqual(models.DataAffiliateAgreement.objects.count(), 1)
+ new_agreement_type = models.DataAffiliateAgreement.objects.latest("pk")
+ self.assertEqual(new_agreement_type.additional_limitations, "Test limitations")
+
+ def test_cannot_create_component_with_additional_limitations(self):
+ """Cannot create a component agreement with additional_limitations."""
+ self.client.force_login(self.user)
+ representative = UserFactory.create()
+ agreement_version = factories.AgreementVersionFactory.create()
+ study = StudyFactory.create()
+ response = self.client.post(
+ self.get_url(),
+ {
+ "cc_id": 1234,
+ "representative": representative.pk,
+ "representative_role": "Test role",
+ "signing_institution": "Test institution",
+ "version": agreement_version.pk,
+ "date_signed": "2023-01-01",
+ "agreementtype-0-is_primary": False,
+ "agreementtype-TOTAL_FORMS": 1,
+ "agreementtype-INITIAL_FORMS": 0,
+ "agreementtype-MIN_NUM_FORMS": 1,
+ "agreementtype-MAX_NUM_FORMS": 1,
+ "agreementtype-0-study": study.pk,
+ "agreementtype-0-additional_limitations": "Test limitations",
+ },
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("form", response.context)
+ form = response.context_data["form"]
+ self.assertTrue(form.is_valid())
+ # Formset has errors in the correct field.
+ formset = response.context_data["formset"]
+ self.assertFalse(formset.is_valid())
+ self.assertFalse(formset.forms[0].is_valid())
+ self.assertEqual(len(formset.forms[0].errors), 1)
+ self.assertIn("additional_limitations", formset.forms[0].errors)
+ self.assertEqual(len(formset.forms[0].errors["additional_limitations"]), 1)
+ self.assertIn(
+ "only allowed for primary",
+ formset.forms[0].errors["additional_limitations"][0],
+ )
+
def test_error_missing_cc_id(self):
"""Form shows an error when cc_id is missing."""
self.client.force_login(self.user)
@@ -2875,7 +3099,7 @@ def test_error_missing_cc_id(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2911,7 +3135,7 @@ def test_invalid_cc_id(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2945,7 +3169,7 @@ def test_error_missing_representative(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -2979,7 +3203,7 @@ def test_error_invalid_representative(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3015,7 +3239,7 @@ def test_error_missing_representative_role(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3051,7 +3275,7 @@ def test_error_missing_signing_institution(self):
# "signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3086,7 +3310,7 @@ def test_error_missing_version(self):
"signing_institution": "Test institution",
# "version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3121,7 +3345,7 @@ def test_error_invalid_version(self):
"signing_institution": "Test institution",
"version": 9999,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3157,7 +3381,7 @@ def test_error_missing_date_signed(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
# "date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3193,7 +3417,7 @@ def test_error_missing_is_primary(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- # "is_primary": True,
+ # "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3208,11 +3432,14 @@ def test_error_missing_is_primary(self):
# Form has errors in the correct field.
self.assertIn("form", response.context_data)
form = response.context_data["form"]
- self.assertFalse(form.is_valid())
- self.assertEqual(len(form.errors), 1)
- self.assertIn("is_primary", form.errors)
- self.assertEqual(len(form.errors["is_primary"]), 1)
- self.assertIn("required", form.errors["is_primary"][0])
+ self.assertTrue(form.is_valid())
+ formset = response.context_data["formset"]
+ self.assertFalse(formset.is_valid())
+ self.assertFalse(formset.forms[0].is_valid())
+ self.assertEqual(len(formset.forms[0].errors), 1)
+ self.assertIn("is_primary", formset.forms[0].errors)
+ self.assertEqual(len(formset.forms[0].errors["is_primary"]), 1)
+ self.assertIn("required", formset.forms[0].errors["is_primary"][0])
def test_error_missing_study(self):
"""Form shows an error when study is missing."""
@@ -3228,7 +3455,7 @@ def test_error_missing_study(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3266,7 +3493,7 @@ def test_error_invalid_memberagreement_study_site(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3304,7 +3531,7 @@ def test_error_duplicate_project_id(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3377,7 +3604,7 @@ def test_creates_anvil_groups(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3452,7 +3679,7 @@ def test_creates_anvil_access_group_different_setting(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3519,7 +3746,7 @@ def test_creates_anvil_groups_different_setting_cc_admins_group_name(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3563,7 +3790,7 @@ def test_access_group_create_api_error(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3623,7 +3850,7 @@ def test_upload_group_create_api_error(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3663,7 +3890,7 @@ def test_access_group_already_exists_in_app(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3700,7 +3927,7 @@ def test_upload_group_already_exists_in_app(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3764,7 +3991,7 @@ def test_admin_group_membership_access_api_error(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -3830,7 +4057,7 @@ def test_admin_group_membership_upload_api_error(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
+ "agreementtype-0-is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4005,6 +4232,76 @@ def test_change_status_button_user_has_view_perm(self):
),
)
+ def test_response_includes_additional_limitations(self):
+ """Response includes a link to the study detail page."""
+ instance = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ additional_limitations="Test limitations for this data affiliate agreement",
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(instance.signed_agreement.cc_id))
+ self.assertContains(response, "Additional limitations")
+ self.assertContains(
+ response, "Test limitations for this data affiliate agreement"
+ )
+
+ def test_response_with_no_additional_limitations(self):
+ """Response includes a link to the study detail page."""
+ instance = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ additional_limitations="",
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(instance.signed_agreement.cc_id))
+ self.assertNotContains(response, "Additional limitations")
+
+ def test_response_is_primary(self):
+ """Response includes info about requires_study_review."""
+ instance = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(instance.signed_agreement.cc_id))
+ self.assertContains(response, "Primary?")
+ self.assertContains(
+ response,
+ """Primary?Yes """, # noqa: E501
+ html=True,
+ )
+ instance.is_primary = False
+ instance.save()
+ response = self.client.get(self.get_url(instance.signed_agreement.cc_id))
+ self.assertContains(response, "Primary?")
+ self.assertContains(
+ response,
+ """Primary?No """, # noqa: E501
+ html=True,
+ )
+
+ def test_response_requires_study_review(self):
+ """Response includes info about requires_study_review."""
+ instance = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True, requires_study_review=True
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url(instance.signed_agreement.cc_id))
+ self.assertContains(response, "Study review required?")
+ # import ipdb; ipdb.set_trace()
+ self.assertContains(
+ response,
+ """Study review required? Yes """, # noqa: E501
+ html=True,
+ )
+ instance.requires_study_review = False
+ instance.save()
+ response = self.client.get(self.get_url(instance.signed_agreement.cc_id))
+ self.assertContains(response, "Study review required?")
+ self.assertContains(
+ response,
+ """Study review required? No """, # noqa: E501
+ html=True,
+ )
+
class DataAffiliateAgreementListTest(TestCase):
"""Tests for the DataAffiliateAgreement view."""
@@ -4193,7 +4490,6 @@ def test_can_create_object(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4210,7 +4506,6 @@ def test_can_create_object(self):
self.assertEqual(new_agreement.representative_role, "Test role")
self.assertEqual(new_agreement.signing_institution, "Test institution")
self.assertEqual(new_agreement.date_signed, date.fromisoformat("2023-01-01"))
- self.assertEqual(new_agreement.is_primary, True)
# Type was set correctly.
self.assertEqual(new_agreement.type, new_agreement.NON_DATA_AFFILIATE)
# AnVIL group was set correctly.
@@ -4256,7 +4551,6 @@ def test_redirect_url(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4296,7 +4590,6 @@ def test_success_message(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4324,7 +4617,6 @@ def test_error_missing_cc_id(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4359,7 +4651,6 @@ def test_invalid_cc_id(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4392,7 +4683,6 @@ def test_error_missing_representative(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4425,7 +4715,6 @@ def test_error_invalid_representative(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4460,7 +4749,6 @@ def test_error_missing_representative_role(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4495,7 +4783,6 @@ def test_error_missing_signing_institution(self):
# "signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4529,7 +4816,6 @@ def test_error_missing_version(self):
"signing_institution": "Test institution",
# "version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4563,7 +4849,6 @@ def test_error_invalid_version(self):
"signing_institution": "Test institution",
"version": 999,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4598,7 +4883,6 @@ def test_error_missing_date_signed(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
# "date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4619,41 +4903,6 @@ def test_error_missing_date_signed(self):
self.assertEqual(len(form.errors["date_signed"]), 1)
self.assertIn("required", form.errors["date_signed"][0])
- def test_error_missing_is_primary(self):
- """Form shows an error when representative is missing."""
- self.client.force_login(self.user)
- representative = UserFactory.create()
- agreement_version = factories.AgreementVersionFactory.create()
- response = self.client.post(
- self.get_url(),
- {
- "cc_id": 1,
- "representative": representative.pk,
- "representative_role": "Test role",
- "signing_institution": "Test institution",
- "version": agreement_version.pk,
- "date_signed": "2023-01-01",
- # "is_primary": True,
- "agreementtype-TOTAL_FORMS": 1,
- "agreementtype-INITIAL_FORMS": 0,
- "agreementtype-MIN_NUM_FORMS": 1,
- "agreementtype-MAX_NUM_FORMS": 1,
- "agreementtype-0-affiliation": "Foo Bar",
- },
- )
- self.assertEqual(response.status_code, 200)
- # No new objects were created.
- self.assertEqual(models.SignedAgreement.objects.count(), 0)
- self.assertEqual(models.MemberAgreement.objects.count(), 0)
- # Form has errors in the correct field.
- self.assertIn("form", response.context_data)
- form = response.context_data["form"]
- self.assertFalse(form.is_valid())
- self.assertEqual(len(form.errors), 1)
- self.assertIn("is_primary", form.errors)
- self.assertEqual(len(form.errors["is_primary"]), 1)
- self.assertIn("required", form.errors["is_primary"][0])
-
def test_error_missing_nondataaffiliateagreement_affiliation(self):
"""Form shows an error when study_site is missing."""
self.client.force_login(self.user)
@@ -4668,7 +4917,6 @@ def test_error_missing_nondataaffiliateagreement_affiliation(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4707,7 +4955,6 @@ def test_error_duplicate_project_id(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4731,40 +4978,6 @@ def test_error_duplicate_project_id(self):
self.assertEqual(len(form.errors["cc_id"]), 1)
self.assertIn("already exists", form.errors["cc_id"][0])
- def test_error_is_primary_false(self):
- """Form shows an error when trying to create a duplicate dbgap_phs."""
- self.client.force_login(self.user)
- representative = UserFactory.create()
- agreement_version = factories.AgreementVersionFactory.create()
- response = self.client.post(
- self.get_url(),
- {
- "cc_id": 1,
- "representative": representative.pk,
- "representative_role": "Test role",
- "signing_institution": "Test institution",
- "version": agreement_version.pk,
- "date_signed": "2023-01-01",
- "is_primary": False,
- "agreementtype-TOTAL_FORMS": 1,
- "agreementtype-INITIAL_FORMS": 0,
- "agreementtype-MIN_NUM_FORMS": 1,
- "agreementtype-MAX_NUM_FORMS": 1,
- "agreementtype-0-affiliation": "Foo Bar",
- },
- )
- self.assertEqual(response.status_code, 200)
- # No new objects were created.
- self.assertEqual(models.SignedAgreement.objects.count(), 0)
- self.assertEqual(models.NonDataAffiliateAgreement.objects.count(), 0)
- # Form has errors in the correct field.
- form = response.context_data["form"]
- self.assertFalse(form.is_valid())
- self.assertEqual(len(form.errors), 1)
- self.assertIn(NON_FIELD_ERRORS, form.errors)
- self.assertEqual(len(form.errors[NON_FIELD_ERRORS]), 1)
- self.assertIn("primary", form.errors[NON_FIELD_ERRORS][0])
-
def test_post_blank_data(self):
"""Posting blank data does not create an object."""
self.client.force_login(self.user)
@@ -4801,7 +5014,6 @@ def test_creates_anvil_access_group(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4851,7 +5063,6 @@ def test_creates_anvil_groups_different_setting(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4898,7 +5109,6 @@ def test_creates_anvil_groups_different_setting_cc_admins_group_name(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4935,7 +5145,6 @@ def test_manage_group_create_api_error(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -4974,7 +5183,6 @@ def test_managed_group_already_exists_in_app(self):
"signing_institution": "Test institution",
"version": agreement_version.pk,
"date_signed": "2023-01-01",
- "is_primary": True,
"agreementtype-TOTAL_FORMS": 1,
"agreementtype-INITIAL_FORMS": 0,
"agreementtype-MIN_NUM_FORMS": 1,
@@ -5457,9 +5665,7 @@ def test_context_needs_action_table_grant(self):
def test_context_error_table_has_access(self):
"""error shows a record when audit finds that access needs to be removed."""
- member_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False
- )
+ member_agreement = factories.MemberAgreementFactory.create(is_primary=False)
GroupGroupMembershipFactory.create(
parent_group=self.anvil_cdsa_group,
child_group=member_agreement.signed_agreement.anvil_access_group,
@@ -6856,25 +7062,19 @@ def test_table_no_rows(self):
def test_table_three_rows(self):
"""Three rows are shown if there are three SignedAgreement objects."""
- factories.DataAffiliateAgreementFactory.create_batch(
- 3, signed_agreement__is_primary=True
- )
+ factories.DataAffiliateAgreementFactory.create_batch(3, is_primary=True)
self.client.force_login(self.user)
response = self.client.get(self.get_url())
self.assertIn("table", response.context_data)
self.assertEqual(len(response.context_data["table"].rows), 3)
def test_only_shows_data_affiliate_records(self):
- member_agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ member_agreement = factories.MemberAgreementFactory.create(is_primary=True)
data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=True
+ is_primary=True
)
non_data_affiliate_agreement = (
- factories.NonDataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ factories.NonDataAffiliateAgreementFactory.create()
)
self.client.force_login(self.user)
response = self.client.get(self.get_url())
@@ -6886,10 +7086,10 @@ def test_only_shows_data_affiliate_records(self):
def test_only_shows_primary_data_affiliate_records(self):
primary_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=True
+ is_primary=True
)
component_agreement = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False
+ is_primary=False
)
self.client.force_login(self.user)
response = self.client.get(self.get_url())
@@ -6963,7 +7163,7 @@ def test_table_no_rows(self):
def test_table_one_agreement_no_members(self):
"""No row is shown if there is one agreement with no account group members."""
- factories.MemberAgreementFactory.create(signed_agreement__is_primary=True)
+ factories.MemberAgreementFactory.create(is_primary=True)
self.client.force_login(self.user)
response = self.client.get(self.get_url())
self.assertIn("table", response.context_data)
@@ -6971,9 +7171,7 @@ def test_table_one_agreement_no_members(self):
def test_table_one_agreement_one_member(self):
"""One row is shown if there is one agreement and one account group member."""
- agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ agreement = factories.MemberAgreementFactory.create(is_primary=True)
GroupAccountMembershipFactory.create(
group=agreement.signed_agreement.anvil_access_group
)
@@ -6984,9 +7182,7 @@ def test_table_one_agreement_one_member(self):
def test_table_one_agreements_two_members(self):
"""Two rows are shown if there is one agreement with two account group members."""
- agreement = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ agreement = factories.MemberAgreementFactory.create(is_primary=True)
GroupAccountMembershipFactory.create_batch(
2, group=agreement.signed_agreement.anvil_access_group
)
@@ -6997,15 +7193,11 @@ def test_table_one_agreements_two_members(self):
def test_table_two_agreements(self):
"""Multiple rows is shown if there are two agreements and multiple account group members."""
- agreement_1 = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ agreement_1 = factories.MemberAgreementFactory.create(is_primary=True)
GroupAccountMembershipFactory.create_batch(
2, group=agreement_1.signed_agreement.anvil_access_group
)
- agreement_2 = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ agreement_2 = factories.MemberAgreementFactory.create(is_primary=True)
GroupAccountMembershipFactory.create_batch(
3, group=agreement_2.signed_agreement.anvil_access_group
)
@@ -7015,21 +7207,15 @@ def test_table_two_agreements(self):
self.assertEqual(len(response.context_data["table"].rows), 5)
def test_only_shows_records_for_all_agreement_types(self):
- agreement_1 = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ agreement_1 = factories.MemberAgreementFactory.create(is_primary=True)
GroupAccountMembershipFactory.create(
group=agreement_1.signed_agreement.anvil_access_group
)
- agreement_2 = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ agreement_2 = factories.DataAffiliateAgreementFactory.create(is_primary=True)
GroupAccountMembershipFactory.create(
group=agreement_2.signed_agreement.anvil_access_group
)
- agreement_3 = factories.NonDataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=True
- )
+ agreement_3 = factories.NonDataAffiliateAgreementFactory.create()
GroupAccountMembershipFactory.create(
group=agreement_3.signed_agreement.anvil_access_group
)
@@ -7039,28 +7225,18 @@ def test_only_shows_records_for_all_agreement_types(self):
self.assertEqual(len(table.rows), 3)
def test_shows_includes_component_agreements(self):
- agreement_1 = factories.MemberAgreementFactory.create(
- signed_agreement__is_primary=False
- )
+ agreement_1 = factories.MemberAgreementFactory.create(is_primary=False)
GroupAccountMembershipFactory.create(
group=agreement_1.signed_agreement.anvil_access_group
)
- agreement_2 = factories.DataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False
- )
+ agreement_2 = factories.DataAffiliateAgreementFactory.create(is_primary=False)
GroupAccountMembershipFactory.create(
group=agreement_2.signed_agreement.anvil_access_group
)
- agreement_3 = factories.NonDataAffiliateAgreementFactory.create(
- signed_agreement__is_primary=False
- )
- GroupAccountMembershipFactory.create(
- group=agreement_3.signed_agreement.anvil_access_group
- )
self.client.force_login(self.user)
response = self.client.get(self.get_url())
table = response.context_data["table"]
- self.assertEqual(len(table.rows), 3)
+ self.assertEqual(len(table.rows), 2)
def test_does_not_show_anvil_upload_group_members(self):
agreement = factories.DataAffiliateAgreementFactory.create()
@@ -7260,6 +7436,237 @@ def test_render_duo_modifiers(self):
self.assertContains(response, modifiers[0].abbreviation)
self.assertContains(response, modifiers[1].abbreviation)
+ def test_associated_data_prep_view_user(self):
+ """View users do not see the associated data prep section"""
+ user = User.objects.create_user(username="test-view", password="test-view")
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME
+ )
+ )
+
+ obj = factories.CDSAWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(target_workspace=obj.workspace)
+ self.client.force_login(user)
+ response = self.client.get(obj.get_absolute_url())
+ self.assertNotContains(response, "Associated data prep workspaces")
+
+ def test_associated_data_prep_staff_view_user(self):
+ """Staff view users do see the associated data prep section."""
+ obj = factories.CDSAWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(target_workspace=obj.workspace)
+ self.client.force_login(self.user)
+ response = self.client.get(obj.get_absolute_url())
+ self.assertContains(response, "Associated data prep workspaces")
+
+ def test_associated_data_prep_workspaces_context_exists(self):
+ obj = factories.CDSAWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(obj.get_absolute_url())
+ self.assertIn("associated_data_prep_workspaces", response.context_data)
+ self.assertIsInstance(
+ response.context_data["associated_data_prep_workspaces"],
+ DataPrepWorkspaceUserTable,
+ )
+
+ def test_only_show_one_associated_data_prep_workspace(self):
+ cdsa_obj = factories.CDSAWorkspaceFactory.create()
+ dataPrep_obj = DataPrepWorkspaceFactory.create(
+ target_workspace=cdsa_obj.workspace
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(cdsa_obj.get_absolute_url())
+ self.assertIn("associated_data_prep_workspaces", response.context_data)
+ self.assertEqual(
+ len(response.context_data["associated_data_prep_workspaces"].rows), 1
+ )
+ self.assertIn(
+ dataPrep_obj.workspace,
+ response.context_data["associated_data_prep_workspaces"].data,
+ )
+
+ def test_show_two_associated_data_prep_workspaces(self):
+ cdsa_obj = factories.CDSAWorkspaceFactory.create()
+ dataPrep_obj1 = DataPrepWorkspaceFactory.create(
+ target_workspace=cdsa_obj.workspace
+ )
+ dataPrep_obj2 = DataPrepWorkspaceFactory.create(
+ target_workspace=cdsa_obj.workspace
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(cdsa_obj.get_absolute_url())
+ self.assertIn("associated_data_prep_workspaces", response.context_data)
+ self.assertEqual(
+ len(response.context_data["associated_data_prep_workspaces"].rows), 2
+ )
+ self.assertIn(
+ dataPrep_obj1.workspace,
+ response.context_data["associated_data_prep_workspaces"].data,
+ )
+ self.assertIn(
+ dataPrep_obj2.workspace,
+ response.context_data["associated_data_prep_workspaces"].data,
+ )
+
+ def test_context_data_prep_active_with_no_prep_workspace(self):
+ instance = factories.CDSAWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertIn("data_prep_active", response.context_data)
+ self.assertFalse(response.context["data_prep_active"])
+
+ def test_context_data_prep_active_with_one_inactive_prep_workspace(self):
+ instance = factories.CDSAWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(
+ target_workspace=instance.workspace, is_active=False
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertIn("data_prep_active", response.context_data)
+ self.assertFalse(response.context["data_prep_active"])
+
+ def test_context_data_prep_active_with_one_active_prep_workspace(self):
+ instance = factories.CDSAWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(
+ target_workspace=instance.workspace, is_active=True
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertIn("data_prep_active", response.context_data)
+ self.assertTrue(response.context["data_prep_active"])
+
+ def test_context_data_prep_active_with_one_active_one_inactive_prep_workspace(self):
+ instance = factories.CDSAWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(
+ target_workspace=instance.workspace, is_active=True
+ )
+ DataPrepWorkspaceFactory.create(
+ target_workspace=instance.workspace, is_active=True
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertIn("data_prep_active", response.context_data)
+ self.assertTrue(response.context["data_prep_active"])
+
+ def test_response_context_primary_cdsa(self):
+ agreement = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ )
+ instance = factories.CDSAWorkspaceFactory.create(
+ study=agreement.study,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertIn("primary_cdsa", response.context)
+ self.assertEqual(response.context["primary_cdsa"], agreement)
+
+ def test_response_includes_additional_limitations(self):
+ """Response includes DataAffiliate additional limitations if they exist."""
+ agreement = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ additional_limitations="Test limitations for this data affiliate agreement",
+ )
+ instance = factories.CDSAWorkspaceFactory.create(
+ study=agreement.study,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertContains(
+ response, "Test limitations for this data affiliate agreement"
+ )
+
+ def test_response_data_use_limitations(self):
+ """All data use limitations appear in the response content."""
+ instance = factories.CDSAWorkspaceFactory.create(
+ data_use_permission__definition="Test permission.",
+ data_use_permission__abbreviation="P",
+ additional_limitations="Test additional limitations for workspace",
+ )
+ modifier_1 = DataUseModifierFactory.create(
+ abbreviation="M1", definition="Test modifier 1."
+ )
+ modifier_2 = DataUseModifierFactory.create(
+ abbreviation="M2", definition="Test modifier 2."
+ )
+ instance.data_use_modifiers.add(modifier_1, modifier_2)
+ # Create an agreement with data use limitations.
+ factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ study=instance.study,
+ additional_limitations="Test limitations for this data affiliate agreement",
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertContains(response, "P: Test permission.")
+ self.assertContains(response, "M1: Test modifier 1.")
+ self.assertContains(response, "M2: Test modifier 2.")
+ self.assertContains(
+ response,
+ "Additional limitations from CDSA",
+ )
+ self.assertContains(
+ response,
+ "Test limitations for this data affiliate agreement",
+ )
+ self.assertContains(
+ response,
+ "Additional limitations for this consent group",
+ )
+ self.assertContains(
+ response,
+ "Test additional limitations for workspace",
+ )
+
+ def test_response_requires_study_review_true(self):
+ """Response includes DataAffiliate info about study review required if true."""
+ agreement = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ requires_study_review=True,
+ )
+ instance = factories.CDSAWorkspaceFactory.create(
+ study=agreement.study,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertContains(response, "Study review required")
+
+ def test_response_requires_study_review_false(self):
+ """Response includes DataAffiliate info about study review required if true."""
+ agreement = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ requires_study_review=False,
+ )
+ instance = factories.CDSAWorkspaceFactory.create(
+ study=agreement.study,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertNotContains(response, "Study review required")
+
+ def test_response_primary_cdsa(self):
+ """Response includes note about missing primary cdsa about study review required if true."""
+ agreement = factories.DataAffiliateAgreementFactory.create(
+ is_primary=True,
+ )
+ instance = factories.CDSAWorkspaceFactory.create(
+ study=agreement.study,
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertContains(response, agreement.get_absolute_url())
+
+ def test_response_no_primary_cdsa(self):
+ """Response includes note about missing primary cdsa about study review required if true."""
+ instance = factories.CDSAWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertContains(
+ response,
+ # """Associated CDSAmdash;"""
+ """No primary CDSA"""
+ # """Associated CDSA —""", # noqa: E501
+ )
+
class CDSAWorkspaceCreateTest(AnVILAPIMockTestMixin, TestCase):
"""Tests of the WorkspaceCreate view from ACM with this app's CDSAWorkspace model."""
@@ -7321,7 +7728,6 @@ def test_creates_upload_workspace_without_duos(self):
"workspacedata-MAX_NUM_FORMS": 1,
"workspacedata-0-study": study.pk,
"workspacedata-0-data_use_permission": duo_permission.pk,
- "workspacedata-0-data_use_limitations": "test limitations",
"workspacedata-0-acknowledgments": "test acknowledgments",
"workspacedata-0-requested_by": self.requester.pk,
"workspacedata-0-gsr_restricted": False,
@@ -7336,7 +7742,6 @@ def test_creates_upload_workspace_without_duos(self):
self.assertEqual(new_workspace_data.workspace, new_workspace)
self.assertEqual(new_workspace_data.study, study)
self.assertEqual(new_workspace_data.data_use_permission, duo_permission)
- self.assertEqual(new_workspace_data.data_use_limitations, "test limitations")
self.assertEqual(new_workspace_data.acknowledgments, "test acknowledgments")
self.assertEqual(new_workspace_data.requested_by, self.requester)
@@ -7373,7 +7778,6 @@ def test_creates_upload_workspace_with_duo_modifiers(self):
"workspacedata-MIN_NUM_FORMS": 1,
"workspacedata-MAX_NUM_FORMS": 1,
"workspacedata-0-study": study.pk,
- "workspacedata-0-data_use_limitations": "test limitations",
"workspacedata-0-acknowledgments": "test acknowledgments",
"workspacedata-0-data_use_permission": data_use_permission.pk,
"workspacedata-0-data_use_modifiers": [
@@ -7422,7 +7826,6 @@ def test_creates_upload_workspace_with_disease_term(self):
"workspacedata-MIN_NUM_FORMS": 1,
"workspacedata-MAX_NUM_FORMS": 1,
"workspacedata-0-study": study.pk,
- "workspacedata-0-data_use_limitations": "test limitations",
"workspacedata-0-acknowledgments": "test acknowledgments",
"workspacedata-0-data_use_permission": data_use_permission.pk,
"workspacedata-0-disease_term": "foo",
diff --git a/primed/collaborative_analysis/tables.py b/primed/collaborative_analysis/tables.py
index a17c5cb1..dfaeddb7 100644
--- a/primed/collaborative_analysis/tables.py
+++ b/primed/collaborative_analysis/tables.py
@@ -2,12 +2,10 @@
from anvil_consortium_manager.models import Workspace
-class CollaborativeAnalysisWorkspaceStaffTable(tables.Table):
+class CollaborativeAnalysisWorkspaceUserTable(tables.Table):
"""Class to render a table of Workspace objects with CollaborativeAnalysisWorkspace data."""
name = tables.columns.Column(linkify=True)
- billing_project = tables.Column(linkify=True)
- collaborativeanalysisworkspace__custodian = tables.Column(linkify=True)
number_source_workspaces = tables.columns.Column(
accessor="pk",
verbose_name="Number of source workspaces",
@@ -18,7 +16,6 @@ class Meta:
model = Workspace
fields = (
"name",
- "billing_project",
"collaborativeanalysisworkspace__custodian",
"number_source_workspaces",
)
@@ -29,16 +26,12 @@ def render_number_source_workspaces(self, record):
return record.collaborativeanalysisworkspace.source_workspaces.count()
-class CollaborativeAnalysisWorkspaceUserTable(tables.Table):
+class CollaborativeAnalysisWorkspaceStaffTable(CollaborativeAnalysisWorkspaceUserTable):
"""Class to render a table of Workspace objects with CollaborativeAnalysisWorkspace data."""
name = tables.columns.Column(linkify=True)
- billing_project = tables.Column()
- number_source_workspaces = tables.columns.Column(
- accessor="pk",
- verbose_name="Number of source workspaces",
- orderable=False,
- )
+ billing_project = tables.Column(linkify=True)
+ collaborativeanalysisworkspace__custodian = tables.Column(linkify=True)
class Meta:
model = Workspace
@@ -49,7 +42,3 @@ class Meta:
"number_source_workspaces",
)
order_by = ("name",)
-
- def render_number_source_workspaces(self, record):
- """Render the number of source workspaces."""
- return record.collaborativeanalysisworkspace.source_workspaces.count()
diff --git a/primed/dbgap/adapters.py b/primed/dbgap/adapters.py
index 5c567a5b..a2ed57a9 100644
--- a/primed/dbgap/adapters.py
+++ b/primed/dbgap/adapters.py
@@ -1,5 +1,8 @@
from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter
from anvil_consortium_manager.forms import WorkspaceForm
+from anvil_consortium_manager.models import Workspace
+
+from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable
from . import forms, models, tables
@@ -16,3 +19,16 @@ class dbGaPWorkspaceAdapter(BaseWorkspaceAdapter):
workspace_data_model = models.dbGaPWorkspace
workspace_data_form_class = forms.dbGaPWorkspaceForm
workspace_detail_template_name = "dbgap/dbgapworkspace_detail.html"
+
+ def get_extra_detail_context_data(self, workspace, request):
+ extra_context = {}
+ associated_data_prep = Workspace.objects.filter(
+ dataprepworkspace__target_workspace=workspace
+ )
+ extra_context["associated_data_prep_workspaces"] = DataPrepWorkspaceUserTable(
+ associated_data_prep
+ )
+ extra_context["data_prep_active"] = associated_data_prep.filter(
+ dataprepworkspace__is_active=True
+ ).exists()
+ return extra_context
diff --git a/primed/dbgap/tables.py b/primed/dbgap/tables.py
index 08443706..fa22c5b1 100644
--- a/primed/dbgap/tables.py
+++ b/primed/dbgap/tables.py
@@ -74,11 +74,10 @@ def render_dbgap_phs(self, value):
return "phs{0:06d}".format(value)
-class dbGaPWorkspaceStaffTable(tables.Table):
+class dbGaPWorkspaceUserTable(tables.Table):
"""Class to render a table of Workspace objects with dbGaPWorkspace workspace data."""
name = tables.columns.Column(linkify=True)
- billing_project = tables.Column(linkify=True)
dbgap_accession = dbGaPAccessionColumn(
accessor="dbgapworkspace__get_dbgap_accession",
dbgap_link_accessor="dbgapworkspace__get_dbgap_link",
@@ -91,11 +90,6 @@ class dbGaPWorkspaceStaffTable(tables.Table):
dbgapworkspace__dbgap_consent_abbreviation = tables.columns.Column(
verbose_name="Consent"
)
- number_approved_dars = tables.columns.Column(
- accessor="pk",
- verbose_name="Approved DARs",
- orderable=False,
- )
dbgapworkspace__gsr_restricted = BooleanIconColumn(
orderable=False, true_icon="dash-circle-fill", true_color="#ffc107"
)
@@ -105,43 +99,23 @@ class Meta:
model = Workspace
fields = (
"name",
- "billing_project",
"dbgap_accession",
"dbgapworkspace__dbgap_consent_abbreviation",
- "number_approved_dars",
"dbgapworkspace__gsr_restricted",
"is_shared",
)
order_by = ("name",)
- def render_number_approved_dars(self, record):
- n = (
- record.dbgapworkspace.get_data_access_requests(most_recent=True)
- .filter(dbgap_current_status=models.dbGaPDataAccessRequest.APPROVED)
- .count()
- )
- return n
-
-class dbGaPWorkspaceUserTable(tables.Table):
+class dbGaPWorkspaceStaffTable(dbGaPWorkspaceUserTable):
"""Class to render a table of Workspace objects with dbGaPWorkspace workspace data."""
- name = tables.columns.Column(linkify=True)
- billing_project = tables.Column()
- dbgap_accession = dbGaPAccessionColumn(
- accessor="dbgapworkspace__get_dbgap_accession",
- dbgap_link_accessor="dbgapworkspace__get_dbgap_link",
- order_by=(
- "dbgapworkspace__dbgap_study_accession__dbgap_phs",
- "dbgapworkspace__dbgap_version",
- "dbgapworkspace__dbgap_participant_set",
- ),
- )
- dbgapworkspace__dbgap_consent_abbreviation = tables.columns.Column(
- verbose_name="Consent"
+ billing_project = tables.Column(linkify=True)
+ number_approved_dars = tables.columns.Column(
+ accessor="pk",
+ verbose_name="Approved DARs",
+ orderable=False,
)
- dbgapworkspace__gsr_restricted = BooleanIconColumn(orderable=False)
- is_shared = WorkspaceSharedWithConsortiumColumn()
class Meta:
model = Workspace
@@ -150,11 +124,20 @@ class Meta:
"billing_project",
"dbgap_accession",
"dbgapworkspace__dbgap_consent_abbreviation",
+ "number_approved_dars",
"dbgapworkspace__gsr_restricted",
"is_shared",
)
order_by = ("name",)
+ def render_number_approved_dars(self, record):
+ n = (
+ record.dbgapworkspace.get_data_access_requests(most_recent=True)
+ .filter(dbgap_current_status=models.dbGaPDataAccessRequest.APPROVED)
+ .count()
+ )
+ return n
+
class dbGaPApplicationTable(tables.Table):
"""Class to render a table of dbGaPApplication objects."""
diff --git a/primed/dbgap/tests/test_views.py b/primed/dbgap/tests/test_views.py
index e345cbaf..a2ebf614 100644
--- a/primed/dbgap/tests/test_views.py
+++ b/primed/dbgap/tests/test_views.py
@@ -32,6 +32,8 @@
from freezegun import freeze_time
from primed.duo.tests.factories import DataUseModifierFactory, DataUsePermissionFactory
+from primed.miscellaneous_workspaces.tables import DataPrepWorkspaceUserTable
+from primed.miscellaneous_workspaces.tests.factories import DataPrepWorkspaceFactory
from primed.primed_anvil.tests.factories import ( # DataUseModifierFactory,; DataUsePermissionFactory,
StudyFactory,
)
@@ -951,6 +953,118 @@ def test_links_audit_access_view_permission(self):
),
)
+ def test_associated_data_prep_view_user(self):
+ """View users do not see the associated data prep section"""
+ user = User.objects.create_user(username="test-view", password="test-view")
+ user.user_permissions.add(
+ Permission.objects.get(
+ codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME
+ )
+ )
+
+ obj = factories.dbGaPWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(target_workspace=obj.workspace)
+ self.client.force_login(user)
+ response = self.client.get(obj.get_absolute_url())
+ self.assertNotContains(response, "Associated data prep workspaces")
+
+ def test_associated_data_prep_staff_view_user(self):
+ """Staff view users do see the associated data prep section."""
+ obj = factories.dbGaPWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(target_workspace=obj.workspace)
+ self.client.force_login(self.user)
+ response = self.client.get(obj.get_absolute_url())
+ self.assertContains(response, "Associated data prep workspaces")
+
+ def test_associated_data_prep_workspaces_context_exists(self):
+ obj = factories.dbGaPWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(obj.get_absolute_url())
+ self.assertIn("associated_data_prep_workspaces", response.context_data)
+ self.assertIsInstance(
+ response.context_data["associated_data_prep_workspaces"],
+ DataPrepWorkspaceUserTable,
+ )
+
+ def test_only_show_one_associated_data_prep_workspace(self):
+ dbGaP_obj = factories.dbGaPWorkspaceFactory.create()
+ dataPrep_obj = DataPrepWorkspaceFactory.create(
+ target_workspace=dbGaP_obj.workspace
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(dbGaP_obj.get_absolute_url())
+ self.assertIn("associated_data_prep_workspaces", response.context_data)
+ self.assertEqual(
+ len(response.context_data["associated_data_prep_workspaces"].rows), 1
+ )
+ self.assertIn(
+ dataPrep_obj.workspace,
+ response.context_data["associated_data_prep_workspaces"].data,
+ )
+
+ def test_show_two_associated_data_prep_workspaces(self):
+ dbGaP_obj = factories.dbGaPWorkspaceFactory.create()
+ dataPrep_obj1 = DataPrepWorkspaceFactory.create(
+ target_workspace=dbGaP_obj.workspace
+ )
+ dataPrep_obj2 = DataPrepWorkspaceFactory.create(
+ target_workspace=dbGaP_obj.workspace
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(dbGaP_obj.get_absolute_url())
+ self.assertIn("associated_data_prep_workspaces", response.context_data)
+ self.assertEqual(
+ len(response.context_data["associated_data_prep_workspaces"].rows), 2
+ )
+ self.assertIn(
+ dataPrep_obj1.workspace,
+ response.context_data["associated_data_prep_workspaces"].data,
+ )
+ self.assertIn(
+ dataPrep_obj2.workspace,
+ response.context_data["associated_data_prep_workspaces"].data,
+ )
+
+ def test_context_data_prep_active_with_no_prep_workspace(self):
+ instance = factories.dbGaPWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertIn("data_prep_active", response.context_data)
+ self.assertFalse(response.context["data_prep_active"])
+
+ def test_context_data_prep_active_with_one_inactive_prep_workspace(self):
+ instance = factories.dbGaPWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(
+ target_workspace=instance.workspace, is_active=False
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertIn("data_prep_active", response.context_data)
+ self.assertFalse(response.context["data_prep_active"])
+
+ def test_context_data_prep_active_with_one_active_prep_workspace(self):
+ instance = factories.dbGaPWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(
+ target_workspace=instance.workspace, is_active=True
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertIn("data_prep_active", response.context_data)
+ self.assertTrue(response.context["data_prep_active"])
+
+ def test_context_data_prep_active_with_one_active_one_inactive_prep_workspace(self):
+ instance = factories.dbGaPWorkspaceFactory.create()
+ DataPrepWorkspaceFactory.create(
+ target_workspace=instance.workspace, is_active=True
+ )
+ DataPrepWorkspaceFactory.create(
+ target_workspace=instance.workspace, is_active=True
+ )
+ self.client.force_login(self.user)
+ response = self.client.get(instance.get_absolute_url())
+ self.assertIn("data_prep_active", response.context_data)
+ self.assertTrue(response.context["data_prep_active"])
+
class dbGaPWorkspaceCreateTest(AnVILAPIMockTestMixin, TestCase):
"""Tests of the WorkspaceCreate view from ACM with this app's dbGaPWorkspace model."""
diff --git a/primed/duo/models.py b/primed/duo/models.py
index 8a79e9b8..7c6efb25 100644
--- a/primed/duo/models.py
+++ b/primed/duo/models.py
@@ -1,3 +1,5 @@
+import re
+
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
@@ -41,6 +43,12 @@ def get_ols_url(self):
self.identifier.replace("DUO:", "DUO_")
)
+ def get_short_definition(self):
+ text = re.sub(r"This .+? indicates that ", "", self.definition)
+ # Only capitalize the first letter - keep the remaining text as is.
+ text = text[0].capitalize() + text[1:]
+ return text
+
class DataUsePermission(DUOFields, TreeNode):
"""A model to track the allowed main consent codes using GA4GH DUO codes."""
diff --git a/primed/duo/tests/test_models.py b/primed/duo/tests/test_models.py
index f216087d..baeb1416 100644
--- a/primed/duo/tests/test_models.py
+++ b/primed/duo/tests/test_models.py
@@ -87,6 +87,24 @@ def test_unique_identifier(self):
with self.assertRaises(IntegrityError):
instance2.save()
+ def test_get_short_definition(self):
+ instance = factories.DataUsePermissionFactory.create(
+ definition="Test definition"
+ )
+ self.assertEqual(instance.get_short_definition(), "Test definition")
+
+ def test_get_short_definition_re_sub(self):
+ instance = factories.DataUsePermissionFactory.create(
+ definition="This XXX indicates that everything is fine."
+ )
+ self.assertEqual(instance.get_short_definition(), "Everything is fine.")
+
+ def test_get_short_definition_capitalization(self):
+ instance = factories.DataUsePermissionFactory.create(
+ definition="Test definition XyXy"
+ )
+ self.assertEqual(instance.get_short_definition(), "Test definition XyXy")
+
class DataUseModifierTest(TestCase):
"""Tests for the DataUseModifier model."""
@@ -157,6 +175,16 @@ def test_unique_identifier(self):
with self.assertRaises(IntegrityError):
instance2.save()
+ def test_get_short_definition(self):
+ instance = factories.DataUseModifierFactory.create(definition="Test definition")
+ self.assertEqual(instance.get_short_definition(), "Test definition")
+
+ def test_get_short_definition_re_sub(self):
+ instance = factories.DataUseModifierFactory.create(
+ definition="This XXX indicates that use is allowed."
+ )
+ self.assertEqual(instance.get_short_definition(), "Use is allowed.")
+
class DataUseOntologyTestCase(TestCase):
"""Tests for the DataUseOntology abstract model."""
diff --git a/primed/duo/tests/test_views.py b/primed/duo/tests/test_views.py
index 96f2a5af..e22ca408 100644
--- a/primed/duo/tests/test_views.py
+++ b/primed/duo/tests/test_views.py
@@ -23,7 +23,7 @@ def setUp(self):
self.user = UserFactory.create(username="test", password="test")
self.user.user_permissions.add(
Permission.objects.get(
- codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME
+ codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME
)
)
@@ -180,7 +180,7 @@ def setUp(self):
self.user = UserFactory.create(username="test", password="test")
self.user.user_permissions.add(
Permission.objects.get(
- codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME
+ codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME
)
)
diff --git a/primed/duo/views.py b/primed/duo/views.py
index f27c1e25..308cc3e1 100644
--- a/primed/duo/views.py
+++ b/primed/duo/views.py
@@ -1,14 +1,11 @@
-from anvil_consortium_manager.auth import (
- AnVILConsortiumManagerStaffViewRequired,
- AnVILConsortiumManagerViewRequired,
-)
+from anvil_consortium_manager.auth import AnVILConsortiumManagerViewRequired
from django.http import Http404
from django.views.generic import DetailView, ListView
from . import models
-class DataUsePermissionList(AnVILConsortiumManagerStaffViewRequired, ListView):
+class DataUsePermissionList(AnVILConsortiumManagerViewRequired, ListView):
model = models.DataUsePermission
@@ -41,7 +38,7 @@ def get_context_data(self, **kwargs):
return context
-class DataUseModifierList(AnVILConsortiumManagerStaffViewRequired, ListView):
+class DataUseModifierList(AnVILConsortiumManagerViewRequired, ListView):
model = models.DataUseModifier
diff --git a/primed/miscellaneous_workspaces/adapters.py b/primed/miscellaneous_workspaces/adapters.py
index 24262017..17bdfb9b 100644
--- a/primed/miscellaneous_workspaces/adapters.py
+++ b/primed/miscellaneous_workspaces/adapters.py
@@ -93,8 +93,8 @@ class DataPrepWorkspaceAdapter(BaseWorkspaceAdapter):
type = "data_prep"
name = "Data prep workspace"
description = "Workspaces used to prepare data for sharing or update data that is already shared"
- list_table_class_staff_view = tables.DataPrepWorkspaceTable
- list_table_class_view = tables.DataPrepWorkspaceTable
+ list_table_class_staff_view = tables.DataPrepWorkspaceStaffTable
+ list_table_class_view = tables.DataPrepWorkspaceUserTable
workspace_form_class = WorkspaceForm
workspace_data_model = models.DataPrepWorkspace
workspace_data_form_class = forms.DataPrepWorkspaceForm
diff --git a/primed/miscellaneous_workspaces/forms.py b/primed/miscellaneous_workspaces/forms.py
index 6a0825ac..31eb2e46 100644
--- a/primed/miscellaneous_workspaces/forms.py
+++ b/primed/miscellaneous_workspaces/forms.py
@@ -1,6 +1,5 @@
"""Forms for the `workspaces` app."""
-from anvil_consortium_manager.adapters.workspace import workspace_adapter_registry
from anvil_consortium_manager.forms import Bootstrap5MediaFormMixin
from dal import autocomplete
from django import forms
@@ -62,23 +61,11 @@ class Meta:
class TemplateWorkspaceForm(forms.ModelForm):
"""Form for a TemplateWorkspace object."""
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Set the intended_workspace_type options, excluding "template".
- workspace_type_choices = [
- (key, value)
- for key, value in workspace_adapter_registry.get_registered_names().items()
- if key != "template"
- ]
- self.fields["intended_workspace_type"] = forms.ChoiceField(
- choices=[("", "---------")] + workspace_type_choices
- )
-
class Meta:
model = models.TemplateWorkspace
fields = (
"workspace",
- "intended_workspace_type",
+ "intended_usage",
)
diff --git a/primed/miscellaneous_workspaces/migrations/0011_alter_templateworkspace_remove_intended_workspace_type_add_intended_usage.py b/primed/miscellaneous_workspaces/migrations/0011_alter_templateworkspace_remove_intended_workspace_type_add_intended_usage.py
new file mode 100644
index 00000000..f632dc22
--- /dev/null
+++ b/primed/miscellaneous_workspaces/migrations/0011_alter_templateworkspace_remove_intended_workspace_type_add_intended_usage.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.2.11 on 2024-03-22 22:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("miscellaneous_workspaces", "0010_update_workspace_type_field"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="historicaltemplateworkspace",
+ name="intended_workspace_type",
+ ),
+ migrations.RemoveField(
+ model_name="templateworkspace",
+ name="intended_workspace_type",
+ ),
+ migrations.AddField(
+ model_name="historicaltemplateworkspace",
+ name="intended_usage",
+ field=models.TextField(default=""),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name="templateworkspace",
+ name="intended_usage",
+ field=models.TextField(default=""),
+ preserve_default=False,
+ ),
+ ]
diff --git a/primed/miscellaneous_workspaces/models.py b/primed/miscellaneous_workspaces/models.py
index 210bae3b..d702122d 100644
--- a/primed/miscellaneous_workspaces/models.py
+++ b/primed/miscellaneous_workspaces/models.py
@@ -1,6 +1,5 @@
"""Model definitions for the `miscellaneous_workspaces` app."""
-from anvil_consortium_manager.adapters.workspace import workspace_adapter_registry
from anvil_consortium_manager.models import BaseWorkspaceData, Workspace
from django.core.exceptions import ValidationError
from django.db import models
@@ -24,28 +23,7 @@ class ResourceWorkspace(RequesterModel, TimeStampedModel, BaseWorkspaceData):
class TemplateWorkspace(TimeStampedModel, BaseWorkspaceData):
"""A model to track template workspaces."""
- intended_workspace_type = models.CharField(max_length=63)
-
- def clean(self):
- """Custom cleaning checks.
-
- - Verify that intended_workspace_type is one of the registered types, excluding this type."""
- registered_workspace_types = workspace_adapter_registry.get_registered_names()
- if self.intended_workspace_type:
- if self.intended_workspace_type not in registered_workspace_types:
- raise ValidationError(
- {
- "intended_workspace_type": "intended_workspace_type must be one of the registered types."
- }
- )
- # We cannot import the adapter here because it would lead to a circular import, but we don't want
- # to create a template workspace for TemplateWorkspaces. So check if the type is not "template".
- elif self.intended_workspace_type == "template":
- raise ValidationError(
- {
- "intended_workspace_type": "intended_workspace_type may not be 'template'."
- }
- )
+ intended_usage = models.TextField()
class OpenAccessWorkspace(RequesterModel, TimeStampedModel, BaseWorkspaceData):
diff --git a/primed/miscellaneous_workspaces/tables.py b/primed/miscellaneous_workspaces/tables.py
index fd1132df..014f746c 100644
--- a/primed/miscellaneous_workspaces/tables.py
+++ b/primed/miscellaneous_workspaces/tables.py
@@ -9,30 +9,29 @@
)
-class OpenAccessWorkspaceStaffTable(tables.Table):
+class OpenAccessWorkspaceUserTable(tables.Table):
"""Class to render a table of Workspace objects with OpenAccessWorkspace workspace data."""
name = tables.columns.Column(linkify=True)
- billing_project = tables.Column(linkify=True)
is_shared = WorkspaceSharedWithConsortiumColumn()
+ openaccessworkspace__studies = tables.ManyToManyColumn(
+ linkify_item=True,
+ )
class Meta:
model = Workspace
fields = (
"name",
- "billing_project",
"openaccessworkspace__studies",
"is_shared",
)
order_by = ("name",)
-class OpenAccessWorkspaceUserTable(tables.Table):
+class OpenAccessWorkspaceStaffTable(OpenAccessWorkspaceUserTable):
"""Class to render a table of Workspace objects with OpenAccessWorkspace workspace data."""
- name = tables.columns.Column(linkify=True)
- billing_project = tables.Column()
- is_shared = WorkspaceSharedWithConsortiumColumn()
+ billing_project = tables.Column(linkify=True)
class Meta:
model = Workspace
@@ -45,11 +44,10 @@ class Meta:
order_by = ("name",)
-class DataPrepWorkspaceTable(tables.Table):
+class DataPrepWorkspaceUserTable(tables.Table):
"""Class to render a table of Workspace objects with DataPrepWorkspace workspace data."""
name = tables.columns.Column(linkify=True)
- # TODO: Figure out why this is not showing up
dataprepworkspace__target_workspace__name = tables.columns.Column(
linkify=True, verbose_name="Target workspace"
)
@@ -65,3 +63,19 @@ class Meta:
"dataprepworkspace__is_active",
)
order_by = ("name",)
+
+
+class DataPrepWorkspaceStaffTable(DataPrepWorkspaceUserTable):
+ """Class to render a table of Workspace objects with DataPrepWorkspace workspace data."""
+
+ billing_project = tables.columns.Column(linkify=True)
+
+ class Meta:
+ model = Workspace
+ fields = (
+ "name",
+ "billing_project",
+ "dataprepworkspace__target_workspace__name",
+ "dataprepworkspace__is_active",
+ )
+ order_by = ("name",)
diff --git a/primed/miscellaneous_workspaces/tests/factories.py b/primed/miscellaneous_workspaces/tests/factories.py
index c562c36a..a68d9e54 100644
--- a/primed/miscellaneous_workspaces/tests/factories.py
+++ b/primed/miscellaneous_workspaces/tests/factories.py
@@ -1,8 +1,5 @@
-import random
-
-from anvil_consortium_manager.adapters.workspace import workspace_adapter_registry
from anvil_consortium_manager.tests.factories import WorkspaceFactory
-from factory import Faker, SubFactory, lazy_attribute
+from factory import Faker, SubFactory
from factory.django import DjangoModelFactory
from primed.users.tests.factories import UserFactory
@@ -53,19 +50,11 @@ class TemplateWorkspaceFactory(DjangoModelFactory):
WorkspaceFactory,
workspace_type=adapters.TemplateWorkspaceAdapter().get_type(),
)
+ intended_usage = Faker("sentence")
class Meta:
model = models.TemplateWorkspace
- @lazy_attribute
- def intended_workspace_type(self):
- """Select a random registered workspace_type other than template."""
- registered_types = list(
- workspace_adapter_registry.get_registered_adapters().keys()
- )
- registered_types.remove(adapters.TemplateWorkspaceAdapter().get_type())
- return random.choice(registered_types)
-
class OpenAccessWorkspaceFactory(DjangoModelFactory):
"""A factory for the OpenAccessWorkspace model."""
diff --git a/primed/miscellaneous_workspaces/tests/test_forms.py b/primed/miscellaneous_workspaces/tests/test_forms.py
index 40b20431..b6dd125a 100644
--- a/primed/miscellaneous_workspaces/tests/test_forms.py
+++ b/primed/miscellaneous_workspaces/tests/test_forms.py
@@ -152,7 +152,7 @@ def test_valid(self):
"""Form is valid with necessary input."""
form_data = {
"workspace": self.workspace,
- "intended_workspace_type": "resource",
+ "intended_usage": "Test usage",
}
form = self.form_class(data=form_data)
self.assertTrue(form.is_valid())
@@ -160,7 +160,7 @@ def test_valid(self):
def test_invalid_missing_workspace(self):
"""Form is invalid when missing workspace."""
form_data = {
- "intended_workspace_type": "resource",
+ "intended_usage": "Test usage",
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
@@ -169,7 +169,7 @@ def test_invalid_missing_workspace(self):
self.assertEqual(len(form.errors["workspace"]), 1)
self.assertIn("required", form.errors["workspace"][0])
- def test_invalid_missing_intended_workspace_type(self):
+ def test_invalid_missing_intended_usage(self):
"""Form is invalid if intended_workspace_type is missing."""
form_data = {
"workspace": self.workspace,
@@ -177,62 +177,22 @@ def test_invalid_missing_intended_workspace_type(self):
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
self.assertEqual(len(form.errors), 1)
- self.assertIn("intended_workspace_type", form.errors)
- self.assertEqual(len(form.errors["intended_workspace_type"]), 1)
- self.assertIn("required", form.errors["intended_workspace_type"][0])
+ self.assertIn("intended_usage", form.errors)
+ self.assertEqual(len(form.errors["intended_usage"]), 1)
+ self.assertIn("required", form.errors["intended_usage"][0])
- def test_invalid_blank_intended_workspace_type(self):
+ def test_invalid_blank_intended_usage(self):
"""Form is invalid if intended_workspace_type is missing."""
form_data = {
"workspace": self.workspace,
- "intended_workspace_type": "",
+ "intended_usage": "",
}
form = self.form_class(data=form_data)
self.assertFalse(form.is_valid())
self.assertEqual(len(form.errors), 1)
- self.assertIn("intended_workspace_type", form.errors)
- self.assertEqual(len(form.errors["intended_workspace_type"]), 1)
- self.assertIn("required", form.errors["intended_workspace_type"][0])
-
- def test_invalid_intended_workspace_type_template(self):
- """Form is invalid if intended_workspace_type is "template"."""
- form_data = {
- "workspace": self.workspace,
- "intended_workspace_type": "template",
- }
- form = self.form_class(data=form_data)
- self.assertFalse(form.is_valid())
- self.assertEqual(len(form.errors), 1)
- self.assertIn("intended_workspace_type", form.errors)
- self.assertEqual(len(form.errors["intended_workspace_type"]), 1)
- self.assertIn("template", form.errors["intended_workspace_type"][0])
-
- def test_invalid_workspace_type_unregistered_type(self):
- """Form is invalid if intended_workspace_type is not a registered type."""
- form_data = {
- "workspace": self.workspace,
- "intended_workspace_type": "foo",
- }
- form = self.form_class(data=form_data)
- self.assertFalse(form.is_valid())
- self.assertEqual(len(form.errors), 1)
- self.assertIn("intended_workspace_type", form.errors)
- self.assertEqual(len(form.errors["intended_workspace_type"]), 1)
- self.assertIn("valid choice", form.errors["intended_workspace_type"][0])
-
- def test_form_all_registered_adapters(self):
- """Form is invalid if intended_workspace_type is not a registered type."""
- workspace_types = list(workspace_adapter_registry.get_registered_names().keys())
- for workspace_type in workspace_types:
- if workspace_type == "template":
- pass
- else:
- form_data = {
- "workspace": self.workspace,
- "intended_workspace_type": workspace_type,
- }
- form = self.form_class(data=form_data)
- self.assertTrue(form.is_valid())
+ self.assertIn("intended_usage", form.errors)
+ self.assertEqual(len(form.errors["intended_usage"]), 1)
+ self.assertIn("required", form.errors["intended_usage"][0])
class OpenAccessWorkspaceFormTest(TestCase):
diff --git a/primed/miscellaneous_workspaces/tests/test_models.py b/primed/miscellaneous_workspaces/tests/test_models.py
index 7b38836c..c8222656 100644
--- a/primed/miscellaneous_workspaces/tests/test_models.py
+++ b/primed/miscellaneous_workspaces/tests/test_models.py
@@ -7,7 +7,7 @@
from primed.primed_anvil.tests.factories import AvailableDataFactory, StudyFactory
from primed.users.tests.factories import UserFactory
-from .. import adapters, models
+from .. import models
from . import factories
@@ -79,7 +79,7 @@ class TemplateWorkspaceTest(TestCase):
def test_model_saving(self):
"""Creation using the model constructor and .save() works."""
workspace = WorkspaceFactory.create()
- instance = models.TemplateWorkspace(workspace=workspace)
+ instance = models.TemplateWorkspace(workspace=workspace, intended_usage="Test")
instance.save()
self.assertIsInstance(instance, models.TemplateWorkspace)
@@ -91,81 +91,6 @@ def test_str_method(self):
self.assertIsInstance(str(instance), str)
self.assertEqual(str(instance), "test-bp/test-ws")
- def test_clean_missing_intended_workspace_type_missing(self):
- workspace = WorkspaceFactory.create(
- billing_project__name="test-bp", name="test-ws"
- )
- instance = models.TemplateWorkspace(workspace=workspace)
- with self.assertRaises(ValidationError) as e:
- instance.full_clean()
- self.assertEqual(len(e.exception.message_dict), 1)
- self.assertIn("intended_workspace_type", e.exception.message_dict)
- self.assertEqual(len(e.exception.message_dict["intended_workspace_type"]), 1)
- self.assertIn(
- "cannot be blank", e.exception.message_dict["intended_workspace_type"][0]
- )
-
- def test_clean_intended_workspace_type_blank(self):
- workspace = WorkspaceFactory.create(
- billing_project__name="test-bp", name="test-ws"
- )
- instance = factories.TemplateWorkspaceFactory.build(
- workspace=workspace,
- intended_workspace_type="",
- )
- with self.assertRaises(ValidationError) as e:
- instance.full_clean()
- self.assertEqual(len(e.exception.message_dict), 1)
- self.assertIn("intended_workspace_type", e.exception.message_dict)
- self.assertEqual(len(e.exception.message_dict["intended_workspace_type"]), 1)
- self.assertIn(
- "cannot be blank", e.exception.message_dict["intended_workspace_type"][0]
- )
-
- def test_clean_intended_workspace_type_with_registered_adapter(self):
- """No ValidationError is raised if intended_workspace_type is a registered type."""
- workspace = WorkspaceFactory.create(
- billing_project__name="test-bp", name="test-ws"
- )
- instance = factories.TemplateWorkspaceFactory.build(workspace=workspace)
- instance.full_clean()
-
- def test_clean_intended_workspace_type_with_unregistered_adapter(self):
- """ValidationError is raised if intended_workspace_type is not a registered type."""
- workspace = WorkspaceFactory.create(
- billing_project__name="test-bp", name="test-ws"
- )
- instance = factories.TemplateWorkspaceFactory.build(
- workspace=workspace, intended_workspace_type="foo"
- )
- with self.assertRaises(ValidationError) as e:
- instance.full_clean()
- self.assertEqual(len(e.exception.message_dict), 1)
- self.assertIn("intended_workspace_type", e.exception.message_dict)
- self.assertEqual(len(e.exception.message_dict["intended_workspace_type"]), 1)
- self.assertIn(
- "registered types", e.exception.message_dict["intended_workspace_type"][0]
- )
-
- def test_clean_intended_workspace_type_template(self):
- """ValidationError is raised if intended_workspace_type is set to "template"."""
- workspace = WorkspaceFactory.create(
- billing_project__name="test-bp", name="test-ws"
- )
- template_workspace_type = adapters.TemplateWorkspaceAdapter().get_type()
- instance = factories.TemplateWorkspaceFactory.build(
- workspace=workspace, intended_workspace_type=template_workspace_type
- )
- with self.assertRaises(ValidationError) as e:
- instance.full_clean()
- self.assertEqual(len(e.exception.message_dict), 1)
- self.assertIn("intended_workspace_type", e.exception.message_dict)
- self.assertEqual(len(e.exception.message_dict["intended_workspace_type"]), 1)
- self.assertIn(
- template_workspace_type,
- e.exception.message_dict["intended_workspace_type"][0],
- )
-
class OpenAccessWorkspaceTest(TestCase):
"""Tests for the OpenAccessWorkspace model."""
diff --git a/primed/miscellaneous_workspaces/tests/test_tables.py b/primed/miscellaneous_workspaces/tests/test_tables.py
index de536164..d6252a70 100644
--- a/primed/miscellaneous_workspaces/tests/test_tables.py
+++ b/primed/miscellaneous_workspaces/tests/test_tables.py
@@ -49,11 +49,32 @@ def test_row_count_with_two_objects(self):
self.assertEqual(len(table.rows), 2)
-class DataPrepWorkspaceTableTest(TestCase):
- """Tests for the DataPrepWorkspaceTable table."""
+class DataPrepWorkspaceUserTableTest(TestCase):
+ """Tests for the DataPrepWorkspaceUserTable table."""
model_factory = factories.DataPrepWorkspaceFactory
- table_class = tables.DataPrepWorkspaceTable
+ table_class = tables.DataPrepWorkspaceUserTable
+
+ def test_row_count_with_no_objects(self):
+ table = self.table_class(Workspace.objects.filter(workspace_type="data_prep"))
+ self.assertEqual(len(table.rows), 0)
+
+ def test_row_count_with_one_object(self):
+ self.model_factory.create()
+ table = self.table_class(Workspace.objects.filter(workspace_type="data_prep"))
+ self.assertEqual(len(table.rows), 1)
+
+ def test_row_count_with_two_objects(self):
+ self.model_factory.create_batch(2)
+ table = self.table_class(Workspace.objects.filter(workspace_type="data_prep"))
+ self.assertEqual(len(table.rows), 2)
+
+
+class DataPrepWorkspaceStaffTableTest(TestCase):
+ """Tests for the DataPrepWorkspaceUserTable table."""
+
+ model_factory = factories.DataPrepWorkspaceFactory
+ table_class = tables.DataPrepWorkspaceStaffTable
def test_row_count_with_no_objects(self):
table = self.table_class(Workspace.objects.filter(workspace_type="data_prep"))
diff --git a/primed/miscellaneous_workspaces/tests/test_views.py b/primed/miscellaneous_workspaces/tests/test_views.py
index ed75975b..6e8d5916 100644
--- a/primed/miscellaneous_workspaces/tests/test_views.py
+++ b/primed/miscellaneous_workspaces/tests/test_views.py
@@ -734,7 +734,7 @@ def test_creates_workspace(self):
"workspacedata-INITIAL_FORMS": 0,
"workspacedata-MIN_NUM_FORMS": 1,
"workspacedata-MAX_NUM_FORMS": 1,
- "workspacedata-0-intended_workspace_type": "resource",
+ "workspacedata-0-intended_usage": "Test usage",
},
)
self.assertEqual(response.status_code, 302)
@@ -744,7 +744,7 @@ def test_creates_workspace(self):
self.assertEqual(models.TemplateWorkspace.objects.count(), 1)
new_workspace_data = models.TemplateWorkspace.objects.latest("pk")
self.assertEqual(new_workspace_data.workspace, new_workspace)
- self.assertEqual(new_workspace_data.intended_workspace_type, "resource")
+ self.assertEqual(new_workspace_data.intended_usage, "Test usage")
class TemplateWorkspaceImportTest(AnVILAPIMockTestMixin, TestCase):
@@ -860,7 +860,7 @@ def test_creates_workspace(self):
"workspacedata-INITIAL_FORMS": 0,
"workspacedata-MIN_NUM_FORMS": 1,
"workspacedata-MAX_NUM_FORMS": 1,
- "workspacedata-0-intended_workspace_type": "resource",
+ "workspacedata-0-intended_usage": "Test usage",
},
)
self.assertEqual(response.status_code, 302)
@@ -870,7 +870,7 @@ def test_creates_workspace(self):
self.assertEqual(models.TemplateWorkspace.objects.count(), 1)
new_workspace_data = models.TemplateWorkspace.objects.latest("pk")
self.assertEqual(new_workspace_data.workspace, new_workspace)
- self.assertEqual(new_workspace_data.intended_workspace_type, "resource")
+ self.assertEqual(new_workspace_data.intended_usage, "Test usage")
class OpenAccessWorkspaceDetailTest(TestCase):
diff --git a/primed/primed_anvil/admin.py b/primed/primed_anvil/admin.py
index ce6d19ee..a4265dad 100644
--- a/primed/primed_anvil/admin.py
+++ b/primed/primed_anvil/admin.py
@@ -26,18 +26,9 @@ class StudyAdmin(SimpleHistoryAdmin):
class StudySiteAdmin(admin.ModelAdmin):
"""Admin class for the `Study` model."""
- list_display = (
- "short_name",
- "full_name",
- )
- search_fields = (
- "short_name",
- "full_name",
- )
- sortable_by = (
- "short_name",
- "full_name",
- )
+ list_display = ("short_name", "full_name", "drupal_node_id")
+ search_fields = ("short_name", "full_name", "drupal_node_id")
+ sortable_by = ("short_name", "full_name", "drupal_node_id")
@admin.register(models.AvailableData)
diff --git a/primed/primed_anvil/helpers.py b/primed/primed_anvil/helpers.py
index 88460087..39193ade 100644
--- a/primed/primed_anvil/helpers.py
+++ b/primed/primed_anvil/helpers.py
@@ -2,6 +2,7 @@
from anvil_consortium_manager.models import WorkspaceGroupSharing
from django.db.models import Exists, F, OuterRef, Value
+from primed.cdsa.models import CDSAWorkspace
from primed.dbgap.models import dbGaPWorkspace
from primed.miscellaneous_workspaces.models import OpenAccessWorkspace
@@ -34,7 +35,7 @@ def get_summary_table_data():
"access_mechanism",
# Rename columns to have the same names.
workspace_name=F("workspace__name"),
- study=F("dbgap_study_accession__studies__short_name"),
+ study_name=F("dbgap_study_accession__studies__short_name"),
data=F("available_data__name"),
)
df_dbgap = pd.DataFrame.from_dict(dbgap)
@@ -48,11 +49,25 @@ def get_summary_table_data():
"access_mechanism",
# Rename columns to have the same names.
workspace_name=F("workspace__name"),
- study=F("studies__short_name"),
+ study_name=F("studies__short_name"),
data=F("available_data__name"),
)
df_open = pd.DataFrame.from_dict(open)
+ # Query for CDSAWorkspaces.
+ cdsa = CDSAWorkspace.objects.annotate(
+ access_mechanism=Value("CDSA"),
+ is_shared=Exists(shared),
+ ).values(
+ "is_shared",
+ "access_mechanism",
+ # Rename columns to have the same names.
+ workspace_name=F("workspace__name"),
+ study_name=F("study__short_name"),
+ data=F("available_data__name"),
+ )
+ df_cdsa = pd.DataFrame.from_dict(cdsa)
+
# This union may not work with MySQL < 10.3:
# https://code.djangoproject.com/ticket/31445
# qs = dbgap.union(open)
@@ -65,20 +80,20 @@ def get_summary_table_data():
# df = pd.DataFrame.from_dict(qs)
# Instead combine in pandas.
- df = pd.concat([df_dbgap, df_open])
+ df = pd.concat([df_cdsa, df_dbgap, df_open])
# If there are no workspaces, return an empty list.
if df.empty:
return []
# Sort by specific columns
- df = df.sort_values(by=["study", "access_mechanism"])
+ df = df.sort_values(by=["study_name", "access_mechanism"])
# Concatenate multiple studies into a single comma-delimited string.
df = (
df.groupby(
["workspace_name", "data", "is_shared", "access_mechanism"],
dropna=False,
- )["study"]
+ )["study_name"]
.apply(lambda x: ", ".join(x))
.reset_index()
.drop("workspace_name", axis=1)
@@ -90,7 +105,7 @@ def get_summary_table_data():
data = (
pd.pivot_table(
df,
- index=["study", "is_shared", "access_mechanism"],
+ index=["study_name", "is_shared", "access_mechanism"],
columns=["data"],
# set this to len to count the number of workspaces instead of returning a boolean value.
aggfunc=lambda x: len(x) > 0,
@@ -100,6 +115,7 @@ def get_summary_table_data():
)
.rename_axis(columns=None)
.reset_index()
+ .rename(columns={"study_name": "study", "B": "c"})
)
# Remove the dummy "no_data" column if it exists.
if "no_data" in data:
diff --git a/primed/primed_anvil/migrations/0006_studysite_drupal_node_id.py b/primed/primed_anvil/migrations/0006_studysite_drupal_node_id.py
new file mode 100644
index 00000000..91ee9bd1
--- /dev/null
+++ b/primed/primed_anvil/migrations/0006_studysite_drupal_node_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.19 on 2023-12-06 16:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('primed_anvil', '0005_availabledata'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='studysite',
+ name='drupal_node_id',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ ]
diff --git a/primed/primed_anvil/models.py b/primed/primed_anvil/models.py
index 528d85ca..2277c9b8 100644
--- a/primed/primed_anvil/models.py
+++ b/primed/primed_anvil/models.py
@@ -14,6 +14,7 @@ class Study(TimeStampedModel, models.Model):
full_name = models.CharField(
max_length=255, help_text="The full name for this Study."
)
+
history = HistoricalRecords()
class Meta:
@@ -32,13 +33,16 @@ def get_absolute_url(self):
class StudySite(TimeStampedModel, models.Model):
- """A model to track Research Centers."""
+ """A model to track Study Sites."""
short_name = models.CharField(max_length=15, unique=True)
- """The short name of the Research Center."""
+ """The short name of the Study Sites."""
full_name = models.CharField(max_length=255)
- """The full name of the Research Center."""
+ """The full name of the Study Sites."""
+
+ drupal_node_id = models.IntegerField(blank=True, null=True)
+ """Reference node ID for entity in drupal"""
def __str__(self):
"""String method.
diff --git a/primed/primed_anvil/tables.py b/primed/primed_anvil/tables.py
index 30048579..24534216 100644
--- a/primed/primed_anvil/tables.py
+++ b/primed/primed_anvil/tables.py
@@ -59,42 +59,38 @@ def _get_bool_value(self, record, value, bound_column):
return is_shared
-class DefaultWorkspaceStaffTable(tables.Table):
+class DefaultWorkspaceUserTable(tables.Table):
"""Class to use for default workspace tables in PRIMED."""
name = tables.Column(linkify=True, verbose_name="Workspace")
- billing_project = tables.Column(linkify=True)
- number_groups = tables.Column(
- verbose_name="Number of groups shared with",
- empty_values=(),
- orderable=False,
- accessor="workspacegroupsharing_set__count",
- )
is_shared = WorkspaceSharedWithConsortiumColumn()
class Meta:
model = Workspace
fields = (
"name",
- "billing_project",
- "number_groups",
"is_shared",
)
order_by = ("name",)
-class DefaultWorkspaceUserTable(tables.Table):
+class DefaultWorkspaceStaffTable(DefaultWorkspaceUserTable):
"""Class to use for default workspace tables in PRIMED."""
- name = tables.Column(linkify=True, verbose_name="Workspace")
- billing_project = tables.Column()
- is_shared = WorkspaceSharedWithConsortiumColumn()
+ billing_project = tables.Column(linkify=True)
+ number_groups = tables.Column(
+ verbose_name="Number of groups shared with",
+ empty_values=(),
+ orderable=False,
+ accessor="workspacegroupsharing_set__count",
+ )
class Meta:
model = Workspace
fields = (
"name",
"billing_project",
+ "number_groups",
"is_shared",
)
order_by = ("name",)
diff --git a/primed/primed_anvil/tests/test_helpers.py b/primed/primed_anvil/tests/test_helpers.py
index d62b5507..78a20b57 100644
--- a/primed/primed_anvil/tests/test_helpers.py
+++ b/primed/primed_anvil/tests/test_helpers.py
@@ -3,6 +3,7 @@
from anvil_consortium_manager.tests.factories import WorkspaceGroupSharingFactory
from django.test import TestCase
+from primed.cdsa.tests.factories import CDSAWorkspaceFactory
from primed.dbgap.tests.factories import dbGaPWorkspaceFactory
from primed.miscellaneous_workspaces.tests.factories import OpenAccessWorkspaceFactory
from primed.primed_anvil.tests.factories import AvailableDataFactory, StudyFactory
@@ -44,8 +45,8 @@ def test_one_dbgap_workspace_one_study_not_shared_no_available_data(self):
def test_one_open_access_workspace_one_study_not_shared_no_available_data(self):
AvailableDataFactory.create(name="Foo")
study = StudyFactory.create(short_name="TEST")
- open_access_workspace = OpenAccessWorkspaceFactory.create()
- open_access_workspace.studies.add(study)
+ workspace = OpenAccessWorkspaceFactory.create()
+ workspace.studies.add(study)
res = helpers.get_summary_table_data()
self.assertEqual(len(res), 1)
self.assertEqual(len(res[0]), 4)
@@ -59,13 +60,11 @@ def test_one_open_access_workspace_one_study_not_shared_no_available_data(self):
self.assertIn("Foo", res[0])
self.assertEqual(res[0]["Foo"], False)
- def test_one_workspace_one_study_not_shared_with_one_available_data(self):
+ def test_one_dbgap_workspace_one_study_not_shared_with_one_available_data(self):
available_data = AvailableDataFactory.create(name="Foo")
study = StudyFactory.create(short_name="TEST")
- dbgap_workspace = dbGaPWorkspaceFactory.create(
- dbgap_study_accession__studies=[study]
- )
- dbgap_workspace.available_data.add(available_data)
+ workspace = dbGaPWorkspaceFactory.create(dbgap_study_accession__studies=[study])
+ workspace.available_data.add(available_data)
res = helpers.get_summary_table_data()
self.assertEqual(len(res), 1)
self.assertEqual(len(res[0]), 4)
@@ -79,15 +78,13 @@ def test_one_workspace_one_study_not_shared_with_one_available_data(self):
self.assertIn("Foo", res[0])
self.assertEqual(res[0]["Foo"], True)
- def test_one_workspace_one_study_not_shared_with_two_available_data(self):
+ def test_one_dbgap_workspace_one_study_not_shared_with_two_available_data(self):
available_data_1 = AvailableDataFactory.create(name="Foo")
available_data_2 = AvailableDataFactory.create(name="Bar")
study = StudyFactory.create(short_name="TEST")
- dbgap_workspace = dbGaPWorkspaceFactory.create(
- dbgap_study_accession__studies=[study]
- )
- dbgap_workspace.available_data.add(available_data_1)
- dbgap_workspace.available_data.add(available_data_2)
+ workspace = dbGaPWorkspaceFactory.create(dbgap_study_accession__studies=[study])
+ workspace.available_data.add(available_data_1)
+ workspace.available_data.add(available_data_2)
res = helpers.get_summary_table_data()
self.assertEqual(len(res), 1)
self.assertEqual(len(res[0]), 5)
@@ -101,7 +98,7 @@ def test_one_workspace_one_study_not_shared_with_two_available_data(self):
self.assertIn("Foo", res[0])
self.assertEqual(res[0]["Foo"], True)
- def test_one_workspace_two_studies_not_shared_no_available_data(self):
+ def test_one_dbgap_workspace_two_studies_not_shared_no_available_data(self):
AvailableDataFactory.create(name="Foo")
study_1 = StudyFactory.create(short_name="TEST")
study_2 = StudyFactory.create(short_name="Other")
@@ -119,14 +116,12 @@ def test_one_workspace_two_studies_not_shared_no_available_data(self):
self.assertIn("Foo", res[0])
self.assertEqual(res[0]["Foo"], False)
- def test_one_workspace_one_study_shared_no_available_data(self):
+ def test_one_dbgap_workspace_one_study_shared_no_available_data(self):
AvailableDataFactory.create(name="Foo")
study = StudyFactory.create(short_name="TEST")
- dbgap_workspace = dbGaPWorkspaceFactory.create(
- dbgap_study_accession__studies=[study]
- )
+ workspace = dbGaPWorkspaceFactory.create(dbgap_study_accession__studies=[study])
WorkspaceGroupSharingFactory.create(
- workspace=dbgap_workspace.workspace, group__name="PRIMED_ALL"
+ workspace=workspace.workspace, group__name="PRIMED_ALL"
)
res = helpers.get_summary_table_data()
self.assertEqual(len(res), 1)
@@ -141,7 +136,7 @@ def test_one_workspace_one_study_shared_no_available_data(self):
self.assertIn("Foo", res[0])
self.assertEqual(res[0]["Foo"], False)
- def test_two_workspaces_one_study(self):
+ def test_two_dbgap_workspaces_one_study(self):
AvailableDataFactory.create(name="Foo")
study = StudyFactory.create(short_name="TEST")
dbGaPWorkspaceFactory.create(dbgap_study_accession__studies=[study])
@@ -159,21 +154,21 @@ def test_two_workspaces_one_study(self):
self.assertIn("Foo", res[0])
self.assertEqual(res[0]["Foo"], False)
- def test_two_workspaces_one_study_one_shared(self):
+ def test_two_dbgap_workspaces_one_study_one_shared(self):
available_data_1 = AvailableDataFactory.create(name="Foo")
available_data_2 = AvailableDataFactory.create(name="Bar")
study = StudyFactory.create(short_name="TEST")
- dbgap_workspace_1 = dbGaPWorkspaceFactory.create(
+ workspace_1 = dbGaPWorkspaceFactory.create(
dbgap_study_accession__studies=[study]
)
- dbgap_workspace_1.available_data.add(available_data_1)
+ workspace_1.available_data.add(available_data_1)
WorkspaceGroupSharingFactory.create(
- workspace=dbgap_workspace_1.workspace, group__name="PRIMED_ALL"
+ workspace=workspace_1.workspace, group__name="PRIMED_ALL"
)
- dbgap_workspace_2 = dbGaPWorkspaceFactory.create(
+ workspace_2 = dbGaPWorkspaceFactory.create(
dbgap_study_accession__studies=[study]
)
- dbgap_workspace_2.available_data.add(available_data_2)
+ workspace_2.available_data.add(available_data_2)
res = helpers.get_summary_table_data()
self.assertEqual(len(res), 2)
self.assertIn(
@@ -197,7 +192,7 @@ def test_two_workspaces_one_study_one_shared(self):
res,
)
- def test_two_workspaces_multiple_studies(self):
+ def test_two_dbgap_workspaces_multiple_studies(self):
AvailableDataFactory.create(name="Foo")
study_1 = StudyFactory.create(short_name="TEST")
study_2 = StudyFactory.create(short_name="Other")
@@ -229,8 +224,8 @@ def test_one_dbgap_workspace_one_open_access_workspace_different_studies(self):
study_1 = StudyFactory.create(short_name="TEST")
dbGaPWorkspaceFactory.create(dbgap_study_accession__studies=[study_1])
study_2 = StudyFactory.create(short_name="Other")
- open_access_workspace = OpenAccessWorkspaceFactory.create()
- open_access_workspace.studies.add(study_2)
+ workspace = OpenAccessWorkspaceFactory.create()
+ workspace.studies.add(study_2)
res = helpers.get_summary_table_data()
self.assertEqual(len(res), 2)
self.assertIn(
@@ -256,8 +251,8 @@ def test_one_dbgap_workspace_one_open_access_workspace_same_study(self):
AvailableDataFactory.create(name="Foo")
study = StudyFactory.create(short_name="TEST")
dbGaPWorkspaceFactory.create(dbgap_study_accession__studies=[study])
- open_access_workspace = OpenAccessWorkspaceFactory.create()
- open_access_workspace.studies.add(study)
+ workspace = OpenAccessWorkspaceFactory.create()
+ workspace.studies.add(study)
res = helpers.get_summary_table_data()
self.assertEqual(len(res), 2)
self.assertIn(
@@ -284,12 +279,10 @@ def test_one_dbgap_workspace_one_open_access_workspace_different_available_data(
):
available_data_1 = AvailableDataFactory.create(name="Foo")
study = StudyFactory.create(short_name="TEST")
- dbgap_workspace = dbGaPWorkspaceFactory.create(
- dbgap_study_accession__studies=[study]
- )
- dbgap_workspace.available_data.add(available_data_1)
- open_access_workspace = OpenAccessWorkspaceFactory.create()
- open_access_workspace.studies.add(study)
+ workspace = dbGaPWorkspaceFactory.create(dbgap_study_accession__studies=[study])
+ workspace.available_data.add(available_data_1)
+ workspace = OpenAccessWorkspaceFactory.create()
+ workspace.studies.add(study)
res = helpers.get_summary_table_data()
self.assertEqual(len(res), 2)
self.assertIn(
@@ -310,3 +303,244 @@ def test_one_dbgap_workspace_one_open_access_workspace_different_available_data(
},
res,
)
+
+ def test_one_cdsa_workspace_not_shared_no_available_data(self):
+ AvailableDataFactory.create(name="Foo")
+ study = StudyFactory.create(short_name="TEST")
+ CDSAWorkspaceFactory.create(study=study)
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]), 4)
+ self.assertIn("study", res[0])
+ self.assertEqual(res[0]["study"], "TEST")
+ self.assertIn("access_mechanism", res[0])
+ self.assertEqual(res[0]["access_mechanism"], "CDSA")
+ self.assertIn("is_shared", res[0])
+ self.assertEqual(res[0]["is_shared"], False)
+ # Available data columns.
+ self.assertIn("Foo", res[0])
+ self.assertEqual(res[0]["Foo"], False)
+
+ def test_one_cdsa_workspace_not_shared_with_one_available_data(self):
+ available_data = AvailableDataFactory.create(name="Foo")
+ study = StudyFactory.create(short_name="TEST")
+ workspace = CDSAWorkspaceFactory.create(study=study)
+ workspace.available_data.add(available_data)
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]), 4)
+ self.assertIn("study", res[0])
+ self.assertEqual(res[0]["study"], "TEST")
+ self.assertIn("access_mechanism", res[0])
+ self.assertEqual(res[0]["access_mechanism"], "CDSA")
+ self.assertIn("is_shared", res[0])
+ self.assertEqual(res[0]["is_shared"], False)
+ # Available data columns.
+ self.assertIn("Foo", res[0])
+ self.assertEqual(res[0]["Foo"], True)
+
+ def test_one_cdsa_workspace_not_shared_with_two_available_data(self):
+ available_data_1 = AvailableDataFactory.create(name="Foo")
+ available_data_2 = AvailableDataFactory.create(name="Bar")
+ study = StudyFactory.create(short_name="TEST")
+ workspace = CDSAWorkspaceFactory.create(
+ study=study,
+ )
+ workspace.available_data.add(available_data_1)
+ workspace.available_data.add(available_data_2)
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]), 5)
+ self.assertIn("study", res[0])
+ self.assertEqual(res[0]["study"], "TEST")
+ self.assertIn("access_mechanism", res[0])
+ self.assertEqual(res[0]["access_mechanism"], "CDSA")
+ self.assertIn("is_shared", res[0])
+ self.assertEqual(res[0]["is_shared"], False)
+ # Available data columns.
+ self.assertIn("Foo", res[0])
+ self.assertEqual(res[0]["Foo"], True)
+
+ def test_one_cdsa_workspace_one_study_shared_no_available_data(self):
+ AvailableDataFactory.create(name="Foo")
+ study = StudyFactory.create(short_name="TEST")
+ workspace = CDSAWorkspaceFactory.create(study=study)
+ WorkspaceGroupSharingFactory.create(
+ workspace=workspace.workspace, group__name="PRIMED_ALL"
+ )
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]), 4)
+ self.assertIn("study", res[0])
+ self.assertEqual(res[0]["study"], "TEST")
+ self.assertIn("access_mechanism", res[0])
+ self.assertEqual(res[0]["access_mechanism"], "CDSA")
+ self.assertIn("is_shared", res[0])
+ self.assertEqual(res[0]["is_shared"], True)
+ # Available data columns.
+ self.assertIn("Foo", res[0])
+ self.assertEqual(res[0]["Foo"], False)
+
+ def test_two_cdsa_workspaces_one_study(self):
+ AvailableDataFactory.create(name="Foo")
+ study = StudyFactory.create(short_name="TEST")
+ CDSAWorkspaceFactory.create(study=study)
+ CDSAWorkspaceFactory.create(study=study)
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]), 4)
+ self.assertIn("study", res[0])
+ self.assertEqual(res[0]["study"], "TEST")
+ self.assertIn("access_mechanism", res[0])
+ self.assertEqual(res[0]["access_mechanism"], "CDSA")
+ self.assertIn("is_shared", res[0])
+ self.assertEqual(res[0]["is_shared"], False)
+ # Available data columns.
+ self.assertIn("Foo", res[0])
+ self.assertEqual(res[0]["Foo"], False)
+
+ def test_two_cdsa_workspaces_one_study_one_shared(self):
+ available_data_1 = AvailableDataFactory.create(name="Foo")
+ available_data_2 = AvailableDataFactory.create(name="Bar")
+ study = StudyFactory.create(short_name="TEST")
+ workspace_1 = CDSAWorkspaceFactory.create(study=study)
+ workspace_1.available_data.add(available_data_1)
+ WorkspaceGroupSharingFactory.create(
+ workspace=workspace_1.workspace, group__name="PRIMED_ALL"
+ )
+ workspace_2 = CDSAWorkspaceFactory.create(study=study)
+ workspace_2.available_data.add(available_data_2)
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 2)
+ self.assertIn(
+ {
+ "study": "TEST",
+ "is_shared": True,
+ "access_mechanism": "CDSA",
+ "Foo": True,
+ "Bar": False,
+ },
+ res,
+ )
+ self.assertIn(
+ {
+ "study": "TEST",
+ "is_shared": False,
+ "access_mechanism": "CDSA",
+ "Foo": False,
+ "Bar": True,
+ },
+ res,
+ )
+
+ def test_two_cdsa_workspaces(self):
+ AvailableDataFactory.create(name="Foo")
+ study_1 = StudyFactory.create(short_name="TEST")
+ study_2 = StudyFactory.create(short_name="Other")
+ CDSAWorkspaceFactory.create(study=study_1)
+ CDSAWorkspaceFactory.create(study=study_2)
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 2)
+ self.assertIn(
+ {
+ "study": "Other",
+ "is_shared": False,
+ "access_mechanism": "CDSA",
+ "Foo": False,
+ },
+ res,
+ )
+ self.assertIn(
+ {
+ "study": "TEST",
+ "is_shared": False,
+ "access_mechanism": "CDSA",
+ "Foo": False,
+ },
+ res,
+ )
+
+ def test_one_cdsa_workspace_one_open_access_workspace_different_studies(self):
+ AvailableDataFactory.create(name="Foo")
+ study_1 = StudyFactory.create(short_name="TEST")
+ CDSAWorkspaceFactory.create(study=study_1)
+ study_2 = StudyFactory.create(short_name="Other")
+ workspace = OpenAccessWorkspaceFactory.create()
+ workspace.studies.add(study_2)
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 2)
+ self.assertIn(
+ {
+ "study": "TEST",
+ "is_shared": False,
+ "access_mechanism": "CDSA",
+ "Foo": False,
+ },
+ res,
+ )
+ self.assertIn(
+ {
+ "study": "Other",
+ "is_shared": False,
+ "access_mechanism": "Open access",
+ "Foo": False,
+ },
+ res,
+ )
+
+ def test_one_cdsa_workspace_one_open_access_workspace_same_study(self):
+ AvailableDataFactory.create(name="Foo")
+ study = StudyFactory.create(short_name="TEST")
+ CDSAWorkspaceFactory.create(study=study)
+ workspace = OpenAccessWorkspaceFactory.create()
+ workspace.studies.add(study)
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 2)
+ self.assertIn(
+ {
+ "study": "TEST",
+ "is_shared": False,
+ "access_mechanism": "CDSA",
+ "Foo": False,
+ },
+ res,
+ )
+ self.assertIn(
+ {
+ "study": "TEST",
+ "is_shared": False,
+ "access_mechanism": "Open access",
+ "Foo": False,
+ },
+ res,
+ )
+
+ def test_one_cdsa_workspace_one_open_access_workspace_different_available_data(
+ self,
+ ):
+ available_data_1 = AvailableDataFactory.create(name="Foo")
+ study = StudyFactory.create(short_name="TEST")
+ workspace = CDSAWorkspaceFactory.create(study=study)
+ workspace.available_data.add(available_data_1)
+ workspace = OpenAccessWorkspaceFactory.create()
+ workspace.studies.add(study)
+ res = helpers.get_summary_table_data()
+ self.assertEqual(len(res), 2)
+ self.assertIn(
+ {
+ "study": "TEST",
+ "is_shared": False,
+ "access_mechanism": "CDSA",
+ "Foo": True,
+ },
+ res,
+ )
+ self.assertIn(
+ {
+ "study": "TEST",
+ "is_shared": False,
+ "access_mechanism": "Open access",
+ "Foo": False,
+ },
+ res,
+ )
diff --git a/primed/primed_anvil/tests/test_views.py b/primed/primed_anvil/tests/test_views.py
index b84d4c5f..9a95ea11 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
from anvil_consortium_manager.views import AccountList
+from constance.test import override_config
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
@@ -133,6 +134,12 @@ def test_user_has_not_linked_account(self):
response, reverse("anvil_consortium_manager:accounts:link")
)
+ def test_unauthenticated_user_has_not_linked_account_message(self):
+ response = self.client.get(settings.LOGIN_URL, follow=True)
+ self.assertNotContains(
+ response, reverse("anvil_consortium_manager:accounts:link")
+ )
+
def test_staff_view_links(self):
user = UserFactory.create()
user.user_permissions.add(
@@ -163,6 +170,24 @@ def test_view_links(self):
response, '"{}"'.format(reverse("anvil_consortium_manager:index"))
)
+ def test_site_announcement_no_text(self):
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url())
+ self.assertNotContains(response, """id="alert-announcement""")
+
+ @override_config(ANNOUNCEMENT_TEXT="This is a test announcement")
+ def test_site_announcement_text(self):
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url())
+ self.assertContains(response, """id="alert-announcement""")
+ self.assertContains(response, "This is a test announcement")
+
+ @override_config(ANNOUNCEMENT_TEXT="This is a test announcement")
+ def test_site_announcement_text_unauthenticated_user(self):
+ response = self.client.get(self.get_url(), follow=True)
+ self.assertContains(response, """id="alert-announcement""")
+ self.assertContains(response, "This is a test announcement")
+
class StudyDetailTest(TestCase):
"""Tests for the StudyDetail view."""
@@ -1214,3 +1239,31 @@ def test_table_rows(self):
response = self.client.get(self.get_url())
self.assertIn("summary_table", response.context_data)
self.assertEqual(len(response.context_data["summary_table"].rows), 2)
+
+ def test_includes_open_access_workspaces(self):
+ """Open access workspaces are included in the table."""
+ study = StudyFactory.create()
+ open_workspace = OpenAccessWorkspaceFactory.create()
+ open_workspace.studies.add(study)
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url())
+ self.assertIn("summary_table", response.context_data)
+ self.assertEqual(len(response.context_data["summary_table"].rows), 1)
+
+ def test_includes_dbgap_workspaces(self):
+ """dbGaP workspaces are included in the table."""
+ # One open access workspace with one study, with one available data type.
+ # One dbGaP workspae with two studies.
+ dbGaPWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url())
+ self.assertIn("summary_table", response.context_data)
+ self.assertEqual(len(response.context_data["summary_table"].rows), 1)
+
+ def test_includes_cdsa_workspaces(self):
+ """CDSA workspaces are included in the table."""
+ CDSAWorkspaceFactory.create()
+ self.client.force_login(self.user)
+ response = self.client.get(self.get_url())
+ self.assertIn("summary_table", response.context_data)
+ self.assertEqual(len(response.context_data["summary_table"].rows), 1)
diff --git a/primed/static/css/project.css b/primed/static/css/project.css
index 6243cad2..78fa7855 100644
--- a/primed/static/css/project.css
+++ b/primed/static/css/project.css
@@ -134,7 +134,6 @@ h3, .h3 {
}
h4, .h4 {
- font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
@@ -12012,9 +12011,6 @@ a.navbar-brand:hover {
background-color: unset;
}
-.navbar {
- min-height: 132px;
-}
.navbar-brand {
height: 100px;
diff --git a/primed/static/js/project.js b/primed/static/js/project.js
index d26d23b9..9c90629e 100644
--- a/primed/static/js/project.js
+++ b/primed/static/js/project.js
@@ -1 +1,19 @@
/* Project specific Javascript goes here. */
+
+// Handle paste event for text inputs with maxlength.
+const checkPasteLength = (e) => {
+ var paste = (e.clipboardData || window.clipboardData).getData("text");
+ maxlength = e.target.getAttribute("maxlength");
+ if (paste.length > maxlength) {
+ alert("String longer than allowed maximum length of " + maxlength + " characters:\n" + paste)
+ e.preventDefault()
+ e.stopPropagation()
+ }
+}
+
+var textInputs = $('form').find("input[maxlength]")
+// textInputs.on("paste", checkPasteLength);
+for(var i = 0; i < textInputs.length; i++){
+ // Console: print the clicked element
+ textInputs[i].addEventListener("paste", checkPasteLength);
+}
diff --git a/primed/templates/base.html b/primed/templates/base.html
index 59db3d77..1789536c 100644
--- a/primed/templates/base.html
+++ b/primed/templates/base.html
@@ -108,23 +108,32 @@
- {% block extra_navbar %}
- {% endblock %}
-
+
- {% if messages %}
- {% for message in messages %}
-
{{ message }}
- {% endfor %}
+ {% if config.ANNOUNCEMENT_TEXT %}
+
+
+ {{ config.ANNOUNCEMENT_TEXT }}
+
{% endif %}
- {% if not request.user.account %}
-
+ {% if request.user.is_authenticated and not request.user.account %}
+
- {% endif %}
+ {% endif %}
+
+ {% if messages %}
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+ {% endif %}
+
+ {% block extra_navbar %}
+ {% endblock %}
+
{% block content %}
diff --git a/primed/templates/cdsa/cdsaworkspace_detail.html b/primed/templates/cdsa/cdsaworkspace_detail.html
index 76a7952c..3a192665 100644
--- a/primed/templates/cdsa/cdsaworkspace_detail.html
+++ b/primed/templates/cdsa/cdsaworkspace_detail.html
@@ -1,16 +1,46 @@
{% extends "anvil_consortium_manager/workspace_detail.html" %}
+{% load render_table from django_tables2 %}
{% block pills %}
{% if workspace_data_object.gsr_restricted %}
{% include "snippets/gsr_restricted_badge.html" %}
{% endif %}
+ {% if not primary_cdsa %}
+
+
+ No primary CDSA
+
+
+ {% elif primary_cdsa.requires_study_review %}
+
+
+ Study review required
+
+
+ {% endif %}
+
{{ block.super }}
{% endblock pills %}
{% block workspace_data %}
+ - Associated CDSA
-
+ {% if primary_cdsa %}
+ {{ primary_cdsa }}
+ {% else %}
+ —
+ {% endif %}
+
- Study
-
{{ object.cdsaworkspace.study }}
@@ -55,7 +85,27 @@
- {{ object.cdsaworkspace.data_use_limitations }}
+
+ - DUO consent description
+ -
+
- {{ workspace_data_object.data_use_permission.abbreviation }}: {{ workspace_data_object.data_use_permission.get_short_definition }}
+ {% for x in workspace_data_object.data_use_modifiers.all %}
+ - {{ x.abbreviation }}: {{ x.get_short_definition }}
+ {% endfor %}
+
+ {% if workspace_data_object.additional_limitations %}
+ - Additional limitations for this consent group
+ -
+
- {{ workspace_data_object.additional_limitations }}
+
+ {% endif %}
+ {% if primary_cdsa.additional_limitations %}
+ - Additional limitations from CDSA
+ -
+
- {{ primary_cdsa.additional_limitations }}
+
+ {% endif %}
+
@@ -80,5 +130,7 @@
+{% include "snippets/data_prep_workspace_table.html" with table=associated_data_prep_workspaces is_active=data_prep_active %}
+
{{block.super}}
{% endblock after_panel %}
diff --git a/primed/templates/cdsa/dataaffiliateagreement_detail.html b/primed/templates/cdsa/dataaffiliateagreement_detail.html
index 211b3e92..ac650255 100644
--- a/primed/templates/cdsa/dataaffiliateagreement_detail.html
+++ b/primed/templates/cdsa/dataaffiliateagreement_detail.html
@@ -20,7 +20,20 @@
Representative role {{ object.signed_agreement.representative_role }}
Signing institution {{ object.signed_agreement.signing_institution }}
- Primary? {{ object.signed_agreement.is_primary }}
+ Primary?
+ {% if object.is_primary %}
+ Yes
+ {% else %}
+ No
+ {% endif %}
+
+ Study review required?
+ {% if object.requires_study_review %}
+ Yes
+ {% else %}
+ No
+ {% endif %}
+
Agreement version
{{ object.signed_agreement.version }}
@@ -45,6 +58,28 @@
{% endblock panel %}
+{% block after_panel %}
+
+{% if object.additional_limitations %}
+
+
+
+
+
+ {{ object.additional_limitations }}
+
+
+
+
+{% endif %}
+
+{{ block.super }}
+
+{% endblock after_panel %}
+
{% block action_buttons %}
{% if show_update_button %}
diff --git a/primed/templates/cdsa/memberagreement_detail.html b/primed/templates/cdsa/memberagreement_detail.html
index 381b1f23..c4a4b1d9 100644
--- a/primed/templates/cdsa/memberagreement_detail.html
+++ b/primed/templates/cdsa/memberagreement_detail.html
@@ -20,7 +20,13 @@
Representative role {{ object.signed_agreement.representative_role }}
Signing institution {{ object.signed_agreement.signing_institution }}
- Primary? {{ object.signed_agreement.is_primary }}
+ Primary?
+ {% if object.is_primary %}
+ Yes
+ {% else %}
+ No
+ {% endif %}
+
Agreement version
{{ object.signed_agreement.version }}
diff --git a/primed/templates/cdsa/nondataaffiliateagreement_detail.html b/primed/templates/cdsa/nondataaffiliateagreement_detail.html
index bcd4b934..4d08830f 100644
--- a/primed/templates/cdsa/nondataaffiliateagreement_detail.html
+++ b/primed/templates/cdsa/nondataaffiliateagreement_detail.html
@@ -20,7 +20,6 @@
Representative role {{ object.signed_agreement.representative_role }}
Signing institution {{ object.signed_agreement.signing_institution }}
- Primary? {{ object.signed_agreement.is_primary }}
Agreement version
{{ object.signed_agreement.version }}
diff --git a/primed/templates/dbgap/dbgapworkspace_detail.html b/primed/templates/dbgap/dbgapworkspace_detail.html
index 944beecd..c71d751f 100644
--- a/primed/templates/dbgap/dbgapworkspace_detail.html
+++ b/primed/templates/dbgap/dbgapworkspace_detail.html
@@ -1,4 +1,5 @@
{% extends "anvil_consortium_manager/workspace_detail.html" %}
+{% load render_table from django_tables2%}
{% block pills %}
{% if workspace_data_object.gsr_restricted %}
@@ -93,6 +94,8 @@