Skip to content

Commit

Permalink
Merge branch 'main' into feature/add-associated-data-prep-workspace-t…
Browse files Browse the repository at this point in the history
…ables-on-workspace-detail
  • Loading branch information
amstilp committed Mar 14, 2024
2 parents 4ed89f5 + 6e5806f commit 9a63e19
Show file tree
Hide file tree
Showing 40 changed files with 1,810 additions and 176 deletions.
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ updates:
interval: "weekly"
day: "sunday"
allow:
# Allow both direct and indirect updates for all packages
- dependency-type: "all"
# Allow only direct dependencies - should be ok with pip-sync?
- dependency-type: "direct"
# Allow up to 20 dependencies for pip dependencies
open-pull-requests-limit: 20
2 changes: 2 additions & 0 deletions .github/workflows/combine-prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ jobs:
with:
labels: combined-pr # Optional: add a label to the combined PR
ci_required: true # require all checks to pass before combining
select_label: dependencies # Optional: only combine PRs with this label
autoclose: false # do not close the source PRs - dependabot should handle it.
12 changes: 9 additions & 3 deletions add_cdsa_example_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@
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 major versions
major_version = factories.AgreementMajorVersionFactory.create(version=1)

Expand All @@ -28,8 +32,8 @@
)

# Create a couple signed CDSAs.
dup = DataUsePermissionFactory.create(abbreviation="GRU")
dum = DataUseModifierFactory.create(abbreviation="NPU")
dup = DataUsePermission.objects.get(abbreviation="GRU")
dum = DataUseModifier.objects.get(abbreviation="NPU")

# create the CDSA auth group
cdsa_group = ManagedGroupFactory.create(name=settings.ANVIL_CDSA_GROUP_NAME)
Expand Down Expand Up @@ -113,6 +117,7 @@
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.",
)
GroupGroupMembershipFactory.create(
parent_group=cdsa_group, child_group=cdsa_1006.signed_agreement.anvil_access_group
Expand Down Expand Up @@ -219,5 +224,6 @@
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)
6 changes: 6 additions & 0 deletions primed/cdsa/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,10 @@ def get_extra_detail_context_data(self, workspace, request):
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
39 changes: 6 additions & 33 deletions primed/cdsa/audit/signed_agreement_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,20 @@
from django.db.models import QuerySet
from django.urls import reverse

from primed.primed_anvil.audit import PRIMEDAudit, PRIMEDAuditResult

from .. import models


# Dataclasses for storing audit results?
@dataclass
class AccessAuditResult:
class AccessAuditResult(PRIMEDAuditResult):
"""Base class to hold results for auditing CDSA access for a specific SignedAgreement."""

note: str
signed_agreement: models.SignedAgreement
action: str = None

# def __init__(self, *args, **kwargs):
# super().__init(*args, **kwargs)
# self.anvil_cdsa_group = ManagedGroup.objects.get(name="PRIMED_CDSA")

def __post_init__(self):
self.anvil_cdsa_group = ManagedGroup.objects.get(
name=settings.ANVIL_CDSA_GROUP_NAME
Expand Down Expand Up @@ -107,7 +105,7 @@ class Meta:
attrs = {"class": "table align-middle"}


class SignedAgreementAccessAudit:
class SignedAgreementAccessAudit(PRIMEDAudit):
"""Audit for Signed Agreements."""

# Access verified.
Expand All @@ -129,12 +127,7 @@ class SignedAgreementAccessAudit:
results_table_class = SignedAgreementAccessAuditTable

def __init__(self, signed_agreement_queryset=None):
# Store the CDSA group for auditing membership.
self.completed = False
# Set up lists to hold audit results.
self.verified = []
self.needs_action = []
self.errors = []
super().__init__()
# Store the queryset to run the audit on.
if signed_agreement_queryset is None:
signed_agreement_queryset = models.SignedAgreement.objects.all()
Expand Down Expand Up @@ -332,27 +325,7 @@ def _audit_signed_agreement(self, signed_agreement):
else:
self._audit_component_agreement(signed_agreement)

def run_audit(self):
def _run_audit(self):
"""Run an audit on all SignedAgreements."""
for signed_agreement in self.signed_agreement_queryset:
self._audit_signed_agreement(signed_agreement)
self.completed = True

def get_all_results(self):
return self.verified + self.needs_action + self.errors

def get_verified_table(self):
"""Return a table of verified results."""
return self.results_table_class(
[x.get_table_dictionary() for x in self.verified]
)

def get_needs_action_table(self):
"""Return a table of results where action is needed."""
return self.results_table_class(
[x.get_table_dictionary() for x in self.needs_action]
)

def get_errors_table(self):
"""Return a table of audit errors."""
return self.results_table_class([x.get_table_dictionary() for x in self.errors])
28 changes: 5 additions & 23 deletions primed/cdsa/audit/workspace_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
from django.db.models import QuerySet
from django.urls import reverse

from primed.primed_anvil.audit import PRIMEDAudit, PRIMEDAuditResult

# from . import models
from .. import models


@dataclass
class AccessAuditResult:
class AccessAuditResult(PRIMEDAuditResult):
"""Base class to hold results for auditing CDSA access for a specific SignedAgreement."""

workspace: models.CDSAWorkspace
Expand Down Expand Up @@ -106,7 +108,7 @@ class Meta:
attrs = {"class": "table align-middle"}


class WorkspaceAccessAudit:
class WorkspaceAccessAudit(PRIMEDAudit):
"""Audit for CDSA Workspaces."""

# Access verified.
Expand Down Expand Up @@ -224,27 +226,7 @@ def _audit_workspace(self, workspace):
)
return

def run_audit(self):
def _run_audit(self):
"""Run an audit on all SignedAgreements."""
for workspace in self.cdsa_workspace_queryset:
self._audit_workspace(workspace)
self.completed = True

def get_verified_table(self):
"""Return a table of verified results."""
return self.results_table_class(
[x.get_table_dictionary() for x in self.verified]
)

def get_all_results(self):
return self.verified + self.needs_action + self.errors

def get_needs_action_table(self):
"""Return a table of results where action is needed."""
return self.results_table_class(
[x.get_table_dictionary() for x in self.needs_action]
)

def get_errors_table(self):
"""Return a table of audit errors."""
return self.results_table_class([x.get_table_dictionary() for x in self.errors])
3 changes: 2 additions & 1 deletion primed/cdsa/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class Meta:
fields = (
"signed_agreement",
"study",
"additional_limitations",
)
widgets = {
"study": autocomplete.ModelSelect2(
Expand Down Expand Up @@ -114,7 +115,7 @@ class Meta:
"data_use_permission",
"data_use_modifiers",
"disease_term",
"data_use_limitations",
"additional_limitations",
"gsr_restricted",
"acknowledgments",
"available_data",
Expand Down
92 changes: 92 additions & 0 deletions primed/cdsa/management/commands/run_cdsa_audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.urls import reverse

from ...audit import signed_agreement_audit, workspace_audit


class Command(BaseCommand):
help = "Run dbGaP access audit."

def add_arguments(self, parser):
email_group = parser.add_argument_group(title="Email reports")
email_group.add_argument(
"--email",
help="""Email to which to send access reports that need action or have errors.""",
)

def _audit_signed_agreements(self):
self.stdout.write("Running SignedAgreement access audit... ", ending="")
data_access_audit = signed_agreement_audit.SignedAgreementAccessAudit()
data_access_audit.run_audit()

# Construct the url for handling errors.
url = (
"https://"
+ Site.objects.get_current().domain
+ reverse("cdsa:audit:signed_agreements:all")
)
self._report_results(data_access_audit, url)
self._send_email(data_access_audit, url)

def _audit_workspaces(self):
self.stdout.write("Running CDSAWorkspace access audit... ", ending="")
data_access_audit = workspace_audit.WorkspaceAccessAudit()
data_access_audit.run_audit()

# Construct the url for handling errors.
url = (
"https://"
+ Site.objects.get_current().domain
+ reverse("cdsa:audit:workspaces:all")
)
self._report_results(data_access_audit, url)
self._send_email(data_access_audit, url)

def _report_results(self, data_access_audit, resolve_url):
# Report errors and needs access.
audit_ok = data_access_audit.ok()
if audit_ok:
self.stdout.write(self.style.SUCCESS("ok!"))
else:
self.stdout.write(self.style.ERROR("problems found."))

# Print results
self.stdout.write("* Verified: {}".format(len(data_access_audit.verified)))
self.stdout.write(
"* Needs action: {}".format(len(data_access_audit.needs_action))
)
self.stdout.write("* Errors: {}".format(len(data_access_audit.errors)))

if not audit_ok:
self.stdout.write(
self.style.ERROR(f"Please visit {resolve_url} to resolve these issues.")
)

def _send_email(self, data_access_audit, url):
# Send email if requested and there are problems.
if not data_access_audit.ok():
subject = "CDSA {} errors".format(data_access_audit.__class__.__name__)
html_body = render_to_string(
"primed_anvil/email_audit_report.html",
context={
"title": subject,
"data_access_audit": data_access_audit,
"url": url,
},
)
send_mail(
subject,
"Audit problems found. Please see attached report.",
None,
[self.email],
fail_silently=False,
html_message=html_body,
)

def handle(self, *args, **options):
self.email = options["email"]
self._audit_signed_agreements()
self._audit_workspaces()
Original file line number Diff line number Diff line change
@@ -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.",
),
),
]
Original file line number Diff line number Diff line change
@@ -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",
),
]
Loading

0 comments on commit 9a63e19

Please sign in to comment.