diff --git a/add_cdsa_example_data.py b/add_cdsa_example_data.py index 16152baf..61e28226 100644 --- a/add_cdsa_example_data.py +++ b/add_cdsa_example_data.py @@ -9,6 +9,7 @@ ManagedGroupFactory, WorkspaceGroupSharingFactory, ) +from django.conf import settings from primed.cdsa.tests import factories from primed.duo.tests.factories import DataUseModifierFactory, DataUsePermissionFactory @@ -17,16 +18,23 @@ from primed.users.models import User from primed.users.tests.factories import UserFactory +# Create major versions +major_version = factories.AgreementMajorVersionFactory.create(version=1) + # Create some agreement versions -v10 = factories.AgreementVersionFactory.create(major_version=1, minor_version=0) -v11 = factories.AgreementVersionFactory.create(major_version=1, minor_version=1) +v10 = factories.AgreementVersionFactory.create( + major_version=major_version, minor_version=0 +) +v11 = factories.AgreementVersionFactory.create( + major_version=major_version, minor_version=1 +) # 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="PRIMED_CDSA") +cdsa_group = ManagedGroupFactory.create(name=settings.ANVIL_CDSA_GROUP_NAME) # Create some study sites. StudySiteFactory.create(short_name="CARDINAL", full_name="CARDINAL") diff --git a/config/settings/local.py b/config/settings/local.py index 252382b3..b6b17daa 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -76,3 +76,4 @@ ANVIL_DATA_ACCESS_GROUP_PREFIX = env( "ANVIL_DATA_ACCESS_GROUP_PREFIX", default="DEV_PRIMED" ) +ANVIL_CDSA_GROUP_NAME = env("ANVIL_CDSA_GROUP_NAME", default="DEV_PRIMED_CDSA") diff --git a/primed/cdsa/admin.py b/primed/cdsa/admin.py index 6f5d373c..cdc71f4d 100644 --- a/primed/cdsa/admin.py +++ b/primed/cdsa/admin.py @@ -4,17 +4,33 @@ from . import models +@admin.register(models.AgreementMajorVersion) +class AgreementMajorVersion(SimpleHistoryAdmin): + """Admin class for the `AgreementMajorVersion` model.""" + + list_display = ( + "version", + "is_valid", + ) + list_filter = ( + "version", + "is_valid", + ) + sortable_by = ("version",) + + @admin.register(models.AgreementVersion) class AgreementVersion(SimpleHistoryAdmin): """Admin class for the `AgreementVersion` model.""" list_display = ( "full_version", - "major_version", - "minor_version", "date_approved", ) - list_filter = ("major_version",) + list_filter = ( + "major_version", + "major_version__is_valid", + ) sortable_by = ( "major_version", "minor_version", @@ -38,6 +54,7 @@ class SignedAgreement(SimpleHistoryAdmin): "type", "is_primary", "version", + "status", ) search_fields = ( "representative", @@ -63,6 +80,7 @@ class MemberAgreementAdmin(SimpleHistoryAdmin): list_filter = ( "study_site", "signed_agreement__is_primary", + "signed_agreement__status", ) @@ -77,6 +95,7 @@ class DataAffiliateAgreementAdmin(SimpleHistoryAdmin): list_filter = ( "study", "signed_agreement__is_primary", + "signed_agreement__status", ) @@ -88,7 +107,10 @@ class NonDataAffiliateAgreementAdmin(SimpleHistoryAdmin): "signed_agreement", "affiliation", ) - list_filter = ("signed_agreement__is_primary",) + list_filter = ( + "signed_agreement__is_primary", + "signed_agreement__status", + ) @admin.register(models.CDSAWorkspace) diff --git a/primed/cdsa/audit/signed_agreement_audit.py b/primed/cdsa/audit/signed_agreement_audit.py index 19002e6a..c264271f 100644 --- a/primed/cdsa/audit/signed_agreement_audit.py +++ b/primed/cdsa/audit/signed_agreement_audit.py @@ -2,7 +2,7 @@ from dataclasses import dataclass import django_tables2 as tables -from anvil_consortium_manager.models import GroupGroupMembership, ManagedGroup +from anvil_consortium_manager.models import ManagedGroup from django.conf import settings from django.urls import reverse from django.utils.safestring import mark_safe @@ -39,8 +39,6 @@ def get_table_dictionary(self): """Return a dictionary that can be used to populate an instance of `SignedAgreementAccessAuditTable`.""" row = { "signed_agreement": self.signed_agreement, - "agreement_group": self.signed_agreement.agreement_group, - "agreement_type": self.signed_agreement.combined_type, "note": self.note, "action": self.get_action(), "action_url": self.get_action_url(), @@ -107,8 +105,9 @@ class SignedAgreementAccessAuditTable(tables.Table): """A table to show results from a SignedAgreementAccessAudit instance.""" signed_agreement = tables.Column(linkify=True) - agreement_group = tables.Column() - agreement_type = tables.Column() + agreement_group = tables.Column(accessor="signed_agreement__agreement_group") + agreement_type = tables.Column(accessor="signed_agreement__combined_type") + agreement_version = tables.Column(accessor="signed_agreement__version") note = tables.Column() action = tables.Column() @@ -127,118 +126,215 @@ class SignedAgreementAccessAudit: """Audit for Signed Agreements.""" # Access verified. - VALID_PRIMARY_CDSA = "Valid primary CDSA." - VALID_COMPONENT_CDSA = "Valid component CDSA." + ACTIVE_PRIMARY_AGREEMENT = "Active primary CDSA." + ACTIVE_COMPONENT_AGREEMENT = "Active component CDSA with active primary." # Allowed reasons for no access. - NO_PRIMARY_CDSA = "No primary CDSA for this group exists." + INACTIVE_AGREEMENT = "CDSA is inactive." + # INVALID_AGREEMENT_VERSION = "CDSA version is not valid." + NO_PRIMARY_AGREEMENT = "No primary CDSA for this group exists." + 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 def __init__(self): # Store the CDSA group for auditing membership. - self.anvil_cdsa_group = ManagedGroup.objects.get( - name=settings.ANVIL_CDSA_GROUP_NAME - ) self.completed = False # Set up lists to hold audit results. self.verified = [] self.needs_action = [] self.errors = [] - # Audit a single signed agreement. - def _audit_signed_agreement(self, signed_agreement): - # Check if the access group is in the overall CDSA group. - in_cdsa_group = GroupGroupMembership.objects.filter( - parent_group=self.anvil_cdsa_group, - child_group=signed_agreement.anvil_access_group, - ).exists() - # Primary agreements don't need to check components. - if signed_agreement.is_primary and in_cdsa_group: - self.verified.append( - VerifiedAccess( - signed_agreement=signed_agreement, - note=self.VALID_PRIMARY_CDSA, - ) - ) - return - elif signed_agreement.is_primary and not in_cdsa_group: - self.needs_action.append( - GrantAccess( - signed_agreement=signed_agreement, - note=self.VALID_PRIMARY_CDSA, - ) - ) - return - elif not signed_agreement.is_primary: - # component agreements need to check for a primary. - if hasattr(signed_agreement, "memberagreement"): - # Member - primary_exists = models.MemberAgreement.objects.filter( - signed_agreement__is_primary=True, - study_site=signed_agreement.memberagreement.study_site, - ).exists() - elif hasattr(signed_agreement, "dataaffiliateagreement"): - # Data affiliate - primary_exists = models.DataAffiliateAgreement.objects.filter( - signed_agreement__is_primary=True, - study=signed_agreement.dataaffiliateagreement.study, - ).exists() - elif hasattr(signed_agreement, "nondataaffiliateagreement"): - # Non-data affiliate should not have components so this is an error. - raise RuntimeError( - "Non data affiliates should always be a primary CDSA." - ) - else: - # Some other case happened - log it as an error. - self.errors.append( - OtherError( - signed_agreement=signed_agreement, note=self.ERROR_OTHER_CASE - ) - ) - return + def _audit_primary_agreement(self, signed_agreement): + """Audit a single component signed agreement. + + The following items are checked: + * if the primary agreement is active. + * if the primary agreement is in the CDSA group. + + This audit does *not* check if the AgreementMajorVersion associated with the SignedAgreement is valid. + """ + in_cdsa_group = signed_agreement.is_in_cdsa_group() + is_active = ( + signed_agreement.status == models.SignedAgreement.StatusChoices.ACTIVE + ) - # Now check access for the component given the primary agreement. - if primary_exists and in_cdsa_group: + if is_active: + if in_cdsa_group: self.verified.append( VerifiedAccess( signed_agreement=signed_agreement, - note=self.VALID_COMPONENT_CDSA, + note=self.ACTIVE_PRIMARY_AGREEMENT, ) ) return - elif primary_exists and not in_cdsa_group: + else: self.needs_action.append( GrantAccess( signed_agreement=signed_agreement, - note=self.VALID_COMPONENT_CDSA, + note=self.ACTIVE_PRIMARY_AGREEMENT, ) ) return - elif not primary_exists and not in_cdsa_group: + else: + if in_cdsa_group: + self.needs_action.append( + RemoveAccess( + signed_agreement=signed_agreement, + note=self.INACTIVE_AGREEMENT, + ) + ) + return + else: self.verified.append( VerifiedNoAccess( signed_agreement=signed_agreement, - note=self.NO_PRIMARY_CDSA, + note=self.INACTIVE_AGREEMENT, ) ) return - elif not primary_exists and in_cdsa_group: + + # If we made it this far in audit, some other case happened - log it as an error. + # Haven't figured out a test for this because it is unexpected. + self.errors.append( # pragma: no cover + OtherError( + signed_agreement=signed_agreement, note=self.ERROR_OTHER_CASE + ) # pragma: no cover + ) # pragma: no cover + + def _audit_component_agreement(self, signed_agreement): + """Audit a single component signed agreement. + + The following items are checked: + * If a primary agreement exists + # If the primary agreement is active + * if the component agreement is active + * if the component agreement is in the CDSA group + + This audit does *not* check if the AgreementMajorVersion associated with either the + SignedAgreement or its component is valid. + """ + in_cdsa_group = signed_agreement.is_in_cdsa_group() + is_active = ( + signed_agreement.status == models.SignedAgreement.StatusChoices.ACTIVE + ) + + # Get the set of potential primary agreements for this component. + if hasattr(signed_agreement, "memberagreement"): + # Member + primary_qs = models.SignedAgreement.objects.filter( + 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__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, + ).exists() + + if primary_exists: + if primary_active: + if is_active: + if in_cdsa_group: + self.verified.append( + VerifiedAccess( + signed_agreement=signed_agreement, + note=self.ACTIVE_COMPONENT_AGREEMENT, + ) + ) + return + else: + self.needs_action.append( + GrantAccess( + signed_agreement=signed_agreement, + note=self.ACTIVE_COMPONENT_AGREEMENT, + ) + ) + return + else: + if in_cdsa_group: + self.needs_action.append( + RemoveAccess( + signed_agreement=signed_agreement, + note=self.INACTIVE_AGREEMENT, + ) + ) + return + else: + self.verified.append( + VerifiedNoAccess( + signed_agreement=signed_agreement, + note=self.INACTIVE_AGREEMENT, + ) + ) + return + else: + if in_cdsa_group: + self.needs_action.append( + RemoveAccess( + signed_agreement=signed_agreement, + note=self.PRIMARY_NOT_ACTIVE, + ) + ) + return + else: + self.verified.append( + VerifiedNoAccess( + signed_agreement=signed_agreement, + note=self.PRIMARY_NOT_ACTIVE, + ) + ) + return + else: + if in_cdsa_group: self.errors.append( RemoveAccess( signed_agreement=signed_agreement, - note=self.NO_PRIMARY_CDSA, + note=self.NO_PRIMARY_AGREEMENT, + ) + ) + return + else: + self.verified.append( + VerifiedNoAccess( + signed_agreement=signed_agreement, + note=self.NO_PRIMARY_AGREEMENT, ) ) return # If we made it this far in audit, some other case happened - log it as an error. - self.errors.append( - OtherError(signed_agreement=signed_agreement, note=self.ERROR_OTHER_CASE) - ) + # Haven't figured out a test for this because it is unexpected. + self.errors.append( # pragma: no cover + OtherError( + signed_agreement=signed_agreement, note=self.ERROR_OTHER_CASE + ) # pragma: no cover + ) # pragma: no cover + + def _audit_signed_agreement(self, signed_agreement): + if signed_agreement.is_primary: + self._audit_primary_agreement(signed_agreement) + else: + self._audit_component_agreement(signed_agreement) def run_audit(self): """Run an audit on all SignedAgreements.""" diff --git a/primed/cdsa/audit/workspace_audit.py b/primed/cdsa/audit/workspace_audit.py index 2d7d9267..e6ac2fc4 100644 --- a/primed/cdsa/audit/workspace_audit.py +++ b/primed/cdsa/audit/workspace_audit.py @@ -107,6 +107,9 @@ class WorkspaceAccessAuditTable(tables.Table): workspace = tables.Column(linkify=True) data_affiliate_agreement = tables.Column(linkify=True) + agreement_version = tables.Column( + accessor="data_affiliate_agreement__signed_agreement__version" + ) note = tables.Column() action = tables.Column() @@ -125,10 +128,11 @@ class WorkspaceAccessAudit: """Audit for CDSA Workspaces.""" # Access verified. - VALID_PRIMARY_CDSA = "Valid primary CDSA." + ACTIVE_PRIMARY_AGREEMENT = "Active primary CDSA." # Allowed reasons for no access. - NO_PRIMARY_CDSA = "No primary CDSA for this study exists." + NO_PRIMARY_AGREEMENT = "No primary CDSA for this study." + INACTIVE_PRIMARY_AGREEMENT = "Primary CDSA for this study is inactive." # Other errors ERROR_OTHER_CASE = "Workspace did not match any expected situations." @@ -146,7 +150,6 @@ def __init__(self): self.needs_action = [] self.errors = [] - # Audit a single signed agreement. def _audit_workspace(self, workspace): # Check if the access group is in the overall CDSA group. auth_domain = workspace.workspace.authorization_domains.first() @@ -154,47 +157,76 @@ def _audit_workspace(self, workspace): parent_group=auth_domain, child_group=self.anvil_cdsa_group, ).exists() - # WRITE ME! - # See if there is a primary data affiliate agreement for this study. - try: - primary_agreement = models.DataAffiliateAgreement.objects.get( - study=workspace.study, - signed_agreement__is_primary=True, + primary_qs = models.DataAffiliateAgreement.objects.filter( + study=workspace.study, signed_agreement__is_primary=True + ) + primary_exists = primary_qs.exists() + + if primary_exists: + primary_agreement = ( + primary_qs.filter( + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE, + ) + .order_by( + "-signed_agreement__version__major_version__version", + "-signed_agreement__version__minor_version", + ) + .first() ) - if has_cdsa_group_in_auth_domain: - self.verified.append( - VerifiedAccess( - workspace=workspace, - data_affiliate_agreement=primary_agreement, - note=self.VALID_PRIMARY_CDSA, + if primary_agreement: + if has_cdsa_group_in_auth_domain: + self.verified.append( + VerifiedAccess( + workspace=workspace, + data_affiliate_agreement=primary_agreement, + note=self.ACTIVE_PRIMARY_AGREEMENT, + ) ) - ) - return + return + else: + self.needs_action.append( + GrantAccess( + workspace=workspace, + data_affiliate_agreement=primary_agreement, + note=self.ACTIVE_PRIMARY_AGREEMENT, + ) + ) + return else: - self.needs_action.append( - GrantAccess( - workspace=workspace, - data_affiliate_agreement=primary_agreement, - note=self.VALID_PRIMARY_CDSA, + if has_cdsa_group_in_auth_domain: + self.needs_action.append( + RemoveAccess( + workspace=workspace, + data_affiliate_agreement=primary_agreement, + note=self.INACTIVE_PRIMARY_AGREEMENT, + ) ) - ) - return - except models.DataAffiliateAgreement.DoesNotExist: - if not has_cdsa_group_in_auth_domain: - self.verified.append( - VerifiedNoAccess( + return + else: + self.verified.append( + VerifiedNoAccess( + workspace=workspace, + data_affiliate_agreement=primary_agreement, + note=self.INACTIVE_PRIMARY_AGREEMENT, + ) + ) + return + else: + if has_cdsa_group_in_auth_domain: + self.errors.append( + RemoveAccess( workspace=workspace, data_affiliate_agreement=None, - note=self.NO_PRIMARY_CDSA, + note=self.NO_PRIMARY_AGREEMENT, ) ) return else: - self.errors.append( - RemoveAccess( + self.verified.append( + VerifiedNoAccess( workspace=workspace, data_affiliate_agreement=None, - note=self.NO_PRIMARY_CDSA, + note=self.NO_PRIMARY_AGREEMENT, ) ) return diff --git a/primed/cdsa/forms.py b/primed/cdsa/forms.py index dde759cd..678991df 100644 --- a/primed/cdsa/forms.py +++ b/primed/cdsa/forms.py @@ -10,6 +10,15 @@ from . import models +class AgreementMajorVersionIsValidForm(forms.ModelForm): + class Meta: + model = models.AgreementMajorVersion + fields = ("is_valid",) + widgets = { + "is_valid": forms.HiddenInput, + } + + class SignedAgreementForm(Bootstrap5MediaFormMixin, forms.ModelForm): """Form for a SignedAgreement object.""" @@ -19,6 +28,9 @@ class SignedAgreementForm(Bootstrap5MediaFormMixin, forms.ModelForm): widget=forms.RadioSelect, label="Agreement type", ) + version = forms.ModelChoiceField( + queryset=models.AgreementVersion.objects.filter(major_version__is_valid=True) + ) class Meta: model = models.SignedAgreement @@ -40,6 +52,16 @@ class Meta: } +class SignedAgreementStatusForm(forms.ModelForm): + """Form to update the status of a SignedAgreement.""" + + class Meta: + model = models.SignedAgreement + fields = ("status",) + help_texts = {"status": """The status of this Signed Agreement."""} + widgets = {"status": forms.RadioSelect} + + class MemberAgreementForm(forms.ModelForm): class Meta: model = models.MemberAgreement diff --git a/primed/cdsa/helpers.py b/primed/cdsa/helpers.py index 945185fc..5f613969 100644 --- a/primed/cdsa/helpers.py +++ b/primed/cdsa/helpers.py @@ -5,23 +5,34 @@ def get_representative_records_table(): """Return the queryset for representative records.""" - qs = models.SignedAgreement.objects.all() + qs = models.SignedAgreement.active.all() return tables.RepresentativeRecordsTable(qs) def get_study_records_table(): """Return the queryset for study records.""" - qs = models.DataAffiliateAgreement.objects.filter(signed_agreement__is_primary=True) + qs = models.DataAffiliateAgreement.objects.filter( + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE, + signed_agreement__is_primary=True, + ) return tables.StudyRecordsTable(qs) def get_user_access_records_table(): """Return the queryset for user access records.""" - qs = GroupAccountMembership.objects.filter(group__signedagreement__isnull=False) + qs = GroupAccountMembership.objects.filter( + group__signedagreement__status=models.SignedAgreement.StatusChoices.ACTIVE, + group__signedagreement__isnull=False, + ) return tables.UserAccessRecordsTable(qs) def get_cdsa_workspace_records_table(): """Return the queryset for workspace records.""" - qs = models.CDSAWorkspace.objects.all() + active_data_affiliates = models.DataAffiliateAgreement.objects.filter( + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE, + ) + qs = models.CDSAWorkspace.objects.filter( + study__dataaffiliateagreement__in=active_data_affiliates, + ) return tables.CDSAWorkspaceRecordsTable(qs) diff --git a/primed/cdsa/migrations/0002_agreementmajorversion_historicalagreementmajorversion.py b/primed/cdsa/migrations/0002_agreementmajorversion_historicalagreementmajorversion.py new file mode 100644 index 00000000..a23e069b --- /dev/null +++ b/primed/cdsa/migrations/0002_agreementmajorversion_historicalagreementmajorversion.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.19 on 2023-08-31 18:41 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields +import simple_history.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('cdsa', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AgreementMajorVersion', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('version', models.IntegerField(help_text='Major version of the CDSA. Changes to the major version require resigning.', unique=True, validators=[django.core.validators.MinValueValidator(1)])), + ], + options={ + 'get_latest_by': 'modified', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HistoricalAgreementMajorVersion', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('version', models.IntegerField(db_index=True, help_text='Major version of the CDSA. Changes to the major version require resigning.', validators=[django.core.validators.MinValueValidator(1)])), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical agreement major version', + 'verbose_name_plural': 'historical agreement major versions', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/primed/cdsa/migrations/0003_agreementversion_add_major_version_fk.py b/primed/cdsa/migrations/0003_agreementversion_add_major_version_fk.py new file mode 100644 index 00000000..e96af1f0 --- /dev/null +++ b/primed/cdsa/migrations/0003_agreementversion_add_major_version_fk.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.19 on 2023-08-31 18:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cdsa', '0002_agreementmajorversion_historicalagreementmajorversion'), + ] + + operations = [ + migrations.AddField( + model_name='agreementversion', + name='major_version_fk', + field=models.ForeignKey(blank=True, help_text='Major version of the CDSA. Changes to the major version require resigning.', null=True, on_delete=django.db.models.deletion.CASCADE, to='cdsa.agreementmajorversion'), + ), + migrations.AddField( + model_name='historicalagreementversion', + name='major_version_fk', + field=models.ForeignKey(blank=True, db_constraint=False, help_text='Major version of the CDSA. Changes to the major version require resigning.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='cdsa.agreementmajorversion'), + ), + ] diff --git a/primed/cdsa/migrations/0004_populate_agreementmajorversion.py b/primed/cdsa/migrations/0004_populate_agreementmajorversion.py new file mode 100644 index 00000000..9c88b576 --- /dev/null +++ b/primed/cdsa/migrations/0004_populate_agreementmajorversion.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.19 on 2023-08-31 18:58 + +from django.db import migrations + + +def populate_agreementversion_major_version_fk(apps, schema_editor): + """Populate the AgreementMajorVersion model using major_versions in AgreementVersion, + and set AgreementVersion.major_version_fk.""" + AgreementVersion = apps.get_model("cdsa", "AgreementVersion") + AgreementMajorVersion = apps.get_model("cdsa", "AgreementMajorVersion") + for row in AgreementVersion.objects.all(): + # Get or create the AgreementMajorVersion object. + try: + major_version = AgreementMajorVersion.objects.get(version=row.major_version) + except AgreementMajorVersion.DoesNotExist: + major_version = AgreementMajorVersion( + version=row.major_version, + ) + major_version.full_clean() + major_version.save() + # Set major_version_fk for the agreement_version object. + row.major_version_fk = major_version + row.save(update_fields=["major_version_fk"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ('cdsa', '0003_agreementversion_add_major_version_fk'), + ] + + operations = [ + migrations.RunPython(populate_agreementversion_major_version_fk, reverse_code=migrations.RunPython.noop), + ] diff --git a/primed/cdsa/migrations/0005_agreementversion_remove_field_major_version.py b/primed/cdsa/migrations/0005_agreementversion_remove_field_major_version.py new file mode 100644 index 00000000..352c471e --- /dev/null +++ b/primed/cdsa/migrations/0005_agreementversion_remove_field_major_version.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.19 on 2023-08-31 22:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cdsa', '0004_populate_agreementmajorversion'), + ] + + operations = [ + migrations.AlterModelOptions( + name='agreementversion', + options={'get_latest_by': 'modified'}, + ), + migrations.RemoveConstraint( + model_name='agreementversion', + name='unique_agreement_version', + ), + migrations.RemoveField( + model_name='agreementversion', + name='major_version', + ), + migrations.RemoveField( + model_name='historicalagreementversion', + name='major_version', + ), + ] diff --git a/primed/cdsa/migrations/0006_agreementversion_rename_major_version_fk_to_major_version.py b/primed/cdsa/migrations/0006_agreementversion_rename_major_version_fk_to_major_version.py new file mode 100644 index 00000000..712457c3 --- /dev/null +++ b/primed/cdsa/migrations/0006_agreementversion_rename_major_version_fk_to_major_version.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.19 on 2023-08-31 22:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cdsa', '0005_agreementversion_remove_field_major_version'), + ] + + operations = [ + migrations.AlterModelOptions( + name='agreementversion', + options={'ordering': ['major_version', 'minor_version']}, + ), + migrations.RenameField( + model_name='agreementversion', + old_name='major_version_fk', + new_name='major_version', + ), + migrations.RenameField( + model_name='historicalagreementversion', + old_name='major_version_fk', + new_name='major_version', + ), + migrations.AddConstraint( + model_name='agreementversion', + constraint=models.UniqueConstraint(fields=('major_version', 'minor_version'), name='unique_agreement_version_2'), + ), + ] diff --git a/primed/cdsa/migrations/0007_alter_agreementversion_major_version.py b/primed/cdsa/migrations/0007_alter_agreementversion_major_version.py new file mode 100644 index 00000000..ced250a9 --- /dev/null +++ b/primed/cdsa/migrations/0007_alter_agreementversion_major_version.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.19 on 2023-08-31 23:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cdsa', '0006_agreementversion_rename_major_version_fk_to_major_version'), + ] + + operations = [ + migrations.AlterField( + model_name='agreementversion', + name='major_version', + field=models.ForeignKey(default=None, help_text='Major version of the CDSA. Changes to the major version require resigning.', on_delete=django.db.models.deletion.CASCADE, to='cdsa.agreementmajorversion'), + preserve_default=False, + ), + ] diff --git a/primed/cdsa/migrations/0008_agreementmajorversion_add_is_valid.py b/primed/cdsa/migrations/0008_agreementmajorversion_add_is_valid.py new file mode 100644 index 00000000..c315857c --- /dev/null +++ b/primed/cdsa/migrations/0008_agreementmajorversion_add_is_valid.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.19 on 2023-09-13 23:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cdsa', '0007_alter_agreementversion_major_version'), + ] + + operations = [ + migrations.AddField( + model_name='agreementmajorversion', + name='is_valid', + field=models.BooleanField(default=True, help_text='Boolean indicator of whether this version is valid.', verbose_name='Valid?'), + ), + migrations.AddField( + model_name='historicalagreementmajorversion', + name='is_valid', + field=models.BooleanField(default=True, help_text='Boolean indicator of whether this version is valid.', verbose_name='Valid?'), + ), + ] diff --git a/primed/cdsa/migrations/0009_signedagreement_add_field_status.py b/primed/cdsa/migrations/0009_signedagreement_add_field_status.py new file mode 100644 index 00000000..aa0a845c --- /dev/null +++ b/primed/cdsa/migrations/0009_signedagreement_add_field_status.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.19 on 2023-09-19 17:33 + +from django.db import migrations, models +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('cdsa', '0008_agreementmajorversion_add_is_valid'), + ] + + operations = [ + migrations.AddField( + model_name='historicalsignedagreement', + name='status', + field=model_utils.fields.StatusField(choices=[(0, 'dummy')], default='active', max_length=100, no_check_for_status=True, verbose_name='status'), + ), + migrations.AddField( + model_name='historicalsignedagreement', + name='status_changed', + field=model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed'), + ), + migrations.AddField( + model_name='signedagreement', + name='status', + field=model_utils.fields.StatusField(choices=[(0, 'dummy')], default='active', max_length=100, no_check_for_status=True, verbose_name='status'), + ), + migrations.AddField( + model_name='signedagreement', + name='status_changed', + field=model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed'), + ), + migrations.AlterField( + model_name='agreementmajorversion', + name='is_valid', + field=models.BooleanField(default=True, help_text='Boolean indicator of whether this version is valid.'), + ), + migrations.AlterField( + model_name='historicalagreementmajorversion', + name='is_valid', + field=models.BooleanField(default=True, help_text='Boolean indicator of whether this version is valid.'), + ), + ] diff --git a/primed/cdsa/models.py b/primed/cdsa/models.py index 4605f305..5dd8a053 100644 --- a/primed/cdsa/models.py +++ b/primed/cdsa/models.py @@ -2,25 +2,57 @@ from datetime import date -from anvil_consortium_manager.models import BaseWorkspaceData, ManagedGroup +from anvil_consortium_manager.models import ( + BaseWorkspaceData, + GroupGroupMembership, + ManagedGroup, +) from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models from django.urls import reverse from django_extensions.db.models import TimeStampedModel +from model_utils.models import StatusModel from simple_history.models import HistoricalRecords from primed.duo.models import DataUseOntologyModel from primed.primed_anvil.models import AvailableData, RequesterModel, Study, StudySite +class AgreementMajorVersion(TimeStampedModel, models.Model): + """A model for a major agreement version.""" + + version = models.IntegerField( + help_text="Major version of the CDSA. Changes to the major version require resigning.", + validators=[MinValueValidator(1)], + unique=True, + ) + is_valid = models.BooleanField( + default=True, help_text="Boolean indicator of whether this version is valid." + ) + + history = HistoricalRecords() + + def __str__(self): + return "v{}".format(self.version) + + def get_absolute_url(self): + return reverse( + "cdsa:agreement_versions:major_version_detail", + kwargs={ + "major_version": self.version, + }, + ) + + class AgreementVersion(TimeStampedModel, models.Model): """Model to track approved CDSA versions.""" - major_version = models.IntegerField( + major_version = models.ForeignKey( + AgreementMajorVersion, help_text="Major version of the CDSA. Changes to the major version require resigning.", - validators=[MinValueValidator(1)], + on_delete=models.CASCADE, ) minor_version = models.IntegerField( help_text="Minor version of the CDSA. Changes to the minor version do not require resigning.", @@ -37,7 +69,7 @@ class Meta: constraints = [ models.UniqueConstraint( fields=["major_version", "minor_version"], - name="unique_agreement_version", + name="unique_agreement_version_2", ) ] ordering = ["major_version", "minor_version"] @@ -45,12 +77,43 @@ class Meta: def __str__(self): return self.full_version + def get_absolute_url(self): + return reverse( + "cdsa:agreement_versions:detail", + kwargs={ + "major_version": self.major_version.version, + "minor_version": self.minor_version, + }, + ) + @property def full_version(self): - return "v{}.{}".format(self.major_version, self.minor_version) + return "{}.{}".format(str(self.major_version), self.minor_version) + + +class SignedAgreementStatusMixin: + """Mixin to define status choices for SignedAgreements.""" + # This is required because we are using django-model-util's StatusModel with django-simple-history: + # See GitHub issue: https://github.com/jazzband/django-simple-history/issues/190 -class SignedAgreement(TimeStampedModel, models.Model): + class StatusChoices(models.TextChoices): + ACTIVE = "active", "Active" + """SignedAgreements that are currently active.""" + + WITHDRAWN = "withdrawn", "Withdrawn" + """SignedAgreements that have been withdrawn for some reason (e.g., PI changed institution, + study no longer wanted to participate.)""" + + LAPSED = "lapsed", "Lapsed" + """SignedAgreements from a AgreementMajorVersion that is no longer valid.""" + + STATUS = StatusChoices.choices + + +class SignedAgreement( + TimeStampedModel, SignedAgreementStatusMixin, StatusModel, models.Model +): """Model to track verified, signed consortium data sharing agreements.""" MEMBER = "member" @@ -107,7 +170,7 @@ class SignedAgreement(TimeStampedModel, models.Model): on_delete=models.PROTECT, ) - history = HistoricalRecords() + history = HistoricalRecords(bases=[SignedAgreementStatusMixin, models.Model]) def __str__(self): return "{}".format(self.cc_id) @@ -140,6 +203,13 @@ def get_agreement_type(self): def agreement_group(self): return self.get_agreement_type().get_agreement_group() + def is_in_cdsa_group(self): + anvil_cdsa_group = ManagedGroup.objects.get(name=settings.ANVIL_CDSA_GROUP_NAME) + return GroupGroupMembership.objects.filter( + parent_group=anvil_cdsa_group, + child_group=self.anvil_access_group, + ).exists() + class AgreementTypeModel(models.Model): """An abstract model that can be used to provide required fields for agreement type models.""" @@ -185,7 +255,7 @@ class MemberAgreement(TimeStampedModel, AgreementTypeModel, models.Model): def get_absolute_url(self): return reverse( - "cdsa:agreements:members:detail", + "cdsa:signed_agreements:members:detail", kwargs={"cc_id": self.signed_agreement.cc_id}, ) @@ -207,7 +277,7 @@ class DataAffiliateAgreement(TimeStampedModel, AgreementTypeModel, models.Model) def get_absolute_url(self): return reverse( - "cdsa:agreements:data_affiliates:detail", + "cdsa:signed_agreements:data_affiliates:detail", kwargs={"cc_id": self.signed_agreement.cc_id}, ) @@ -226,7 +296,7 @@ class NonDataAffiliateAgreement(TimeStampedModel, AgreementTypeModel, models.Mod def get_absolute_url(self): return reverse( - "cdsa:agreements:non_data_affiliates:detail", + "cdsa:signed_agreements:non_data_affiliates:detail", kwargs={"cc_id": self.signed_agreement.cc_id}, ) diff --git a/primed/cdsa/tables.py b/primed/cdsa/tables.py index 2571fb39..856fb643 100644 --- a/primed/cdsa/tables.py +++ b/primed/cdsa/tables.py @@ -8,13 +8,33 @@ ) from primed.primed_anvil.tables import ( - BooleanCheckColumn, + BooleanIconColumn, WorkspaceSharedWithConsortiumTable, ) from . import models +class AgreementVersionTable(tables.Table): + + major_version = tables.Column(linkify=True) + full_version = tables.Column( + linkify=True, order_by=("major_version", "minor_version") + ) + major_version__is_valid = BooleanIconColumn( + verbose_name="Valid?", show_false_icon=True + ) + + class Meta: + model = models.AgreementVersion + fields = ( + "major_version", + "full_version", + "major_version__is_valid", + "date_approved", + ) + + class SignedAgreementTable(tables.Table): cc_id = tables.Column(linkify=True) @@ -35,6 +55,7 @@ class SignedAgreementTable(tables.Table): if hasattr(record.agreement_group, "get_absolute_url") else None ) + version = tables.Column(linkify=True) class Meta: model = models.SignedAgreement @@ -46,6 +67,7 @@ class Meta: "agreement_group", "agreement_type", "version", + "status", "date_signed", "number_accessors", ) @@ -57,7 +79,7 @@ class MemberAgreementTable(tables.Table): signed_agreement__cc_id = tables.Column(linkify=True) study_site = tables.Column(linkify=True) - signed_agreement__is_primary = BooleanCheckColumn(verbose_name="Primary?") + signed_agreement__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", @@ -67,6 +89,7 @@ class MemberAgreementTable(tables.Table): verbose_name="Number of accessors", accessor="signed_agreement__anvil_access_group__groupaccountmembership_set__count", ) + signed_agreement__version = tables.Column(linkify=True) class Meta: model = models.MemberAgreement @@ -78,6 +101,7 @@ class Meta: "signed_agreement__representative_role", "signed_agreement__signing_institution", "signed_agreement__version", + "signed_agreement__status", "signed_agreement__date_signed", "number_accessors", ) @@ -89,7 +113,7 @@ class DataAffiliateAgreementTable(tables.Table): signed_agreement__cc_id = tables.Column(linkify=True) study = tables.Column(linkify=True) - signed_agreement__is_primary = BooleanCheckColumn(verbose_name="Primary?") + signed_agreement__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", @@ -99,6 +123,7 @@ class DataAffiliateAgreementTable(tables.Table): verbose_name="Number of accessors", accessor="signed_agreement__anvil_access_group__groupaccountmembership_set__count", ) + signed_agreement__version = tables.Column(linkify=True) class Meta: model = models.DataAffiliateAgreement @@ -110,6 +135,7 @@ class Meta: "signed_agreement__representative_role", "signed_agreement__signing_institution", "signed_agreement__version", + "signed_agreement__status", "signed_agreement__date_signed", "number_accessors", ) @@ -120,7 +146,6 @@ class NonDataAffiliateAgreementTable(tables.Table): """Table to display `DataAffiliateAgreement` objects.""" signed_agreement__cc_id = tables.Column(linkify=True) - signed_agreement__is_primary = BooleanCheckColumn() signed_agreement__representative__name = tables.Column( linkify=lambda record: record.signed_agreement.representative.get_absolute_url(), verbose_name="Representative", @@ -130,6 +155,7 @@ class NonDataAffiliateAgreementTable(tables.Table): verbose_name="Number of accessors", accessor="signed_agreement__anvil_access_group__groupaccountmembership_set__count", ) + signed_agreement__version = tables.Column(linkify=True) class Meta: model = models.NonDataAffiliateAgreement @@ -140,6 +166,7 @@ class Meta: "signed_agreement__representative_role", "signed_agreement__signing_institution", "signed_agreement__version", + "signed_agreement__status", "signed_agreement__date_signed", "number_accessors", ) @@ -162,6 +189,8 @@ class Meta: "representative_role", "signing_institution", "signing_group", + "agreement_type", + "version", ) order_by = ("representative__name",) diff --git a/primed/cdsa/tests/factories.py b/primed/cdsa/tests/factories.py index e84ac6d5..bf0459b8 100644 --- a/primed/cdsa/tests/factories.py +++ b/primed/cdsa/tests/factories.py @@ -14,13 +14,23 @@ from .. import models +class AgreementMajorVersionFactory(DjangoModelFactory): + """A factory for the AgreementMajorVersion model.""" + + class Meta: + model = models.AgreementMajorVersion + django_get_or_create = ("version",) + + version = Faker("random_int", min=1) + + class AgreementVersionFactory(DjangoModelFactory): """A factory for the AgreementVersion model.""" class Meta: model = models.AgreementVersion - major_version = Faker("random_int", min=1) + major_version = SubFactory(AgreementMajorVersionFactory) minor_version = Faker("random_int") date_approved = Faker("date") diff --git a/primed/cdsa/tests/test_audit.py b/primed/cdsa/tests/test_audit.py index 641e91af..9a85e2e5 100644 --- a/primed/cdsa/tests/test_audit.py +++ b/primed/cdsa/tests/test_audit.py @@ -10,6 +10,7 @@ from primed.primed_anvil.tests.factories import StudyFactory, StudySiteFactory +from .. import models from ..audit import signed_agreement_audit, workspace_audit from . import factories @@ -117,238 +118,1171 @@ def test_no_signed_agreements(self): self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) - def test_one_signed_agreement_primary_verified_access(self): - signed_agreement = factories.SignedAgreementFactory.create() + def test_loops_over_signed_agreements(self): + """run_audit loops over all signed agreements.""" + # Create two signed agreements that need to be added to the SAG group. + factories.MemberAgreementFactory.create_batch(2) + cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() + cdsa_audit.run_audit() + self.assertEqual(len(cdsa_audit.needs_action), 2) + + def test_member_primary_in_group(self): + """Member primary agreement with valid version in CDSA group.""" + this_agreement = factories.MemberAgreementFactory.create() # Add the signed agreement access group to the CDSA group. GroupGroupMembershipFactory.create( parent_group=self.cdsa_group, - child_group=signed_agreement.anvil_access_group, + child_group=this_agreement.signed_agreement.anvil_access_group, ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit.run_audit() + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) record = cdsa_audit.verified[0] self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) - self.assertEqual(record.signed_agreement, signed_agreement) - self.assertEqual(record.note, cdsa_audit.VALID_PRIMARY_CDSA) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_member_primary_not_in_group(self): + """Member primary agreement with valid version not in CDSA group.""" + this_agreement = factories.MemberAgreementFactory.create() + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_member_primary_invalid_version_in_group(self): + """Member primary agreement, active, with invalid version in CDSA group.""" + this_agreement = factories.MemberAgreementFactory.create( + signed_agreement__version__major_version__is_valid=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), 1) self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) - def test_one_signed_agreement_primary_needs_access(self): - signed_agreement = factories.SignedAgreementFactory.create() + def test_member_primary_invalid_version_not_in_group(self): + """Member primary agreement, with invalid version, not in CDSA group.""" + this_agreement = factories.MemberAgreementFactory.create( + signed_agreement__version__major_version__is_valid=False + ) # Do not add the signed agreement access group to the CDSA group. # GroupGroupMembershipFactory.create( - # parent_group=self.cdsa_group, child_group=signed_agreement.anvil_access_group + # parent_group=self.cdsa_group, + # child_group=this_agreement.signed_agreement.anvil_access_group, # ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit.run_audit() + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) self.assertEqual(len(cdsa_audit.verified), 0) self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) record = cdsa_audit.needs_action[0] self.assertIsInstance(record, signed_agreement_audit.GrantAccess) - self.assertEqual(record.signed_agreement, signed_agreement) - self.assertEqual(record.note, cdsa_audit.VALID_PRIMARY_CDSA) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_member_primary_valid_not_active_in_group(self): + """Member primary agreement with valid version but isn't active, in CDSA group.""" + this_agreement = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.RemoveAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT) + + def test_member_primary_valid_not_active_not_in_group(self): + """Member primary agreement with valid version but isn't active, not in CDSA group.""" + this_agreement = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + # # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT) - def test_one_signed_agreement_member_component_verified_access(self): + def test_member_component_has_primary_in_group(self): + """Member component agreement, with valid version, with primary with valid version, in CDSA group.""" study_site = StudySiteFactory.create() factories.MemberAgreementFactory.create(study_site=study_site) - component_agreement = factories.MemberAgreementFactory.create( + this_agreement = factories.MemberAgreementFactory.create( signed_agreement__is_primary=False, study_site=study_site ) - # Add the component agreement access group to the CDSA group. + # Add the signed agreement access group to the CDSA group. GroupGroupMembershipFactory.create( parent_group=self.cdsa_group, - child_group=component_agreement.signed_agreement.anvil_access_group, + child_group=this_agreement.signed_agreement.anvil_access_group, ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit._audit_signed_agreement(component_agreement.signed_agreement) + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) record = cdsa_audit.verified[0] self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) - self.assertEqual(record.signed_agreement, component_agreement.signed_agreement) - self.assertEqual(record.note, cdsa_audit.VALID_COMPONENT_CDSA) - self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_member_component_has_primary_not_in_group(self): + """Member component agreement, with valid version, with primary with valid version, not in CDSA group.""" + 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 + ) + # # 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), 1) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) - def test_one_signed_agreement_member_component_no_primary_no_access(self): - component_agreement = factories.MemberAgreementFactory.create( - signed_agreement__is_primary=False + def test_member_component_inactive_has_primary_in_group(self): + """Member component agreement, inactive, with valid version, with primary with valid version, in CDSA group.""" + 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, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.RemoveAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT) + + def test_member_component_inactive_has_primary_not_in_group(self): + """Member component agreement, inactive, with valid version, with valid active primary, not in CDSA group.""" + 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, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, ) - # Do not add the component agreement access group to the CDSA group. + # # Add the signed agreement access group to the CDSA group. # GroupGroupMembershipFactory.create( # parent_group=self.cdsa_group, - # child_group=component_agreement.signed_agreement.anvil_access_group + # child_group=this_agreement.signed_agreement.anvil_access_group, # ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit._audit_signed_agreement(component_agreement.signed_agreement) + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) record = cdsa_audit.verified[0] self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) - self.assertEqual(record.signed_agreement, component_agreement.signed_agreement) - self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_CDSA) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT) + + def test_member_component_has_primary_with_invalid_version_in_group(self): + """Member component agreement, with valid version, with active primary with invalid version, in CDSA group.""" + study_site = StudySiteFactory.create() + factories.MemberAgreementFactory.create( + study_site=study_site, + signed_agreement__version__major_version__is_valid=False, + ) + this_agreement = factories.MemberAgreementFactory.create( + signed_agreement__is_primary=False, study_site=study_site + ) + # 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), 1) self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) - def test_one_signed_agreement_member_component_needs_access(self): + def test_member_component_has_primary_with_invalid_version_not_in_group(self): + """Member component agreement, with valid version, with active primary with invalid version, not in group.""" study_site = StudySiteFactory.create() - factories.MemberAgreementFactory.create(study_site=study_site) - component_agreement = factories.MemberAgreementFactory.create( + factories.MemberAgreementFactory.create( + study_site=study_site, + signed_agreement__version__major_version__is_valid=False, + ) + this_agreement = factories.MemberAgreementFactory.create( signed_agreement__is_primary=False, study_site=study_site ) - # Do not add the component agreement access group to the CDSA group. + # # Add the signed agreement access group to the CDSA group. # GroupGroupMembershipFactory.create( # parent_group=self.cdsa_group, - # child_group=component_agreement.signed_agreement.anvil_access_group + # child_group=this_agreement.signed_agreement.anvil_access_group, # ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit._audit_signed_agreement(component_agreement.signed_agreement) + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) self.assertEqual(len(cdsa_audit.verified), 0) self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) record = cdsa_audit.needs_action[0] self.assertIsInstance(record, signed_agreement_audit.GrantAccess) - self.assertEqual(record.signed_agreement, component_agreement.signed_agreement) - self.assertEqual(record.note, cdsa_audit.VALID_COMPONENT_CDSA) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_member_component_has_inactive_primary_in_group(self): + """Member component agreement, with valid version, with inactive primary, in CDSA group.""" + study_site = StudySiteFactory.create() + factories.MemberAgreementFactory.create( + study_site=study_site, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + this_agreement = factories.MemberAgreementFactory.create( + signed_agreement__is_primary=False, study_site=study_site + ) + # 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), 1) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.RemoveAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.PRIMARY_NOT_ACTIVE) - def test_one_signed_agreement_member_component_needs_error(self): - # No primary, but the agreement has access (incorrectly). - component_agreement = factories.MemberAgreementFactory.create( - signed_agreement__is_primary=False + def test_member_component_has_inactive_primary_not_in_group(self): + """Member component agreement, with valid version, with inactive primary, not in CDSA group.""" + study_site = StudySiteFactory.create() + factories.MemberAgreementFactory.create( + study_site=study_site, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + this_agreement = factories.MemberAgreementFactory.create( + signed_agreement__is_primary=False, study_site=study_site + ) + # # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.PRIMARY_NOT_ACTIVE) + + 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 ) - # Add the component agreement access group to the CDSA group (to trip the error). + # Add the signed agreement access group to the CDSA group. GroupGroupMembershipFactory.create( parent_group=self.cdsa_group, - child_group=component_agreement.signed_agreement.anvil_access_group, + child_group=this_agreement.signed_agreement.anvil_access_group, ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit._audit_signed_agreement(component_agreement.signed_agreement) + 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.RemoveAccess) - self.assertEqual(record.signed_agreement, component_agreement.signed_agreement) - self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_CDSA) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) - def test_one_signed_agreement_data_affiliate_component_verified_access(self): - study = StudyFactory.create() - factories.DataAffiliateAgreementFactory.create(study=study) - component_agreement = factories.DataAffiliateAgreementFactory.create( - signed_agreement__is_primary=False, study=study + 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 ) - # Add the component agreement access group to the CDSA group. + # # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) + + def test_member_component_invalid_version_has_primary_in_group(self): + """Member component agreement, with invalid version, with a valid primary, in CDSA group.""" + 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, + signed_agreement__version__major_version__is_valid=False, + ) + # Add the signed agreement access group to the CDSA group. GroupGroupMembershipFactory.create( parent_group=self.cdsa_group, - child_group=component_agreement.signed_agreement.anvil_access_group, + child_group=this_agreement.signed_agreement.anvil_access_group, ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit._audit_signed_agreement(component_agreement.signed_agreement) + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) self.assertEqual(len(cdsa_audit.verified), 1) - record = cdsa_audit.verified[0] - self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) - self.assertEqual(record.signed_agreement, component_agreement.signed_agreement) - self.assertEqual(record.note, cdsa_audit.VALID_COMPONENT_CDSA) self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) - def test_one_signed_agreement_data_affiliate_component_no_primary_no_access(self): - component_agreement = factories.DataAffiliateAgreementFactory.create( - signed_agreement__is_primary=False + def test_member_component_invalid_version_has_primary_not_in_group(self): + """Member component agreement, with invalid version, with a valid primary, not in CDSA group.""" + 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, + signed_agreement__version__major_version__is_valid=False, ) - # Do not add the component agreement access group to the CDSA group. + # # Add the signed agreement access group to the CDSA group. # GroupGroupMembershipFactory.create( # parent_group=self.cdsa_group, - # child_group=component_agreement.signed_agreement.anvil_access_group + # child_group=this_agreement.signed_agreement.anvil_access_group, # ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit._audit_signed_agreement(component_agreement.signed_agreement) + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) + self.assertEqual(len(cdsa_audit.verified), 0) + self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_member_component_invalid_version_has_primary_with_invalid_version_in_group( + self, + ): + """Member component agreement, with invalid version, with an invalid primary, in CDSA group.""" + 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, + signed_agreement__version__major_version__is_valid=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), 1) - record = cdsa_audit.verified[0] - self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) - self.assertEqual(record.signed_agreement, component_agreement.signed_agreement) - self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_CDSA) self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) - def test_one_signed_agreement_data_affiliate_component_needs_access(self): - study = StudyFactory.create() - factories.DataAffiliateAgreementFactory.create(study=study) - component_agreement = factories.DataAffiliateAgreementFactory.create( - signed_agreement__is_primary=False, study=study + def test_member_component_invalid_version_has_primary_with_invalid_version_not_in_group( + self, + ): + """Member component agreement, with invalid version, with an invalid primary, not in CDSA group.""" + 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, + signed_agreement__version__major_version__is_valid=False, ) - # Do not add the component agreement access group to the CDSA group. + # # Add the signed agreement access group to the CDSA group. # GroupGroupMembershipFactory.create( # parent_group=self.cdsa_group, - # child_group=component_agreement.signed_agreement.anvil_access_group + # child_group=this_agreement.signed_agreement.anvil_access_group, # ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit._audit_signed_agreement(component_agreement.signed_agreement) + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) self.assertEqual(len(cdsa_audit.verified), 0) self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) record = cdsa_audit.needs_action[0] self.assertIsInstance(record, signed_agreement_audit.GrantAccess) - self.assertEqual(record.signed_agreement, component_agreement.signed_agreement) - self.assertEqual(record.note, cdsa_audit.VALID_COMPONENT_CDSA) - self.assertEqual(len(cdsa_audit.errors), 0) - - def test_one_signed_agreement_data_affiliate_component_needs_error(self): - # No primary, but the agreement has access (incorrectly). - component_agreement = factories.DataAffiliateAgreementFactory.create( - signed_agreement__is_primary=False + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + 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, + signed_agreement__version__major_version__is_valid=False, ) - # Add the component agreement access group to the CDSA group (to trip the error). + # Add the signed agreement access group to the CDSA group. GroupGroupMembershipFactory.create( parent_group=self.cdsa_group, - child_group=component_agreement.signed_agreement.anvil_access_group, + child_group=this_agreement.signed_agreement.anvil_access_group, ) cdsa_audit = signed_agreement_audit.SignedAgreementAccessAudit() - cdsa_audit._audit_signed_agreement(component_agreement.signed_agreement) + 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.RemoveAccess) - self.assertEqual(record.signed_agreement, component_agreement.signed_agreement) - self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_CDSA) - - def test_one_signed_agreement_nondataaffiliate_component(self): - factories.NonDataAffiliateAgreementFactory.create( - signed_agreement__is_primary=False + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) + + 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, + signed_agreement__version__major_version__is_valid=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() - with self.assertRaises(RuntimeError): - cdsa_audit.run_audit() + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) + self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) - def test_other_error(self): - signed_agreement = factories.SignedAgreementFactory.create( - is_primary=False, type="MEMBER" + def test_data_affiliate_primary_in_group(self): + """Member primary agreement with valid version in CDSA group.""" + this_agreement = factories.DataAffiliateAgreementFactory.create() + # 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(signed_agreement) - self.assertEqual(len(cdsa_audit.verified), 0) + cdsa_audit._audit_signed_agreement(this_agreement.signed_agreement) + self.assertEqual(len(cdsa_audit.verified), 1) 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, signed_agreement) - self.assertEqual(record.note, cdsa_audit.ERROR_OTHER_CASE) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + def test_data_affiliate_primary_not_in_group(self): + """Member primary agreement with valid version not in CDSA group.""" + this_agreement = factories.DataAffiliateAgreementFactory.create() + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) -class SignedAgreementAccessAuditTableTest(TestCase): - """Tests for the `SignedAgreementAccessAuditTable` table.""" + def test_data_affiliate_primary_invalid_version_in_group(self): + """Member primary agreement, active, with invalid version in CDSA group.""" + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__version__major_version__is_valid=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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) - def test_no_rows(self): - """Table works with no rows.""" - table = signed_agreement_audit.SignedAgreementAccessAuditTable([]) - self.assertIsInstance( - table, signed_agreement_audit.SignedAgreementAccessAuditTable + def test_data_affiliate_primary_invalid_version_not_in_group(self): + """Member primary agreement, with invalid version, not in CDSA group.""" + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__version__major_version__is_valid=False ) - self.assertEqual(len(table.rows), 0) + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) - def test_one_row(self): - """Table works with one row.""" - signed_agreement = factories.SignedAgreementFactory.create() + def test_data_affiliate_primary_valid_not_active_in_group(self): + """Member primary agreement with valid version but isn't active, in CDSA group.""" + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.RemoveAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT) + + def test_data_affiliate_primary_valid_not_active_not_in_group(self): + """Member primary agreement with valid version but isn't active, not in CDSA group.""" + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + # # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT) + + def test_data_affiliate_component_has_primary_in_group(self): + """Member component agreement, with valid version, with primary with valid version, in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create(study=study) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, study=study + ) + # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_data_affiliate_component_has_primary_not_in_group(self): + """Member component agreement, with valid version, with primary with valid version, not in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create(study=study) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, study=study + ) + # # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_data_affiliate_component_inactive_has_primary_in_group(self): + """Member component agreement, inactive, with valid version, with primary with valid version, in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create(study=study) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.RemoveAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT) + + def test_data_affiliate_component_inactive_has_primary_not_in_group(self): + """Member component agreement, inactive, with valid version, with valid active primary, not in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create(study=study) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + # # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT) + + def test_data_affiliate_component_has_primary_with_invalid_version_in_group(self): + """Member component agreement, with valid version, with active primary with invalid version, in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__version__major_version__is_valid=False, + ) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, study=study + ) + # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_data_affiliate_component_has_primary_with_invalid_version_not_in_group( + self, + ): + """Member component agreement, with valid version, with active primary with invalid version, not in group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__version__major_version__is_valid=False, + ) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, study=study + ) + # # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_data_affiliate_component_has_inactive_primary_in_group(self): + """Member component agreement, with valid version, with inactive primary, in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, study=study + ) + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.RemoveAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.PRIMARY_NOT_ACTIVE) + + def test_data_affiliate_component_has_inactive_primary_not_in_group(self): + """Member component agreement, with valid version, with inactive primary, not in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, study=study + ) + # # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.PRIMARY_NOT_ACTIVE) + + 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 + ) + # 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.RemoveAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) + + 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 + ) + # # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) + + def test_data_affiliate_component_invalid_version_has_primary_in_group(self): + """Member component agreement, with invalid version, with a valid primary, in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create(study=study) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, + study=study, + signed_agreement__version__major_version__is_valid=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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_data_affiliate_component_invalid_version_has_primary_not_in_group(self): + """Member component agreement, with invalid version, with a valid primary, not in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create(study=study) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, + study=study, + signed_agreement__version__major_version__is_valid=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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_data_affiliate_component_invalid_version_has_primary_with_invalid_version_in_group( + self, + ): + """Member component agreement, with invalid version, with an invalid primary, in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create(study=study) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, + study=study, + signed_agreement__version__major_version__is_valid=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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + def test_data_affiliate_component_invalid_version_has_primary_with_invalid_version_not_in_group( + self, + ): + """Member component agreement, with invalid version, with an invalid primary, not in CDSA group.""" + study = StudyFactory.create() + factories.DataAffiliateAgreementFactory.create(study=study) + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__is_primary=False, + study=study, + signed_agreement__version__major_version__is_valid=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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_COMPONENT_AGREEMENT) + + 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, + signed_agreement__version__major_version__is_valid=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.RemoveAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) + + 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, + signed_agreement__version__major_version__is_valid=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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) + + def test_non_data_affiliate_primary_in_group(self): + """Non data affiliate primary agreement with valid version in CDSA group.""" + this_agreement = factories.NonDataAffiliateAgreementFactory.create() + # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_non_data_affiliate_primary_not_in_group(self): + """Non data affiliate primary agreement with valid version not in CDSA group.""" + this_agreement = factories.NonDataAffiliateAgreementFactory.create() + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_non_data_affiliate_primary_invalid_version_in_group(self): + """Non data affiliate primary agreement with invalid version in CDSA group.""" + this_agreement = factories.NonDataAffiliateAgreementFactory.create( + signed_agreement__version__major_version__is_valid=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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_non_data_affiliate_primary_invalid_version_not_in_group(self): + """Non data affiliate primary agreement, with invalid version, not in CDSA group.""" + this_agreement = factories.NonDataAffiliateAgreementFactory.create( + signed_agreement__version__major_version__is_valid=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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.GrantAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_non_data_affiliate_primary_valid_not_active_in_group(self): + """Non Data affiliate primary agreement with valid version but isn't active, in CDSA group.""" + this_agreement = factories.NonDataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + # 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), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, signed_agreement_audit.RemoveAccess) + self.assertEqual(record.signed_agreement, this_agreement.signed_agreement) + self.assertEqual(record.note, cdsa_audit.INACTIVE_AGREEMENT) + + def test_non_data_affiliate_primary_valid_not_active_not_in_group(self): + """Non data affiliate primary agreement with valid version but isn't active, not in CDSA group.""" + this_agreement = factories.NonDataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + # # 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), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, signed_agreement_audit.VerifiedNoAccess) + 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.""" + + def test_no_rows(self): + """Table works with no rows.""" + table = signed_agreement_audit.SignedAgreementAccessAuditTable([]) + self.assertIsInstance( + table, signed_agreement_audit.SignedAgreementAccessAuditTable + ) + self.assertEqual(len(table.rows), 0) + + def test_one_row(self): + """Table works with one row.""" + signed_agreement = factories.SignedAgreementFactory.create() data = [ { "signed_agreement": signed_agreement, @@ -531,33 +1465,126 @@ def test_anvil_group_name_setting(self): class WorkspaceAccessAuditTest(TestCase): """Tests for the WorkspaceAccessAudit class.""" - def setUp(self): - super().setUp() - self.cdsa_group = ManagedGroupFactory.create( - name=settings.ANVIL_CDSA_GROUP_NAME + def setUp(self): + super().setUp() + self.cdsa_group = ManagedGroupFactory.create( + name=settings.ANVIL_CDSA_GROUP_NAME + ) + + def test_completed(self): + """completed is updated properly.""" + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + self.assertFalse(cdsa_audit.completed) + cdsa_audit.run_audit() + self.assertTrue(cdsa_audit.completed) + + def test_primary_in_auth_domain(self): + study = StudyFactory.create() + workspace = factories.CDSAWorkspaceFactory.create(study=study) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study + ) + # Add the CDSA group to the auth domain. + GroupGroupMembershipFactory.create( + parent_group=workspace.workspace.authorization_domains.first(), + child_group=self.cdsa_group, + ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, workspace_audit.VerifiedAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_primary_not_in_auth_domain(self): + study = StudyFactory.create() + workspace = factories.CDSAWorkspaceFactory.create(study=study) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study + ) + # Do not add the CDSA group to the auth domain. + # GroupGroupMembershipFactory.create( + # parent_group=workspace.workspace.authorization_domains.first(), + # child_group=self.cdsa_group, + # ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 0) + self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, workspace_audit.GrantAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_no_primary_not_in_auth_domain(self): + workspace = factories.CDSAWorkspaceFactory.create() + # Do not CDSA group to the auth domain. + # GroupGroupMembershipFactory.create( + # parent_group=workspace.workspace.authorization_domains.first(), + # child_group=self.cdsa_group, + # ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, workspace_audit.VerifiedNoAccess) + self.assertIsNone(record.data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) + + def test_no_primary_in_auth_domain(self): + workspace = factories.CDSAWorkspaceFactory.create() + # Add the CDSA group to the auth domain - this is an error. + GroupGroupMembershipFactory.create( + parent_group=workspace.workspace.authorization_domains.first(), + child_group=self.cdsa_group, ) - - def test_completed(self): - """completed is updated properly.""" cdsa_audit = workspace_audit.WorkspaceAccessAudit() - self.assertFalse(cdsa_audit.completed) - cdsa_audit.run_audit() - self.assertTrue(cdsa_audit.completed) + cdsa_audit._audit_workspace(workspace) + 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, workspace_audit.RemoveAccess) + self.assertIsNone(record.data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) - def test_no_workspaces(self): - """Audit works when there are no workspaces.""" + def test_primary_invalid_version_not_in_auth_domain(self): + workspace = factories.CDSAWorkspaceFactory.create() + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__version__major_version__is_valid=False, + study=workspace.study, + ) + # Do not add the CDSA group to the auth domain. + # GroupGroupMembershipFactory.create( + # parent_group=workspace.workspace.authorization_domains.first(), + # child_group=self.cdsa_group, + # ) cdsa_audit = workspace_audit.WorkspaceAccessAudit() - self.assertFalse(cdsa_audit.completed) - cdsa_audit.run_audit() + cdsa_audit._audit_workspace(workspace) self.assertEqual(len(cdsa_audit.verified), 0) - self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.needs_action), 1) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, workspace_audit.GrantAccess) + self.assertEqual(record.data_affiliate_agreement, this_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) - def test_one_workspace_with_primary_verified_access(self): - study = StudyFactory.create() - workspace = factories.CDSAWorkspaceFactory.create(study=study) - data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( - study=study + def test_primary_invalid_version_in_auth_domain(self): + workspace = factories.CDSAWorkspaceFactory.create() + this_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__version__major_version__is_valid=False, + study=workspace.study, ) # Add the CDSA group to the auth domain. GroupGroupMembershipFactory.create( @@ -565,43 +1592,95 @@ def test_one_workspace_with_primary_verified_access(self): child_group=self.cdsa_group, ) cdsa_audit = workspace_audit.WorkspaceAccessAudit() - cdsa_audit.run_audit() + cdsa_audit._audit_workspace(workspace) self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) record = cdsa_audit.verified[0] self.assertIsInstance(record, workspace_audit.VerifiedAccess) - self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.data_affiliate_agreement, this_agreement) self.assertEqual(record.workspace, workspace) - self.assertEqual(record.note, cdsa_audit.VALID_PRIMARY_CDSA) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_primary_inactive_not_in_auth_domain(self): + workspace = factories.CDSAWorkspaceFactory.create() + factories.DataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + study=workspace.study, + ) + # Do not add the CDSA group to the auth domain. + # GroupGroupMembershipFactory.create( + # parent_group=workspace.workspace.authorization_domains.first(), + # child_group=self.cdsa_group, + # ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 1) self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, workspace_audit.VerifiedNoAccess) + self.assertIsNone(record.data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.INACTIVE_PRIMARY_AGREEMENT) - def test_one_workspace_no_primary_no_verified_access(self): + def test_primary_inactive_in_auth_domain(self): workspace = factories.CDSAWorkspaceFactory.create() - # Do not CDSA group to the auth domain. + factories.DataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + study=workspace.study, + ) + # Add the CDSA group to the auth domain. + GroupGroupMembershipFactory.create( + parent_group=workspace.workspace.authorization_domains.first(), + child_group=self.cdsa_group, + ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 0) + self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, workspace_audit.RemoveAccess) + self.assertIsNone(record.data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.INACTIVE_PRIMARY_AGREEMENT) + + 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 + ) + # Do not add the CDSA group to the auth domain. # GroupGroupMembershipFactory.create( # parent_group=workspace.workspace.authorization_domains.first(), # child_group=self.cdsa_group, # ) cdsa_audit = workspace_audit.WorkspaceAccessAudit() - cdsa_audit.run_audit() + cdsa_audit._audit_workspace(workspace) self.assertEqual(len(cdsa_audit.verified), 1) record = cdsa_audit.verified[0] self.assertIsInstance(record, workspace_audit.VerifiedNoAccess) self.assertIsNone(record.data_affiliate_agreement) self.assertEqual(record.workspace, workspace) - self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_CDSA) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) - def test_one_workspace_no_primary_error_has_access(self): - workspace = factories.CDSAWorkspaceFactory.create() - # Add the CDSA group to the auth domain - this is an error. + 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 + ) + # Add the CDSA group to the auth domain. GroupGroupMembershipFactory.create( parent_group=workspace.workspace.authorization_domains.first(), child_group=self.cdsa_group, ) cdsa_audit = workspace_audit.WorkspaceAccessAudit() - cdsa_audit.run_audit() + cdsa_audit._audit_workspace(workspace) self.assertEqual(len(cdsa_audit.verified), 0) self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 1) @@ -609,13 +1688,71 @@ def test_one_workspace_no_primary_error_has_access(self): self.assertIsInstance(record, workspace_audit.RemoveAccess) self.assertIsNone(record.data_affiliate_agreement) self.assertEqual(record.workspace, workspace) - self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_CDSA) + self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_AGREEMENT) - def test_one_workspace_with_primary_no_access(self): + def test_two_valid_primary_agreements_in_auth_domain(self): study = StudyFactory.create() workspace = factories.CDSAWorkspaceFactory.create(study=study) + factories.DataAffiliateAgreementFactory.create( + study=study, signed_agreement__version__major_version__version=1 + ) data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( - study=study + study=study, signed_agreement__version__major_version__version=2 + ) + # Add the CDSA group to the auth domain. + GroupGroupMembershipFactory.create( + parent_group=workspace.workspace.authorization_domains.first(), + child_group=self.cdsa_group, + ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, workspace_audit.VerifiedAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_two_valid_primary_agreements_same_major_version_in_auth_domain(self): + study = StudyFactory.create() + workspace = factories.CDSAWorkspaceFactory.create(study=study) + major_version = factories.AgreementMajorVersionFactory.create() + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__version__major_version=major_version, + signed_agreement__version__minor_version=1, + ) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__version__major_version=major_version, + signed_agreement__version__minor_version=2, + ) + # Add the CDSA group to the auth domain. + GroupGroupMembershipFactory.create( + parent_group=workspace.workspace.authorization_domains.first(), + child_group=self.cdsa_group, + ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, workspace_audit.VerifiedAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_two_valid_primary_agreements_not_in_auth_domain(self): + study = StudyFactory.create() + workspace = factories.CDSAWorkspaceFactory.create(study=study) + factories.DataAffiliateAgreementFactory.create( + study=study, signed_agreement__version__major_version__version=1 + ) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study, signed_agreement__version__major_version__version=2 ) # Do not add the CDSA group to the auth domain. # GroupGroupMembershipFactory.create( @@ -623,21 +1760,57 @@ def test_one_workspace_with_primary_no_access(self): # child_group=self.cdsa_group, # ) cdsa_audit = workspace_audit.WorkspaceAccessAudit() - cdsa_audit.run_audit() + cdsa_audit._audit_workspace(workspace) self.assertEqual(len(cdsa_audit.verified), 0) self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) record = cdsa_audit.needs_action[0] self.assertIsInstance(record, workspace_audit.GrantAccess) self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) self.assertEqual(record.workspace, workspace) - self.assertEqual(record.note, cdsa_audit.VALID_PRIMARY_CDSA) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_two_primary_one_valid_one_invalid_both_active_in_auth_domain(self): + study = StudyFactory.create() + workspace = factories.CDSAWorkspaceFactory.create(study=study) + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__version__major_version__is_valid=True, + signed_agreement__version__major_version__version=1, + ) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__version__major_version__is_valid=False, + signed_agreement__version__major_version__version=2, + ) + # Add the CDSA group to the auth domain. + GroupGroupMembershipFactory.create( + parent_group=workspace.workspace.authorization_domains.first(), + child_group=self.cdsa_group, + ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, workspace_audit.VerifiedAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) - def test_ignores_component_agreement(self): + def test_two_primary_one_valid_one_invalid_both_active_not_in_auth_domain(self): study = StudyFactory.create() workspace = factories.CDSAWorkspaceFactory.create(study=study) factories.DataAffiliateAgreementFactory.create( - study=study, signed_agreement__is_primary=False + study=study, + signed_agreement__version__major_version__is_valid=True, + signed_agreement__version__major_version__version=1, + ) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__version__major_version__is_valid=False, + signed_agreement__version__major_version__version=2, ) # Do not add the CDSA group to the auth domain. # GroupGroupMembershipFactory.create( @@ -645,13 +1818,134 @@ def test_ignores_component_agreement(self): # child_group=self.cdsa_group, # ) cdsa_audit = workspace_audit.WorkspaceAccessAudit() - cdsa_audit.run_audit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 0) + self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, workspace_audit.GrantAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_two_primary_one_active_one_inactive_in_auth_domain(self): + study = StudyFactory.create() + workspace = factories.CDSAWorkspaceFactory.create(study=study) + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE, + ) + # Add the CDSA group to the auth domain. + GroupGroupMembershipFactory.create( + parent_group=workspace.workspace.authorization_domains.first(), + child_group=self.cdsa_group, + ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) record = cdsa_audit.verified[0] - self.assertIsInstance(record, workspace_audit.VerifiedNoAccess) - self.assertIsNone(record.data_affiliate_agreement) + self.assertIsInstance(record, workspace_audit.VerifiedAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_two_primary_one_active_one_inactive_not_in_auth_domain(self): + study = StudyFactory.create() + workspace = factories.CDSAWorkspaceFactory.create(study=study) + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE, + ) + # Do not add the CDSA group to the auth domain. + # GroupGroupMembershipFactory.create( + # parent_group=workspace.workspace.authorization_domains.first(), + # child_group=self.cdsa_group, + # ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 0) + self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, workspace_audit.GrantAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_two_primary_one_active_invalid_one_inactive_valid_in_auth_domain(self): + study = StudyFactory.create() + workspace = factories.CDSAWorkspaceFactory.create(study=study) + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + signed_agreement__version__major_version__is_valid=True, + ) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE, + signed_agreement__version__major_version__is_valid=False, + ) + # Add the CDSA group to the auth domain. + GroupGroupMembershipFactory.create( + parent_group=workspace.workspace.authorization_domains.first(), + child_group=self.cdsa_group, + ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 1) + self.assertEqual(len(cdsa_audit.needs_action), 0) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.verified[0] + self.assertIsInstance(record, workspace_audit.VerifiedAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) + self.assertEqual(record.workspace, workspace) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_two_primary_one_active_invalid_one_inactive_valid_not_in_auth_domain(self): + study = StudyFactory.create() + workspace = factories.CDSAWorkspaceFactory.create(study=study) + factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + signed_agreement__version__major_version__is_valid=True, + ) + data_affiliate_agreement = factories.DataAffiliateAgreementFactory.create( + study=study, + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE, + signed_agreement__version__major_version__is_valid=False, + ) + # # Add the CDSA group to the auth domain. + # GroupGroupMembershipFactory.create( + # parent_group=workspace.workspace.authorization_domains.first(), + # child_group=self.cdsa_group, + # ) + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + cdsa_audit._audit_workspace(workspace) + self.assertEqual(len(cdsa_audit.verified), 0) + self.assertEqual(len(cdsa_audit.needs_action), 1) + self.assertEqual(len(cdsa_audit.errors), 0) + record = cdsa_audit.needs_action[0] + self.assertIsInstance(record, workspace_audit.GrantAccess) + self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) self.assertEqual(record.workspace, workspace) - self.assertEqual(record.note, cdsa_audit.NO_PRIMARY_CDSA) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) + + def test_no_workspaces(self): + """Audit works when there are no workspaces.""" + cdsa_audit = workspace_audit.WorkspaceAccessAudit() + self.assertFalse(cdsa_audit.completed) + cdsa_audit.run_audit() + self.assertEqual(len(cdsa_audit.verified), 0) self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) @@ -678,12 +1972,12 @@ def test_two_workspaces(self): self.assertIsInstance(record, workspace_audit.VerifiedAccess) self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) self.assertEqual(record.workspace, workspace_1) - self.assertEqual(record.note, cdsa_audit.VALID_PRIMARY_CDSA) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) record = cdsa_audit.verified[1] self.assertIsInstance(record, workspace_audit.VerifiedAccess) self.assertEqual(record.data_affiliate_agreement, data_affiliate_agreement) self.assertEqual(record.workspace, workspace_2) - self.assertEqual(record.note, cdsa_audit.VALID_PRIMARY_CDSA) + self.assertEqual(record.note, cdsa_audit.ACTIVE_PRIMARY_AGREEMENT) self.assertEqual(len(cdsa_audit.needs_action), 0) self.assertEqual(len(cdsa_audit.errors), 0) diff --git a/primed/cdsa/tests/test_commands.py b/primed/cdsa/tests/test_commands.py index b8ac8c76..e5ad8ebe 100644 --- a/primed/cdsa/tests/test_commands.py +++ b/primed/cdsa/tests/test_commands.py @@ -74,7 +74,8 @@ def test_cdsa_workspace_records_zero(self): self.assertEqual(len(lines), 1) def test_cdsa_workspace_records_one(self): - factories.CDSAWorkspaceFactory.create() + agreement = factories.DataAffiliateAgreementFactory.create() + factories.CDSAWorkspaceFactory.create(study=agreement.study) out = StringIO() call_command("cdsa_records", self.outdir, "--no-color", stdout=out) with open(os.path.join(self.outdir, "workspace_records.tsv")) as f: diff --git a/primed/cdsa/tests/test_forms.py b/primed/cdsa/tests/test_forms.py index 1366efc2..09aac23e 100644 --- a/primed/cdsa/tests/test_forms.py +++ b/primed/cdsa/tests/test_forms.py @@ -202,6 +202,68 @@ def test_invalid_duplicate_object(self): self.assertEqual(len(form.errors["cc_id"]), 1) self.assertIn("already exists", form.errors["cc_id"][0]) + def test_invalid_version(self): + """Cannot select an AgreementVersion that is not valid.""" + self.agreement_version.major_version.is_valid = False + self.agreement_version.major_version.save() + 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("version", form.errors) + self.assertEqual(len(form.errors["version"]), 1) + self.assertIn("valid choice", form.errors["version"][0]) + + +class SignedAgreementStatusFormTest(TestCase): + """Tests for the SignedAgreementStatusForm class.""" + + form_class = forms.SignedAgreementStatusForm + + def setUp(self): + """Create related objects for use in the form.""" + self.representative = UserFactory.create() + self.agreement_version = factories.AgreementVersionFactory.create() + self.signed_agreement = factories.SignedAgreementFactory.create() + + def test_valid(self): + """Form is valid with necessary input.""" + form_data = { + "status": models.SignedAgreement.StatusChoices.ACTIVE, + } + form = self.form_class(data=form_data) + self.assertTrue(form.is_valid()) + + def test_missing_status(self): + """Form is invalid when missing status.""" + form_data = {} + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("status", form.errors) + self.assertEqual(len(form.errors["status"]), 1) + self.assertIn("required", form.errors["status"][0]) + + def test_invalid_status(self): + """Cannot select a status that doesn't exist.""" + self.agreement_version.major_version.is_valid = False + self.agreement_version.major_version.save() + form_data = {"status": "foo"} + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("status", form.errors) + self.assertEqual(len(form.errors["status"]), 1) + self.assertIn("valid choice", form.errors["status"][0]) + class MemberAgreementFormTest(TestCase): """Tests for the MemberAgreementForm class.""" diff --git a/primed/cdsa/tests/test_migrations.py b/primed/cdsa/tests/test_migrations.py new file mode 100644 index 00000000..9eb16740 --- /dev/null +++ b/primed/cdsa/tests/test_migrations.py @@ -0,0 +1,102 @@ +"""Tests for data migrations in the app.""" +from datetime import date + +from anvil_consortium_manager.tests.factories import BillingProjectFactory, WorkspaceFactory +from django_test_migrations.contrib.unittest_case import MigratorTestCase +import factory + +from . import factories + +class AgreementMajorVersionMigrationsTest(MigratorTestCase): + """Tests for the migrations associated with creating the new AgreementMajorVersion model.""" + + migrate_from = ("cdsa", "0001_initial") + migrate_to = ("cdsa", "0007_alter_agreementversion_major_version") + + def prepare(self): + """Prepare some data before the migration.""" + # Get model definition for the old state. + AgreementVersion = self.old_state.apps.get_model("cdsa", "AgreementVersion") + # Populate with multiple major versions and minor versions. + self.agreement_version_1_0 = AgreementVersion.objects.create( + major_version=1, + minor_version=0, + ) + self.agreement_version_1_1 = AgreementVersion.objects.create( + major_version=1, + minor_version=1, + ) + self.agreement_version_1_2 = AgreementVersion.objects.create( + major_version=1, + minor_version=2, + ) + self.agreement_version_2_1 = AgreementVersion.objects.create( + major_version=2, + minor_version=1, + ) + self.agreement_version_2_3 = AgreementVersion.objects.create( + major_version=2, + minor_version=3, + ) + self.agreement_version_3_0 = AgreementVersion.objects.create( + major_version=3, + minor_version=0, + ) + self.agreement_version_5_6 = AgreementVersion.objects.create( + major_version=5, + minor_version=6, + ) + + def test_agreementmajorversion_model_correctly_populated(self): + """AgreementMajorVersion is correctly populated.""" + AgreementMajorVersion = self.new_state.apps.get_model("cdsa", "AgreementMajorVersion") + self.assertEqual(AgreementMajorVersion.objects.count(), 4) + major_version = AgreementMajorVersion.objects.get(version=1) + major_version.full_clean() + major_version = AgreementMajorVersion.objects.get(version=2) + major_version.full_clean() + major_version = AgreementMajorVersion.objects.get(version=3) + major_version.full_clean() + major_version = AgreementMajorVersion.objects.get(version=5) + major_version.full_clean() + + def test_agreement_version_major_version_correctly_populated(self): + """AgreementVersion.major_version is correctly populated.""" + AgreementMajorVersion = self.new_state.apps.get_model("cdsa", "AgreementMajorVersion") + AgreementVersion = self.new_state.apps.get_model("cdsa", "AgreementVersion") + # Major version 1. + major_version = AgreementMajorVersion.objects.get(version=1) + agreement_version = AgreementVersion.objects.get(pk=self.agreement_version_1_0.pk) + agreement_version.full_clean() + self.assertEqual(agreement_version.major_version, major_version) + self.assertEqual(agreement_version.minor_version, 0) + agreement_version = AgreementVersion.objects.get(pk=self.agreement_version_1_1.pk) + agreement_version.full_clean() + self.assertEqual(agreement_version.major_version, major_version) + self.assertEqual(agreement_version.minor_version, 1) + agreement_version = AgreementVersion.objects.get(pk=self.agreement_version_1_2.pk) + agreement_version.full_clean() + self.assertEqual(agreement_version.major_version, major_version) + self.assertEqual(agreement_version.minor_version, 2) + # Major version 2. + major_version = AgreementMajorVersion.objects.get(version=2) + agreement_version = AgreementVersion.objects.get(pk=self.agreement_version_2_1.pk) + agreement_version.full_clean() + self.assertEqual(agreement_version.major_version, major_version) + self.assertEqual(agreement_version.minor_version, 1) + agreement_version = AgreementVersion.objects.get(pk=self.agreement_version_2_3.pk) + agreement_version.full_clean() + self.assertEqual(agreement_version.major_version, major_version) + self.assertEqual(agreement_version.minor_version, 3) + # Major version 3. + major_version = AgreementMajorVersion.objects.get(version=3) + agreement_version = AgreementVersion.objects.get(pk=self.agreement_version_3_0.pk) + agreement_version.full_clean() + self.assertEqual(agreement_version.major_version, major_version) + self.assertEqual(agreement_version.minor_version, 0) + # Major version 5. + major_version = AgreementMajorVersion.objects.get(version=5) + agreement_version = AgreementVersion.objects.get(pk=self.agreement_version_5_6.pk) + agreement_version.full_clean() + self.assertEqual(agreement_version.major_version, major_version) + self.assertEqual(agreement_version.minor_version, 6) diff --git a/primed/cdsa/tests/test_models.py b/primed/cdsa/tests/test_models.py index dcd4a36d..32b35cd2 100644 --- a/primed/cdsa/tests/test_models.py +++ b/primed/cdsa/tests/test_models.py @@ -2,14 +2,16 @@ from datetime import datetime +from anvil_consortium_manager.models import ManagedGroup from anvil_consortium_manager.tests.factories import ( + GroupGroupMembershipFactory, ManagedGroupFactory, WorkspaceFactory, ) from django.core.exceptions import NON_FIELD_ERRORS, ValidationError from django.db.models import ProtectedError from django.db.utils import IntegrityError -from django.test import TestCase +from django.test import TestCase, override_settings from primed.duo.tests.factories import DataUseModifierFactory, DataUsePermissionFactory from primed.primed_anvil.tests.factories import ( @@ -23,58 +25,99 @@ from . import factories -class AgreementVersionTest(TestCase): - """Tests for the AgreementVersion model.""" +class AgreementMajorVersionTest(TestCase): + """Tests for the AgreementMajorVersion model.""" def test_model_saving(self): - instance = models.AgreementVersion( - major_version=1, minor_version=0, date_approved=datetime.today() - ) + instance = models.AgreementMajorVersion(version=1) instance.save() - self.assertIsInstance(instance, models.AgreementVersion) + self.assertIsInstance(instance, models.AgreementMajorVersion) + + def test_is_valid_default(self): + instance = factories.AgreementMajorVersionFactory.create() + self.assertTrue(instance.is_valid) def test_unique(self): - factories.AgreementVersionFactory.create(major_version=1, minor_version=0) - instance = factories.AgreementVersionFactory.build( - major_version=1, minor_version=0 - ) + factories.AgreementMajorVersionFactory.create(version=1) + instance = factories.AgreementMajorVersionFactory.build(version=1) with self.assertRaisesMessage(ValidationError, "already exists"): instance.full_clean() with self.assertRaises(IntegrityError): instance.save() - def test_major_version_zero(self): - """ValidationError raised when major_version is zero.""" - instance = factories.AgreementVersionFactory.build(major_version=0) + def test_version_zero(self): + """ValidationError raised when version is zero.""" + instance = factories.AgreementMajorVersionFactory.build(version=0) with self.assertRaises(ValidationError) as e: instance.full_clean() self.assertEqual(len(e.exception.message_dict), 1) - self.assertIn("major_version", e.exception.message_dict) - self.assertEqual(len(e.exception.message_dict["major_version"]), 1) + self.assertIn("version", e.exception.message_dict) + self.assertEqual(len(e.exception.message_dict["version"]), 1) self.assertIn( - "greater than or equal to", e.exception.message_dict["major_version"][0] + "greater than or equal to", e.exception.message_dict["version"][0] ) - def test_major_version_negative(self): - """ValidationError raised when major_version is negative.""" - instance = factories.AgreementVersionFactory.build(major_version=-1) + def test_version_negative(self): + """ValidationError raised when version is negative.""" + instance = factories.AgreementMajorVersionFactory.build(version=-1) with self.assertRaises(ValidationError) as e: instance.full_clean() self.assertEqual(len(e.exception.message_dict), 1) - self.assertIn("major_version", e.exception.message_dict) - self.assertEqual(len(e.exception.message_dict["major_version"]), 1) + self.assertIn("version", e.exception.message_dict) + self.assertEqual(len(e.exception.message_dict["version"]), 1) self.assertIn( - "greater than or equal to", e.exception.message_dict["major_version"][0] + "greater than or equal to", e.exception.message_dict["version"][0] + ) + + def test_str(self): + """__str__ method works as expected.""" + instance = factories.AgreementMajorVersionFactory.build() + self.assertIsInstance(str(instance), str) + + def test_get_absolute_url(self): + """get_absolute_url method works correctly.""" + instance = factories.AgreementMajorVersionFactory.create() + self.assertIsInstance(instance.get_absolute_url(), str) + + +class AgreementVersionTest(TestCase): + """Tests for the AgreementVersion model.""" + + def test_model_saving(self): + major_version = factories.AgreementMajorVersionFactory.create() + instance = models.AgreementVersion( + major_version=major_version, minor_version=0, date_approved=datetime.today() + ) + instance.save() + self.assertIsInstance(instance, models.AgreementVersion) + + def test_unique(self): + major_version = factories.AgreementMajorVersionFactory.create() + factories.AgreementVersionFactory.create( + major_version=major_version, minor_version=0 + ) + instance = factories.AgreementVersionFactory.build( + major_version=major_version, minor_version=0 ) + with self.assertRaisesMessage(ValidationError, "already exists"): + instance.full_clean() + with self.assertRaises(IntegrityError): + instance.save() def test_minor_version_zero(self): """full_clean raises no exception when minor_version is zero.""" - instance = factories.AgreementVersionFactory.build(minor_version=0) + major_version = factories.AgreementMajorVersionFactory.create() + instance = factories.AgreementVersionFactory.build( + major_version=major_version, minor_version=0 + ) instance.full_clean() def test_minor_version_negative(self): """ValidationError raised when minor_version is negative.""" - instance = factories.AgreementVersionFactory.build(minor_version=-1) + major_version = factories.AgreementMajorVersionFactory.create() + instance = factories.AgreementVersionFactory.build( + major_version=major_version, minor_version=-1 + ) with self.assertRaises(ValidationError) as e: instance.full_clean() self.assertEqual(len(e.exception.message_dict), 1) @@ -88,25 +131,25 @@ def test_full_version(self): """full_version property works as expected.""" self.assertEqual( factories.AgreementVersionFactory( - major_version=1, minor_version=0 + major_version__version=1, minor_version=0 ).full_version, "v1.0", ) self.assertEqual( factories.AgreementVersionFactory( - major_version=1, minor_version=5 + major_version__version=1, minor_version=5 ).full_version, "v1.5", ) self.assertEqual( factories.AgreementVersionFactory( - major_version=1, minor_version=10 + major_version__version=1, minor_version=10 ).full_version, "v1.10", ) self.assertEqual( factories.AgreementVersionFactory( - major_version=2, minor_version=3 + major_version__version=2, minor_version=3 ).full_version, "v2.3", ) @@ -116,6 +159,11 @@ def test_str(self): instance = factories.AgreementVersionFactory.build() self.assertIsInstance(str(instance), str) + def test_get_absolute_url(self): + """get_absolute_url method works correctly.""" + instance = factories.AgreementVersionFactory.create() + self.assertIsInstance(instance.get_absolute_url(), str) + class SignedAgreementTest(TestCase): """Tests for the SignedAgreement model.""" @@ -202,7 +250,6 @@ def test_cc_id_zero(self): user = UserFactory.create() group = ManagedGroupFactory.create() agreement_version = factories.AgreementVersionFactory.create() - instance = factories.AgreementVersionFactory.build(major_version=0) instance = factories.SignedAgreementFactory.build( cc_id=0, representative=user, @@ -221,7 +268,6 @@ def test_cc_id_negative(self): user = UserFactory.create() group = ManagedGroupFactory.create() agreement_version = factories.AgreementVersionFactory.create() - instance = factories.AgreementVersionFactory.build(major_version=0) instance = factories.SignedAgreementFactory.build( cc_id=-1, representative=user, @@ -242,6 +288,28 @@ def test_agreement_version_protect(self): with self.assertRaises(ProtectedError): agreement_version.delete() + def test_status_field(self): + # default + instance = factories.SignedAgreementFactory.create() + self.assertEqual(instance.status, instance.StatusChoices.ACTIVE) + instance.full_clean() + # other choices + instance = factories.SignedAgreementFactory.create( + status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + self.assertEqual(instance.status, instance.StatusChoices.WITHDRAWN) + instance.full_clean() + instance = factories.SignedAgreementFactory.create( + status=models.SignedAgreement.StatusChoices.LAPSED + ) + self.assertEqual(instance.status, instance.StatusChoices.LAPSED) + instance.full_clean() + + # not allowed + instance = factories.SignedAgreementFactory.create(status="foo") + with self.assertRaises(ValidationError): + instance.full_clean() + def test_get_combined_type(self): obj = factories.MemberAgreementFactory() self.assertEqual(obj.signed_agreement.combined_type, "Member") @@ -283,7 +351,6 @@ def test_clean_non_data_affiliate_is_primary_false(self): user = UserFactory.create() group = ManagedGroupFactory.create() agreement_version = factories.AgreementVersionFactory.create() - instance = factories.AgreementVersionFactory.build(major_version=0) instance = factories.SignedAgreementFactory.build( representative=user, anvil_access_group=group, @@ -298,6 +365,37 @@ def test_clean_non_data_affiliate_is_primary_false(self): 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() + # When group does not exist + with self.assertRaises(ManagedGroup.DoesNotExist): + obj.is_in_cdsa_group() + # Create group, without adding agreement + cdsa_group = ManagedGroupFactory.create(name="TEST_PRIMED_CDSA") + self.assertFalse(obj.is_in_cdsa_group()) + # Add agreement and check again, + GroupGroupMembershipFactory.create( + parent_group=cdsa_group, child_group=obj.anvil_access_group + ) + self.assertTrue(obj.is_in_cdsa_group()) + + @override_settings(ANVIL_CDSA_GROUP_NAME="FOO") + def test_is_in_cdsa_group_different_group_name(self): + """is_in_cdsa_group works as expected.""" + obj = factories.SignedAgreementFactory.create() + # When group does not exist + with self.assertRaises(ManagedGroup.DoesNotExist): + obj.is_in_cdsa_group() + # Create group, without adding agreement + cdsa_group = ManagedGroupFactory.create(name="FOO") + self.assertFalse(obj.is_in_cdsa_group()) + # Add agreement and check again, + GroupGroupMembershipFactory.create( + parent_group=cdsa_group, child_group=obj.anvil_access_group + ) + self.assertTrue(obj.is_in_cdsa_group()) + class MemberAgreementTest(TestCase): """Tests for the MemberAgremeent model.""" diff --git a/primed/cdsa/tests/test_tables.py b/primed/cdsa/tests/test_tables.py index 6bf1fa68..aaae72d0 100644 --- a/primed/cdsa/tests/test_tables.py +++ b/primed/cdsa/tests/test_tables.py @@ -14,6 +14,26 @@ from . import factories +class AgreementVersionTableTest(TestCase): + model = models.AgreementVersion + model_factory = factories.AgreementVersionFactory + table_class = tables.AgreementVersionTable + + def test_row_count_with_no_objects(self): + table = self.table_class(self.model.objects.all()) + self.assertEqual(len(table.rows), 0) + + def test_row_count_with_one_object(self): + self.model_factory.create() + table = self.table_class(self.model.objects.all()) + self.assertEqual(len(table.rows), 1) + + def test_row_count_with_three_objects(self): + self.model_factory.create_batch(3) + table = self.table_class(self.model.objects.all()) + self.assertEqual(len(table.rows), 3) + + class SignedAgreementTableTest(TestCase): model = models.SignedAgreement table_class = tables.SignedAgreementTable diff --git a/primed/cdsa/tests/test_views.py b/primed/cdsa/tests/test_views.py index 6d928f06..1689582b 100644 --- a/primed/cdsa/tests/test_views.py +++ b/primed/cdsa/tests/test_views.py @@ -40,78 +40,1264 @@ User = get_user_model() +class AgreementVersionListTest(TestCase): + """Tests for the AgreementVersionList view.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("cdsa:agreement_versions:list", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.AgreementVersionList.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url()) + self.assertRedirects( + response, + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(), + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user( + username="test-none", password="test-none" + ) + request = self.factory.get(self.get_url()) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_table_class(self): + """The table is the correct class.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + self.assertIsInstance( + response.context_data["table"], tables.AgreementVersionTable + ) + + def test_workspace_table_none(self): + """No rows are shown if there are no AgreementVersion objects.""" + 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), 0) + + def test_workspace_table_three(self): + """Two rows are shown if there are three AgreementVersion objects.""" + factories.AgreementVersionFactory.create_batch(3) + 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) + + +class AgreementMajorVersionDetailTest(TestCase): + """Tests for the AgreementVersionDetail view.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + # Create an object test this with. + self.obj = factories.AgreementMajorVersionFactory.create() + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("cdsa:agreement_versions:major_version_detail", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.AgreementMajorVersionDetail.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url(2)) + self.assertRedirects( + response, + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(2), + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user( + username="test-none", password="test-none" + ) + request = self.factory.get(self.get_url(2)) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request, major_version=2) + + def test_view_status_code_with_existing_object(self): + """Returns a successful status code for an existing object pk.""" + # Only clients load the template. + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + + def test_view_status_code_with_invalid_version(self): + """Raises a 404 error with an invalid major and minor version.""" + request = self.factory.get(self.get_url(self.obj.version + 1)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()( + request, + major_version=self.obj.version + 1, + ) + + def test_context_table_classes(self): + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + self.assertIn("tables", response.context_data) + self.assertEqual(len(response.context_data["tables"]), 2) + self.assertIsInstance( + response.context_data["tables"][0], tables.AgreementVersionTable + ) + self.assertIsInstance( + response.context_data["tables"][1], tables.SignedAgreementTable + ) + + def test_response_includes_agreement_version_table(self): + """agreement_version_table includes agreement_versions with this major version.""" + factories.AgreementVersionFactory.create(major_version=self.obj) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(len(response.context_data["tables"][0].rows), 1) + + def test_response_includes_agreement_version_table_other_major_version(self): + """agreement_version_table includes only agreement_versions with this major version.""" + other_agreement = factories.AgreementVersionFactory.create( + major_version__version=self.obj.version + 1 + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(len(response.context_data["tables"][0].rows), 0) + self.assertNotIn(other_agreement, response.context_data["tables"][0].data) + + def test_response_signed_agreement_table_three_agreements(self): + """signed_agreement_table includes all types of agreements.""" + factories.MemberAgreementFactory.create( + signed_agreement__version__major_version__version=self.obj.version + ) + factories.DataAffiliateAgreementFactory.create( + signed_agreement__version__major_version__version=self.obj.version + ) + factories.NonDataAffiliateAgreementFactory.create( + signed_agreement__version__major_version__version=self.obj.version + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context_data["tables"][1].rows), 3) + + def test_response_signed_agreement_table_other_major_version(self): + """signed_agreement_table does not include agreements from other versions.""" + factories.MemberAgreementFactory.create() + factories.DataAffiliateAgreementFactory.create() + factories.NonDataAffiliateAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context_data["tables"][1].rows), 0) + + def test_response_show_deprecation_message_valid(self): + """response context does not show a deprecation warning when AgreementMajorVersion is valid.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertFalse(response.context_data["show_deprecation_message"]) + self.assertNotIn(b"Deprecated", response.content) + + def test_response_show_deprecation_message_not_valid(self): + """response context does show a deprecation warning when AgreementMajorVersion is is not valid.""" + self.obj.is_valid = False + self.obj.save() + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertTrue(response.context_data["show_deprecation_message"]) + self.assertIn(b"Deprecated", response.content) + + def test_invalidate_button_valid_user_has_edit_perm(self): + """Invalidate button appears when the user has edit permission and the instance is valid.""" + user = User.objects.create_user(username="test_edit", password="test_edit") + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_invalidate_button", response.context_data) + self.assertTrue(response.context_data["show_invalidate_button"]) + self.assertContains( + response, + reverse("cdsa:agreement_versions:invalidate", args=[self.obj.version]), + ) + + def test_invalidate_button_valid_user_has_view_perm(self): + """Invalidate button does not appear when the user has view permission and the instance is valid.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_invalidate_button", response.context_data) + self.assertFalse(response.context_data["show_invalidate_button"]) + self.assertNotContains( + response, + reverse("cdsa:agreement_versions:invalidate", args=[self.obj.version]), + ) + + def test_invalidate_button_invalid_user_has_edit_perm(self): + """Invalidate button does not appear when the user has edit permission and the instance is invalid.""" + self.obj.is_valid = False + self.obj.save() + user = User.objects.create_user(username="test_edit", password="test_edit") + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_invalidate_button", response.context_data) + self.assertFalse(response.context_data["show_invalidate_button"]) + self.assertNotContains( + response, + reverse("cdsa:agreement_versions:invalidate", args=[self.obj.version]), + ) + + def test_invalidate_button_invalid_user_has_view_perm(self): + """Invalidate button does not appear when the user has view permission and the instance is invalid.""" + self.obj.is_valid = False + self.obj.save() + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.version)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_invalidate_button", response.context_data) + self.assertFalse(response.context_data["show_invalidate_button"]) + self.assertNotContains( + response, + reverse("cdsa:agreement_versions:invalidate", args=[self.obj.version]), + ) + + +class AgreementMajorVersionInvalidateTest(TestCase): + """Tests for the AgreementMajorVersionInvalidate view.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("cdsa:agreement_versions:invalidate", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.AgreementMajorVersionInvalidate.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url(1)) + self.assertRedirects( + response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(1) + ) + + def test_status_code_with_user_permission_edit(self): + """Returns successful response code.""" + instance = factories.AgreementMajorVersionFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.version)) + self.assertEqual(response.status_code, 200) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user( + username="test-none", password="test-none" + ) + request = self.factory.get(self.get_url(1)) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request, major_version=1) + + def test_access_without_user_permission_view(self): + """Raises permission denied if user has only view permission.""" + instance = factories.AgreementMajorVersionFactory.create() + user_view_perm = User.objects.create_user( + username="test-none", password="test-none" + ) + user_view_perm.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + request = self.factory.get(self.get_url(instance.version)) + request.user = user_view_perm + with self.assertRaises(PermissionDenied): + self.get_view()(request, major_version=instance.version) + + def test_object_does_not_exist(self): + request = self.factory.get(self.get_url(1)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, major_version=1) + + def test_has_object_in_context(self): + """Response includes a form.""" + instance = factories.AgreementMajorVersionFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.version)) + self.assertTrue("object" in response.context_data) + self.assertEqual(response.context_data["object"], instance) + + def test_has_form_in_context(self): + """Response includes a form.""" + instance = factories.AgreementMajorVersionFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.version)) + self.assertTrue("form" in response.context_data) + + def test_form_class(self): + """Form is the expected class.""" + instance = factories.AgreementMajorVersionFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.version)) + self.assertIsInstance( + response.context_data["form"], forms.AgreementMajorVersionIsValidForm + ) + + def test_invalidates_instance(self): + """Can invalidate the instance.""" + instance = factories.AgreementMajorVersionFactory.create() + self.client.force_login(self.user) + response = self.client.post(self.get_url(instance.version), {}) + self.assertEqual(response.status_code, 302) + instance.refresh_from_db() + self.assertFalse(instance.is_valid) + + def test_sets_one_signed_agreement_to_lapsed(self): + """Sets SignedAgreements associated with this major version to LAPSED.""" + instance = factories.AgreementMajorVersionFactory.create() + signed_agreement = factories.SignedAgreementFactory.create( + version__major_version=instance + ) + self.client.force_login(self.user) + response = self.client.post(self.get_url(instance.version), {}) + self.assertEqual(response.status_code, 302) + signed_agreement.refresh_from_db() + self.assertEqual( + signed_agreement.status, models.SignedAgreement.StatusChoices.LAPSED + ) + + def test_sets_two_signed_agreements_to_lapsed(self): + """Sets SignedAgreements associated with this major version to LAPSED.""" + instance = factories.AgreementMajorVersionFactory.create() + signed_agreement_1 = factories.SignedAgreementFactory.create( + version__major_version=instance + ) + signed_agreement_2 = factories.SignedAgreementFactory.create( + version__major_version=instance + ) + self.client.force_login(self.user) + response = self.client.post(self.get_url(instance.version), {}) + self.assertEqual(response.status_code, 302) + signed_agreement_1.refresh_from_db() + self.assertEqual( + signed_agreement_1.status, models.SignedAgreement.StatusChoices.LAPSED + ) + signed_agreement_2.refresh_from_db() + self.assertEqual( + signed_agreement_2.status, models.SignedAgreement.StatusChoices.LAPSED + ) + + def test_only_sets_active_signed_agreements_to_lapsed(self): + """Does not set SignedAgreements with a different status to LAPSED.""" + instance = factories.AgreementMajorVersionFactory.create() + withdrawn_agreement = factories.SignedAgreementFactory.create( + version__major_version=instance, + status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + lapsed_agreement = factories.SignedAgreementFactory.create( + version__major_version=instance, + status=models.SignedAgreement.StatusChoices.LAPSED, + ) + self.client.force_login(self.user) + response = self.client.post(self.get_url(instance.version), {}) + self.assertEqual(response.status_code, 302) + lapsed_agreement.refresh_from_db() + self.assertEqual( + lapsed_agreement.status, models.SignedAgreement.StatusChoices.LAPSED + ) + withdrawn_agreement.refresh_from_db() + self.assertEqual( + withdrawn_agreement.status, models.SignedAgreement.StatusChoices.WITHDRAWN + ) + + def test_only_sets_associated_signed_agreements_to_lapsed(self): + """Does not set SignedAgreements associated with a different version to LAPSED.""" + instance = factories.AgreementMajorVersionFactory.create() + signed_agreement = factories.SignedAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.post(self.get_url(instance.version), {}) + self.assertEqual(response.status_code, 302) + signed_agreement.refresh_from_db() + self.assertEqual( + signed_agreement.status, models.SignedAgreement.StatusChoices.ACTIVE + ) + + def test_redirect_url(self): + """Redirects to successful url.""" + instance = factories.AgreementMajorVersionFactory.create() + self.client.force_login(self.user) + response = self.client.post(self.get_url(instance.version), {}) + self.assertRedirects(response, instance.get_absolute_url()) + + def test_success_message(self): + """Redirects to successful url.""" + instance = factories.AgreementMajorVersionFactory.create() + self.client.force_login(self.user) + response = self.client.post(self.get_url(instance.version), {}) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual( + views.AgreementMajorVersionInvalidate.success_message, str(messages[0]) + ) + + def test_version_already_invalid_get(self): + instance = factories.AgreementMajorVersionFactory.create(is_valid=False) + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.version)) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual( + views.AgreementMajorVersionInvalidate.ERROR_ALREADY_INVALID, + str(messages[0]), + ) + + def test_version_already_invalid_post(self): + instance = factories.AgreementMajorVersionFactory.create(is_valid=False) + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.version), {}) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual( + views.AgreementMajorVersionInvalidate.ERROR_ALREADY_INVALID, + str(messages[0]), + ) + + +class AgreementVersionDetailTest(TestCase): + """Tests for the AgreementVersionDetail view.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + # Create an object test this with. + self.obj = factories.AgreementVersionFactory.create() + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("cdsa:agreement_versions:detail", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.AgreementVersionDetail.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url(2, 5)) + self.assertRedirects( + response, + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(2, 5), + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get( + self.get_url(self.obj.major_version.version, self.obj.minor_version) + ) + self.assertEqual(response.status_code, 200) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user( + username="test-none", password="test-none" + ) + request = self.factory.get(self.get_url(2, 5)) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request, major_version=2, minor_version=5) + + def test_view_status_code_with_existing_object(self): + """Returns a successful status code for an existing object pk.""" + # Only clients load the template. + self.client.force_login(self.user) + response = self.client.get( + self.get_url(self.obj.major_version.version, self.obj.minor_version) + ) + self.assertEqual(response.status_code, 200) + + def test_view_status_code_with_invalid_version(self): + """Raises a 404 error with an invalid major and minor version.""" + request = self.factory.get( + self.get_url(self.obj.major_version.version + 1, self.obj.minor_version + 1) + ) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()( + request, + major_version=self.obj.major_version.version + 1, + minor_version=self.obj.minor_version + 1, + ) + + def test_view_status_code_with_other_major_version(self): + """Raises a 404 error with an invalid object major version.""" + request = self.factory.get( + self.get_url(self.obj.major_version.version + 1, self.obj.minor_version) + ) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()( + request, + major_version__version=self.obj.major_version.version + 1, + minor_version=self.obj.minor_version, + ) + + def test_view_status_code_with_other_minor_version(self): + """Raises a 404 error with an invalid object minor version.""" + request = self.factory.get( + self.get_url(self.obj.major_version.version, self.obj.minor_version + 1) + ) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()( + request, + major_version=self.obj.major_version.version, + minor_version=self.obj.minor_version + 1, + ) + + # def test_response_includes_link_to_major_agreement(self): + # """Response includes a link to the user profile page.""" + # self.client.force_login(self.user) + # response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + # self.assertContains( + # response, self.obj.signed_agreement.representative.get_absolute_url() + # ) + + def test_response_includes_signed_agreement_table(self): + """Response includes a table of SignedAgreements.""" + self.client.force_login(self.user) + response = self.client.get( + self.get_url(self.obj.major_version.version, self.obj.minor_version) + ) + self.assertEqual(response.status_code, 200) + self.assertIn("signed_agreement_table", response.context_data) + self.assertIsInstance( + response.context_data["signed_agreement_table"], tables.SignedAgreementTable + ) + + def test_response_signed_agreement_table_three_agreements(self): + """signed_agreement_table includes all types of agreements.""" + member_agreement = factories.MemberAgreementFactory.create( + signed_agreement__version=self.obj + ) + da_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__version=self.obj + ) + nda_agreement = factories.NonDataAffiliateAgreementFactory.create( + signed_agreement__version=self.obj + ) + self.client.force_login(self.user) + response = self.client.get( + self.get_url(self.obj.major_version.version, self.obj.minor_version) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context_data["signed_agreement_table"].rows), 3) + self.assertIn( + member_agreement.signed_agreement, + response.context_data["signed_agreement_table"].data, + ) + self.assertIn( + da_agreement.signed_agreement, + response.context_data["signed_agreement_table"].data, + ) + self.assertIn( + nda_agreement.signed_agreement, + response.context_data["signed_agreement_table"].data, + ) + + def test_response_signed_agreement_table_other_version(self): + """signed_agreement_table does not include agreements from other versions.""" + member_agreement = factories.MemberAgreementFactory.create() + da_agreement = factories.DataAffiliateAgreementFactory.create() + nda_agreement = factories.NonDataAffiliateAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.get( + self.get_url(self.obj.major_version.version, self.obj.minor_version) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context_data["signed_agreement_table"].rows), 0) + self.assertNotIn( + member_agreement.signed_agreement, + response.context_data["signed_agreement_table"].data, + ) + self.assertNotIn( + da_agreement.signed_agreement, + response.context_data["signed_agreement_table"].data, + ) + self.assertNotIn( + nda_agreement.signed_agreement, + response.context_data["signed_agreement_table"].data, + ) + + def test_response_show_deprecation_message_valid(self): + """response context does not show a deprecation warning when AgreementMajorVersion is valid.""" + self.client.force_login(self.user) + response = self.client.get( + self.get_url(self.obj.major_version.version, self.obj.minor_version) + ) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertFalse(response.context_data["show_deprecation_message"]) + self.assertNotIn(b"Deprecated", response.content) + + def test_response_show_deprecation_message_not_valid(self): + """response context does show a deprecation warning when AgreementMajorVersion is not valid.""" + self.obj.major_version.is_valid = False + self.obj.major_version.save() + self.client.force_login(self.user) + response = self.client.get( + self.get_url(self.obj.major_version.version, self.obj.minor_version) + ) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertTrue(response.context_data["show_deprecation_message"]) + self.assertIn(b"Deprecated", response.content) + + class SignedAgreementListTest(TestCase): """Tests for the SignedAgreementList view.""" def setUp(self): """Set up test class.""" self.factory = RequestFactory() - # Create a user with both view and edit permission. + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("cdsa:signed_agreements:list", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.SignedAgreementList.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url()) + self.assertRedirects( + response, + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(), + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user( + username="test-none", password="test-none" + ) + request = self.factory.get(self.get_url()) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_table_class(self): + """The table is the correct class.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + self.assertIsInstance( + response.context_data["table"], tables.SignedAgreementTable + ) + + def test_workspace_table_none(self): + """No rows are shown if there are no SignedAgreement objects.""" + 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), 0) + + def test_workspace_table_three(self): + """Two rows are shown if there are three SignedAgreement objects.""" + factories.MemberAgreementFactory.create() + factories.DataAffiliateAgreementFactory.create() + factories.NonDataAffiliateAgreementFactory.create() + 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) + + +class SignedAgreementStatusUpdateMemberTest(TestCase): + """Tests for the SignedAgreementStatusUpdate view with a MemberAgreement.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with both view and edit permissions. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("cdsa:signed_agreements:members:update", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.SignedAgreementStatusUpdate.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url(1)) + self.assertRedirects( + response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(1) + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + instance = factories.MemberAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + + def test_access_with_view_permission(self): + """Raises permission denied if user has only view permission.""" + user_with_view_perm = User.objects.create_user( + username="test-other", password="test-other" + ) + user_with_view_perm.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + request = self.factory.get(self.get_url(1)) + request.user = user_with_view_perm + with self.assertRaises(PermissionDenied): + self.get_view()(request, cc_id=1) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user( + username="test-none", password="test-none" + ) + request = self.factory.get(self.get_url(1)) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request, cc_id=1) + + def test_object_does_not_exist(self): + """Raises Http404 if object does not exist.""" + request = self.factory.get(self.get_url(1)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, cc_id=1) + + def test_object_different_agreement_type(self): + """Raises Http404 if object has a different agreement type.""" + instance = factories.DataAffiliateAgreementFactory.create() + request = self.factory.get(self.get_url(instance.signed_agreement.cc_id)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, cc_id=instance.signed_agreement.cc_id) + + def test_has_form_in_context(self): + """Response includes a form.""" + instance = factories.MemberAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.signed_agreement.cc_id)) + self.assertTrue("form" in response.context_data) + self.assertIsInstance( + response.context_data["form"], forms.SignedAgreementStatusForm + ) + + def test_can_modify_status(self): + """Can change the status.""" + instance = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), + {"status": models.SignedAgreement.StatusChoices.WITHDRAWN}, + ) + self.assertEqual(response.status_code, 302) + instance.refresh_from_db() + self.assertEqual( + instance.signed_agreement.status, + models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + + def test_invalid_status(self): + """Can change the status.""" + instance = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), {"status": "foo"} + ) + self.assertEqual(response.status_code, 200) + self.assertIn("form", response.context) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("status", form.errors) + self.assertEqual(len(form.errors["status"]), 1) + self.assertIn("valid choice", form.errors["status"][0]) + instance.refresh_from_db() + self.assertEqual( + instance.signed_agreement.status, + models.SignedAgreement.StatusChoices.ACTIVE, + ) + + def test_success_message(self): + """Response includes a success message if successful.""" + instance = factories.MemberAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), + {"status": models.SignedAgreement.StatusChoices.WITHDRAWN}, + follow=True, + ) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual( + views.SignedAgreementStatusUpdate.success_message, str(messages[0]) + ) + + def test_redirects_to_object_detail(self): + """After successfully creating an object, view redirects to the object's detail page.""" + # This needs to use the client because the RequestFactory doesn't handle redirects. + instance = factories.MemberAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), + {"status": models.SignedAgreement.StatusChoices.WITHDRAWN}, + ) + self.assertRedirects(response, instance.get_absolute_url()) + + +class SignedAgreementStatusUpdateDataAffiliateTest(TestCase): + """Tests for the SignedAgreementStatusUpdate view with a DataAffiliateAgreement.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with both view and edit permissions. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("cdsa:signed_agreements:data_affiliates:update", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.SignedAgreementStatusUpdate.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url(1)) + self.assertRedirects( + response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(1) + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + instance = factories.DataAffiliateAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + + def test_access_with_view_permission(self): + """Raises permission denied if user has only view permission.""" + user_with_view_perm = User.objects.create_user( + username="test-other", password="test-other" + ) + user_with_view_perm.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + request = self.factory.get(self.get_url(1)) + request.user = user_with_view_perm + with self.assertRaises(PermissionDenied): + self.get_view()(request, cc_id=1) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user( + username="test-none", password="test-none" + ) + request = self.factory.get(self.get_url(1)) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request, cc_id=1) + + def test_object_does_not_exist(self): + """Raises Http404 if object does not exist.""" + request = self.factory.get(self.get_url(1)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, cc_id=1) + + def test_object_different_agreement_type(self): + """Raises Http404 if object has a different agreement type.""" + instance = factories.MemberAgreementFactory.create() + request = self.factory.get(self.get_url(instance.signed_agreement.cc_id)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, cc_id=instance.signed_agreement.cc_id) + + def test_has_form_in_context(self): + """Response includes a form.""" + instance = factories.DataAffiliateAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(instance.signed_agreement.cc_id)) + self.assertTrue("form" in response.context_data) + self.assertIsInstance( + response.context_data["form"], forms.SignedAgreementStatusForm + ) + + def test_can_modify_status(self): + """Can change the status.""" + instance = factories.DataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), + {"status": models.SignedAgreement.StatusChoices.WITHDRAWN}, + ) + self.assertEqual(response.status_code, 302) + instance.refresh_from_db() + self.assertEqual( + instance.signed_agreement.status, + models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + + def test_invalid_status(self): + """Can change the status.""" + instance = factories.DataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), {"status": "foo"} + ) + self.assertEqual(response.status_code, 200) + self.assertIn("form", response.context) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("status", form.errors) + self.assertEqual(len(form.errors["status"]), 1) + self.assertIn("valid choice", form.errors["status"][0]) + instance.refresh_from_db() + self.assertEqual( + instance.signed_agreement.status, + models.SignedAgreement.StatusChoices.ACTIVE, + ) + + def test_success_message(self): + """Response includes a success message if successful.""" + instance = factories.DataAffiliateAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), + {"status": models.SignedAgreement.StatusChoices.WITHDRAWN}, + follow=True, + ) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual( + views.SignedAgreementStatusUpdate.success_message, str(messages[0]) + ) + + def test_redirects_to_object_detail(self): + """After successfully creating an object, view redirects to the object's detail page.""" + # This needs to use the client because the RequestFactory doesn't handle redirects. + instance = factories.DataAffiliateAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), + {"status": models.SignedAgreement.StatusChoices.WITHDRAWN}, + ) + self.assertRedirects(response, instance.get_absolute_url()) + + +class SignedAgreementStatusUpdateNonDataAffiliateTest(TestCase): + """Tests for the SignedAgreementStatusUpdate view with a NonDataAffiliateAgreement.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with both view and edit permissions. self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get( codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME ) ) + self.user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:list", args=args) + return reverse("cdsa:signed_agreements:non_data_affiliates:update", args=args) def get_view(self): """Return the view being tested.""" - return views.SignedAgreementList.as_view() + return views.SignedAgreementStatusUpdate.as_view() def test_view_redirect_not_logged_in(self): "View redirects to login view when user is not logged in." # Need a client for redirects. - response = self.client.get(self.get_url()) + response = self.client.get(self.get_url(1)) self.assertRedirects( - response, - resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(), + response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(1) ) def test_status_code_with_user_permission(self): """Returns successful response code.""" + instance = factories.NonDataAffiliateAgreementFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url()) + response = self.client.get(self.get_url(instance.signed_agreement.cc_id)) self.assertEqual(response.status_code, 200) + def test_access_with_view_permission(self): + """Raises permission denied if user has only view permission.""" + user_with_view_perm = User.objects.create_user( + username="test-other", password="test-other" + ) + user_with_view_perm.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + request = self.factory.get(self.get_url(1)) + request.user = user_with_view_perm + with self.assertRaises(PermissionDenied): + self.get_view()(request, cc_id=1) + def test_access_without_user_permission(self): """Raises permission denied if user has no permissions.""" user_no_perms = User.objects.create_user( username="test-none", password="test-none" ) - request = self.factory.get(self.get_url()) + request = self.factory.get(self.get_url(1)) request.user = user_no_perms with self.assertRaises(PermissionDenied): - self.get_view()(request) + self.get_view()(request, cc_id=1) - def test_table_class(self): - """The table is the correct class.""" + def test_object_does_not_exist(self): + """Raises Http404 if object does not exist.""" + request = self.factory.get(self.get_url(1)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, cc_id=1) + + def test_object_different_agreement_type(self): + """Raises Http404 if object has a different agreement type.""" + instance = factories.MemberAgreementFactory.create() + request = self.factory.get(self.get_url(instance.signed_agreement.cc_id)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, cc_id=instance.signed_agreement.cc_id) + + def test_has_form_in_context(self): + """Response includes a form.""" + instance = factories.NonDataAffiliateAgreementFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("table", response.context_data) + response = self.client.get(self.get_url(instance.signed_agreement.cc_id)) + self.assertTrue("form" in response.context_data) self.assertIsInstance( - response.context_data["table"], tables.SignedAgreementTable + response.context_data["form"], forms.SignedAgreementStatusForm ) - def test_workspace_table_none(self): - """No rows are shown if there are no SignedAgreement objects.""" + def test_can_modify_status(self): + """Can change the status.""" + instance = factories.NonDataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE + ) 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), 0) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), + {"status": models.SignedAgreement.StatusChoices.WITHDRAWN}, + ) + self.assertEqual(response.status_code, 302) + instance.refresh_from_db() + self.assertEqual( + instance.signed_agreement.status, + models.SignedAgreement.StatusChoices.WITHDRAWN, + ) - def test_workspace_table_three(self): - """Two rows are shown if there are three SignedAgreement objects.""" - factories.MemberAgreementFactory.create() - factories.DataAffiliateAgreementFactory.create() - factories.NonDataAffiliateAgreementFactory.create() + def test_invalid_status(self): + """Can change the status.""" + instance = factories.NonDataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.ACTIVE + ) 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) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), {"status": "foo"} + ) + self.assertEqual(response.status_code, 200) + self.assertIn("form", response.context) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("status", form.errors) + self.assertEqual(len(form.errors["status"]), 1) + self.assertIn("valid choice", form.errors["status"][0]) + instance.refresh_from_db() + self.assertEqual( + instance.signed_agreement.status, + models.SignedAgreement.StatusChoices.ACTIVE, + ) + + def test_success_message(self): + """Response includes a success message if successful.""" + instance = factories.NonDataAffiliateAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), + {"status": models.SignedAgreement.StatusChoices.WITHDRAWN}, + follow=True, + ) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual( + views.SignedAgreementStatusUpdate.success_message, str(messages[0]) + ) + + def test_redirects_to_object_detail(self): + """After successfully creating an object, view redirects to the object's detail page.""" + # This needs to use the client because the RequestFactory doesn't handle redirects. + instance = factories.NonDataAffiliateAgreementFactory.create() + self.client.force_login(self.user) + response = self.client.post( + self.get_url(instance.signed_agreement.cc_id), + {"status": models.SignedAgreement.StatusChoices.WITHDRAWN}, + ) + self.assertRedirects(response, instance.get_absolute_url()) class MemberAgreementCreateTest(AnVILAPIMockTestMixin, TestCase): @@ -136,7 +1322,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:members:new", args=args) + return reverse("cdsa:signed_agreements:members:new", args=args) def get_view(self): """Return the view being tested.""" @@ -246,6 +1432,9 @@ def test_can_create_object(self): self.assertEqual( new_agreement.anvil_access_group.name, "TEST_PRIMED_CDSA_ACCESS_1234" ) + self.assertEqual( + new_agreement.status, models.SignedAgreement.StatusChoices.ACTIVE + ) # Check the agreement type. self.assertEqual(models.MemberAgreement.objects.count(), 1) new_agreement_type = models.MemberAgreement.objects.latest("pk") @@ -977,7 +2166,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:members:detail", args=args) + return reverse("cdsa:signed_agreements:members:detail", args=args) def get_view(self): """Return the view being tested.""" @@ -1044,6 +2233,67 @@ def test_response_includes_link_to_anvil_access_group(self): response, self.obj.signed_agreement.anvil_access_group.get_absolute_url() ) + def test_response_show_deprecation_message_valid(self): + """response context does not show a deprecation warning when AgreementMajorVersion is valid.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertFalse(response.context_data["show_deprecation_message"]) + self.assertNotIn(b"Deprecated CDSA version", response.content) + + def test_response_show_deprecation_message_not_valid(self): + """response context does show a deprecation warning when AgreementMajorVersion is not valid.""" + self.obj.signed_agreement.version.major_version.is_valid = False + self.obj.signed_agreement.version.major_version.save() + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertTrue(response.context_data["show_deprecation_message"]) + self.assertIn(b"Deprecated CDSA version", response.content) + + def test_change_status_button_user_has_edit_perm(self): + """Invalidate button appears when the user has edit permission and the instance is valid.""" + user = User.objects.create_user(username="test_edit", password="test_edit") + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_update_button", response.context_data) + self.assertTrue(response.context_data["show_update_button"]) + self.assertContains( + response, + reverse( + "cdsa:signed_agreements:members:update", + args=[self.obj.signed_agreement.cc_id], + ), + ) + + def test_change_status_button_user_has_view_perm(self): + """Invalidate button does not appear when the user has view permission and the instance is valid.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_update_button", response.context_data) + self.assertFalse(response.context_data["show_update_button"]) + self.assertNotContains( + response, + reverse( + "cdsa:signed_agreements:members:update", + args=[self.obj.signed_agreement.cc_id], + ), + ) + class MemberAgreementListTest(TestCase): """Tests for the MemberAgreementList view.""" @@ -1061,7 +2311,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:members:list", args=args) + return reverse("cdsa:signed_agreements:members:list", args=args) def get_view(self): """Return the view being tested.""" @@ -1139,7 +2389,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:data_affiliates:new", args=args) + return reverse("cdsa:signed_agreements:data_affiliates:new", args=args) def get_view(self): """Return the view being tested.""" @@ -1256,6 +2506,9 @@ def test_can_create_object(self): self.assertEqual( new_agreement.anvil_access_group.name, "TEST_PRIMED_CDSA_ACCESS_1234" ) + self.assertEqual( + new_agreement.status, models.SignedAgreement.StatusChoices.ACTIVE + ) # Check the agreement type. self.assertEqual(models.DataAffiliateAgreement.objects.count(), 1) new_agreement_type = models.DataAffiliateAgreement.objects.latest("pk") @@ -2125,7 +3378,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:data_affiliates:detail", args=args) + return reverse("cdsa:signed_agreements:data_affiliates:detail", args=args) def get_view(self): """Return the view being tested.""" @@ -2198,6 +3451,67 @@ def test_response_includes_link_to_anvil_upload_group(self): response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) self.assertContains(response, self.obj.anvil_upload_group.get_absolute_url()) + def test_response_show_deprecation_message_valid(self): + """response context does not show a deprecation warning when AgreementMajorVersion is valid.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertFalse(response.context_data["show_deprecation_message"]) + self.assertNotIn(b"Deprecated CDSA version", response.content) + + def test_response_show_deprecation_message_not_valid(self): + """response context does show a deprecation warning when AgreementMajorVersion is not valid.""" + self.obj.signed_agreement.version.major_version.is_valid = False + self.obj.signed_agreement.version.major_version.save() + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertTrue(response.context_data["show_deprecation_message"]) + self.assertIn(b"Deprecated CDSA version", response.content) + + def test_change_status_button_user_has_edit_perm(self): + """Invalidate button appears when the user has edit permission and the instance is valid.""" + user = User.objects.create_user(username="test_edit", password="test_edit") + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_update_button", response.context_data) + self.assertTrue(response.context_data["show_update_button"]) + self.assertContains( + response, + reverse( + "cdsa:signed_agreements:data_affiliates:update", + args=[self.obj.signed_agreement.cc_id], + ), + ) + + def test_change_status_button_user_has_view_perm(self): + """Invalidate button does not appear when the user has view permission and the instance is valid.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_update_button", response.context_data) + self.assertFalse(response.context_data["show_update_button"]) + self.assertNotContains( + response, + reverse( + "cdsa:signed_agreements:data_affiliates:update", + args=[self.obj.signed_agreement.cc_id], + ), + ) + class DataAffiliateAgreementListTest(TestCase): """Tests for the DataAffiliateAgreement view.""" @@ -2215,7 +3529,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:data_affiliates:list", args=args) + return reverse("cdsa:signed_agreements:data_affiliates:list", args=args) def get_view(self): """Return the view being tested.""" @@ -2293,7 +3607,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:non_data_affiliates:new", args=args) + return reverse("cdsa:signed_agreements:non_data_affiliates:new", args=args) def get_view(self): """Return the view being tested.""" @@ -2403,6 +3717,9 @@ def test_can_create_object(self): self.assertEqual( new_agreement.anvil_access_group.name, "TEST_PRIMED_CDSA_ACCESS_1234" ) + self.assertEqual( + new_agreement.status, models.SignedAgreement.StatusChoices.ACTIVE + ) # Check the agreement type. self.assertEqual(models.NonDataAffiliateAgreement.objects.count(), 1) new_agreement_type = models.NonDataAffiliateAgreement.objects.latest("pk") @@ -3118,7 +4435,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:non_data_affiliates:detail", args=args) + return reverse("cdsa:signed_agreements:non_data_affiliates:detail", args=args) def get_view(self): """Return the view being tested.""" @@ -3179,6 +4496,67 @@ def test_response_includes_link_to_anvil_access_group(self): response, self.obj.signed_agreement.anvil_access_group.get_absolute_url() ) + def test_response_show_deprecation_message_valid(self): + """response context does not show a deprecation warning when AgreementMajorVersion is valid.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertFalse(response.context_data["show_deprecation_message"]) + self.assertNotIn(b"Deprecated CDSA version", response.content) + + def test_response_show_deprecation_message_is_not_valid(self): + """response context does show a deprecation warning when AgreementMajorVersion is not valid.""" + self.obj.signed_agreement.version.major_version.is_valid = False + self.obj.signed_agreement.version.major_version.save() + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_deprecation_message", response.context_data) + self.assertTrue(response.context_data["show_deprecation_message"]) + self.assertIn(b"Deprecated CDSA version", response.content) + + def test_change_status_button_user_has_edit_perm(self): + """Invalidate button appears when the user has edit permission and the instance is valid.""" + user = User.objects.create_user(username="test_edit", password="test_edit") + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME + ) + ) + user.user_permissions.add( + Permission.objects.get( + codename=AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + ) + self.client.force_login(user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_update_button", response.context_data) + self.assertTrue(response.context_data["show_update_button"]) + self.assertContains( + response, + reverse( + "cdsa:signed_agreements:non_data_affiliates:update", + args=[self.obj.signed_agreement.cc_id], + ), + ) + + def test_change_status_button_user_has_view_perm(self): + """Invalidate button does not appear when the user has view permission and the instance is valid.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.obj.signed_agreement.cc_id)) + self.assertEqual(response.status_code, 200) + self.assertIn("show_update_button", response.context_data) + self.assertFalse(response.context_data["show_update_button"]) + self.assertNotContains( + response, + reverse( + "cdsa:signed_agreements:non_data_affiliates:update", + args=[self.obj.signed_agreement.cc_id], + ), + ) + class NonDataAffiliateAgreementListTest(TestCase): """Tests for the NonDataAffiliateAgreement view.""" @@ -3196,7 +4574,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("cdsa:agreements:non_data_affiliates:list", args=args) + return reverse("cdsa:signed_agreements:non_data_affiliates:list", args=args) def get_view(self): """Return the view being tested.""" @@ -3338,6 +4716,23 @@ def test_table_three_rows(self): self.assertIn("table", response.context_data) self.assertEqual(len(response.context_data["table"].rows), 3) + def test_only_includes_active_agreements(self): + active_agreement = factories.MemberAgreementFactory.create() + lapsed_agreement = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.LAPSED + ) + withdrawn_agreement = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + table = response.context_data["table"] + self.assertEqual(len(table.rows), 1) + self.assertIn(active_agreement.signed_agreement, table.data) + self.assertNotIn(lapsed_agreement.signed_agreement, table.data) + self.assertNotIn(withdrawn_agreement.signed_agreement, table.data) + class SignedAgreementAuditTest(TestCase): """Tests for the SignedAgreementAudit view.""" @@ -3358,7 +4753,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" return reverse( - "cdsa:audit:agreements", + "cdsa:audit:signed_agreements", args=args, ) @@ -3425,14 +4820,14 @@ def test_context_verified_table_access(self): ) self.assertEqual( table.rows[0].get_cell_value("note"), - signed_agreement_audit.SignedAgreementAccessAudit.VALID_PRIMARY_CDSA, + signed_agreement_audit.SignedAgreementAccessAudit.ACTIVE_PRIMARY_AGREEMENT, ) self.assertIsNone(table.rows[0].get_cell_value("action")) def test_context_verified_table_no_access(self): """verified_table shows a record when audit has verified no access.""" member_agreement = factories.MemberAgreementFactory.create( - signed_agreement__is_primary=False + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN ) # Check the table in the context. self.client.force_login(self.user) @@ -3450,7 +4845,7 @@ def test_context_verified_table_no_access(self): ) self.assertEqual( table.rows[0].get_cell_value("note"), - signed_agreement_audit.SignedAgreementAccessAudit.NO_PRIMARY_CDSA, + signed_agreement_audit.SignedAgreementAccessAudit.INACTIVE_AGREEMENT, ) self.assertIsNone(table.rows[0].get_cell_value("action")) @@ -3473,7 +4868,7 @@ def test_context_needs_action_table_grant(self): ) self.assertEqual( table.rows[0].get_cell_value("note"), - signed_agreement_audit.SignedAgreementAccessAudit.VALID_PRIMARY_CDSA, + signed_agreement_audit.SignedAgreementAccessAudit.ACTIVE_PRIMARY_AGREEMENT, ) self.assertIsNotNone(table.rows[0].get_cell_value("action")) @@ -3501,7 +4896,7 @@ def test_context_error_table_has_access(self): ) self.assertEqual( table.rows[0].get_cell_value("note"), - signed_agreement_audit.SignedAgreementAccessAudit.NO_PRIMARY_CDSA, + signed_agreement_audit.SignedAgreementAccessAudit.NO_PRIMARY_AGREEMENT, ) self.assertIsNotNone(table.rows[0].get_cell_value("action")) @@ -3609,7 +5004,7 @@ def test_context_verified_table_access(self): ) self.assertEqual( table.rows[0].get_cell_value("note"), - workspace_audit.WorkspaceAccessAudit.VALID_PRIMARY_CDSA, + workspace_audit.WorkspaceAccessAudit.ACTIVE_PRIMARY_AGREEMENT, ) self.assertIsNone(table.rows[0].get_cell_value("action")) @@ -3633,7 +5028,7 @@ def test_context_verified_table_no_access(self): self.assertIsNone(table.rows[0].get_cell_value("data_affiliate_agreement")) self.assertEqual( table.rows[0].get_cell_value("note"), - workspace_audit.WorkspaceAccessAudit.NO_PRIMARY_CDSA, + workspace_audit.WorkspaceAccessAudit.NO_PRIMARY_AGREEMENT, ) self.assertIsNone(table.rows[0].get_cell_value("action")) @@ -3662,7 +5057,7 @@ def test_context_needs_action_table_grant(self): ) self.assertEqual( table.rows[0].get_cell_value("note"), - workspace_audit.WorkspaceAccessAudit.VALID_PRIMARY_CDSA, + workspace_audit.WorkspaceAccessAudit.ACTIVE_PRIMARY_AGREEMENT, ) self.assertIsNotNone(table.rows[0].get_cell_value("action")) @@ -3686,7 +5081,7 @@ def test_context_error_table_has_access(self): self.assertIsNone(table.rows[0].get_cell_value("data_affiliate_agreement")) self.assertEqual( table.rows[0].get_cell_value("note"), - workspace_audit.WorkspaceAccessAudit.NO_PRIMARY_CDSA, + workspace_audit.WorkspaceAccessAudit.NO_PRIMARY_AGREEMENT, ) self.assertIsNotNone(table.rows[0].get_cell_value("action")) @@ -3786,6 +5181,23 @@ def test_only_shows_primary_data_affiliate_records(self): self.assertIn(primary_agreement, table.data) self.assertNotIn(component_agreement, table.data) + def test_only_includes_active_agreements(self): + active_agreement = factories.DataAffiliateAgreementFactory.create() + lapsed_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.LAPSED + ) + withdrawn_agreement = factories.DataAffiliateAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + table = response.context_data["table"] + self.assertEqual(len(table.rows), 1) + self.assertIn(active_agreement, table.data) + self.assertNotIn(lapsed_agreement, table.data) + self.assertNotIn(withdrawn_agreement, table.data) + class UserAccessRecordsList(TestCase): """Tests for the StudyRecordsList view.""" @@ -3945,6 +5357,32 @@ def test_does_not_show_other_group_members(self): table = response.context_data["table"] self.assertEqual(len(table.rows), 0) + def test_only_includes_active_agreements(self): + active_agreement = factories.MemberAgreementFactory.create() + active_member = GroupAccountMembershipFactory.create( + group=active_agreement.signed_agreement.anvil_access_group + ) + lapsed_agreement = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.LAPSED + ) + lapsed_member = GroupAccountMembershipFactory.create( + group=lapsed_agreement.signed_agreement.anvil_access_group + ) + withdrawn_agreement = factories.MemberAgreementFactory.create( + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN + ) + withdrawn_member = GroupAccountMembershipFactory.create( + group=withdrawn_agreement.signed_agreement.anvil_access_group + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + table = response.context_data["table"] + self.assertEqual(len(table.rows), 1) + self.assertIn(active_member, table.data) + self.assertNotIn(lapsed_member, table.data) + self.assertNotIn(withdrawn_member, table.data) + class CDSAWorkspaceRecordsList(TestCase): """Tests for the CDSAWorkspaceRecords view.""" @@ -3987,13 +5425,41 @@ def test_table_no_rows(self): self.assertIn("table", response.context_data) self.assertEqual(len(response.context_data["table"].rows), 0) - def test_table_three_rows(self): + def test_table_two_rows(self): """Three rows are shown if there are three CDSAWorkspaces objects.""" - factories.CDSAWorkspaceFactory.create_batch(3) + active_workspace_1 = factories.CDSAWorkspaceFactory.create() + factories.DataAffiliateAgreementFactory.create(study=active_workspace_1.study) + active_workspace_2 = factories.CDSAWorkspaceFactory.create() + factories.DataAffiliateAgreementFactory.create(study=active_workspace_2.study) 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) + table = response.context_data["table"] + self.assertEqual(len(table.rows), 2) + self.assertIn(active_workspace_1, table.data) + self.assertIn(active_workspace_2, table.data) + + def test_only_includes_workspaces_with_active_agreements(self): + active_workspace = factories.CDSAWorkspaceFactory.create() + factories.DataAffiliateAgreementFactory.create(study=active_workspace.study) + lapsed_workspace = factories.CDSAWorkspaceFactory.create() + factories.DataAffiliateAgreementFactory.create( + study=lapsed_workspace.study, + signed_agreement__status=models.SignedAgreement.StatusChoices.LAPSED, + ) + withdrawn_workspace = factories.CDSAWorkspaceFactory.create() + factories.DataAffiliateAgreementFactory.create( + study=withdrawn_workspace.study, + signed_agreement__status=models.SignedAgreement.StatusChoices.WITHDRAWN, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + table = response.context_data["table"] + self.assertEqual(len(table.rows), 1) + self.assertIn(active_workspace, table.data) + self.assertNotIn(lapsed_workspace, table.data) + self.assertNotIn(withdrawn_workspace, table.data) class CDSAWorkspaceDetailTest(TestCase): diff --git a/primed/cdsa/urls.py b/primed/cdsa/urls.py index a96e7715..742fffd5 100644 --- a/primed/cdsa/urls.py +++ b/primed/cdsa/urls.py @@ -1,14 +1,45 @@ from django.urls import include, path -from . import views +from . import models, views app_name = "cdsa" +agreement_version_patterns = ( + [ + path( + "", + views.AgreementVersionList.as_view(), + name="list", + ), + path( + "v/", + views.AgreementMajorVersionDetail.as_view(), + name="major_version_detail", + ), + path( + "v/invalidate/", + views.AgreementMajorVersionInvalidate.as_view(), + name="invalidate", + ), + path( + "v./", + views.AgreementVersionDetail.as_view(), + name="detail", + ), + ], + "agreement_versions", +) member_agreement_patterns = ( [ path("", views.MemberAgreementList.as_view(), name="list"), path("new/", views.MemberAgreementCreate.as_view(), name="new"), path("/", views.MemberAgreementDetail.as_view(), name="detail"), + path( + "/update/", + views.SignedAgreementStatusUpdate.as_view(), + {"agreement_type": models.SignedAgreement.MEMBER}, + name="update", + ), ], "members", ) @@ -20,6 +51,12 @@ path( "/", views.DataAffiliateAgreementDetail.as_view(), name="detail" ), + path( + "/update/", + views.SignedAgreementStatusUpdate.as_view(), + {"agreement_type": models.SignedAgreement.DATA_AFFILIATE}, + name="update", + ), ], "data_affiliates", ) @@ -33,23 +70,33 @@ views.NonDataAffiliateAgreementDetail.as_view(), name="detail", ), + path( + "/update/", + views.SignedAgreementStatusUpdate.as_view(), + {"agreement_type": models.SignedAgreement.NON_DATA_AFFILIATE}, + name="update", + ), ], "non_data_affiliates", ) -agreement_patterns = ( +signed_agreement_patterns = ( [ path("", views.SignedAgreementList.as_view(), name="list"), path("members/", include(member_agreement_patterns)), path("data_affiliates/", include(data_affiliate_agreement_patterns)), path("non_data_affiliates/", include(non_data_affiliate_agreement_patterns)), ], - "agreements", + "signed_agreements", ) audit_patterns = ( [ - path("agreements/", views.SignedAgreementAudit.as_view(), name="agreements"), + path( + "signed_agreements/", + views.SignedAgreementAudit.as_view(), + name="signed_agreements", + ), path("workspaces/", views.CDSAWorkspaceAudit.as_view(), name="workspaces"), ], "audit", @@ -83,7 +130,8 @@ urlpatterns = [ - path("agreements/", include(agreement_patterns)), + path("agreement_versions/", include(agreement_version_patterns)), + path("signed_agreements/", include(signed_agreement_patterns)), path("records/", include(records_patterns)), path("audit/", include(audit_patterns)), ] diff --git a/primed/cdsa/views.py b/primed/cdsa/views.py index c3c16263..df0c4f8b 100644 --- a/primed/cdsa/views.py +++ b/primed/cdsa/views.py @@ -4,6 +4,7 @@ from anvil_consortium_manager.auth import ( AnVILConsortiumManagerEditRequired, AnVILConsortiumManagerViewRequired, + AnVILProjectManagerAccess, ) from anvil_consortium_manager.models import GroupAccountMembership, ManagedGroup from django.conf import settings @@ -14,8 +15,9 @@ from django.forms import inlineformset_factory from django.http import Http404, HttpResponseRedirect from django.urls import reverse -from django.views.generic import DetailView, FormView, TemplateView -from django_tables2 import SingleTableView +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import DetailView, FormView, TemplateView, UpdateView +from django_tables2 import MultiTableMixin, SingleTableMixin, SingleTableView from . import forms, helpers, models, tables from .audit import signed_agreement_audit, workspace_audit @@ -23,6 +25,155 @@ logger = logging.getLogger(__name__) +class AgreementMajorVersionDetail( + AnVILConsortiumManagerViewRequired, MultiTableMixin, DetailView +): + """Display a "detail" page for an agreement major version (e.g., 1.x).""" + + model = models.AgreementMajorVersion + template_name = "cdsa/agreementmajorversion_detail.html" + tables = (tables.AgreementVersionTable, tables.SignedAgreementTable) + + def get_object(self, queryset=None): + queryset = self.model.objects.all() + try: + major_version = self.kwargs["major_version"] + obj = queryset.get(version=major_version) + except (KeyError, self.model.DoesNotExist): + raise Http404( + _("No %(verbose_name)s found matching the query") + % {"verbose_name": queryset.model._meta.verbose_name} + ) + return obj + + def get_tables_data(self): + agreement_version_qs = models.AgreementVersion.objects.filter( + major_version=self.object + ) + signed_agreement_qs = models.SignedAgreement.objects.filter( + version__major_version=self.object + ) + return [agreement_version_qs, signed_agreement_qs] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["show_deprecation_message"] = not self.object.is_valid + edit_permission_codename = "anvil_consortium_manager." + ( + AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + context[ + "show_invalidate_button" + ] = self.object.is_valid and self.request.user.has_perm( + edit_permission_codename + ) + return context + + +class AgreementMajorVersionInvalidate( + AnVILConsortiumManagerEditRequired, SuccessMessageMixin, UpdateView +): + """A view to invalidate an AgreementMajorVersion instance. + + This view sets the is_valid field to False. It also sets the status of all associated + CDSAs to LAPSED. + """ + + # Note that this view mimics the DeleteView. + model = models.AgreementMajorVersion + # form_class = Form + form_class = forms.AgreementMajorVersionIsValidForm + template_name = "cdsa/agreementmajorversion_confirm_invalidate.html" + success_message = "Successfully invalidated major agreement version." + ERROR_ALREADY_INVALID = "This version has already been invalidated." + + def get_object(self, queryset=None): + queryset = self.model.objects.all() + try: + major_version = self.kwargs["major_version"] + obj = queryset.get(version=major_version) + except (KeyError, self.model.DoesNotExist): + raise Http404( + _("No %(verbose_name)s found matching the query") + % {"verbose_name": queryset.model._meta.verbose_name} + ) + return obj + + def get_initial(self): + """Set is_valid to False, since this view is invalidating a specific AgreementMajorVersion.""" + initial = super().get_initial() + initial["is_valid"] = False + return initial + + def get(self, response, *args, **kwargs): + self.object = self.get_object() + if not self.object.is_valid: + messages.error(self.request, self.ERROR_ALREADY_INVALID) + return HttpResponseRedirect(self.object.get_absolute_url()) + return super().get(response, *args, **kwargs) + + def post(self, response, *args, **kwargs): + self.object = self.get_object() + if not self.object.is_valid: + messages.error(self.request, self.ERROR_ALREADY_INVALID) + return HttpResponseRedirect(self.object.get_absolute_url()) + return super().post(response, *args, **kwargs) + + def form_valid(self, form): + models.SignedAgreement.objects.filter( + status=models.SignedAgreement.StatusChoices.ACTIVE, + version__major_version=self.object, + ).update(status=models.SignedAgreement.StatusChoices.LAPSED) + return super().form_valid(form) + + def get_success_url(self): + return self.object.get_absolute_url() + + # Change status for CDSAs to lapsed when their major version is invalidated. + + +class AgreementVersionDetail( + AnVILConsortiumManagerViewRequired, SingleTableMixin, DetailView +): + """Display a "detail" page for an agreement major/minor version (e.g., 1.3).""" + + model = models.AgreementVersion + table_class = tables.SignedAgreementTable + context_table_name = "signed_agreement_table" + + def get_table_data(self): + qs = models.SignedAgreement.objects.filter(version=self.object) + # import ipdb; ipdb.set_trace() + print(qs) + return qs + + def get_object(self, queryset=None): + queryset = self.model.objects.all() + try: + major_version = self.kwargs["major_version"] + minor_version = self.kwargs["minor_version"] + obj = queryset.get( + major_version__version=major_version, minor_version=minor_version + ) + except (KeyError, self.model.DoesNotExist): + raise Http404( + _("No %(verbose_name)s found matching the query") + % {"verbose_name": queryset.model._meta.verbose_name} + ) + return obj + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["show_deprecation_message"] = not self.object.major_version.is_valid + return context + + +class AgreementVersionList(AnVILConsortiumManagerViewRequired, SingleTableView): + """Display a list of AgreementVersions.""" + + model = models.AgreementVersion + table_class = tables.AgreementVersionTable + + class SignedAgreementList(AnVILConsortiumManagerViewRequired, SingleTableView): """Display a list of SignedAgreement objects.""" @@ -214,6 +365,19 @@ def get_object(self, queryset=None): ) return obj + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[ + "show_deprecation_message" + ] = not self.object.signed_agreement.version.major_version.is_valid + edit_permission_codename = "anvil_consortium_manager." + ( + AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + context["show_update_button"] = self.request.user.has_perm( + edit_permission_codename + ) + return context + class MemberAgreementList(AnVILConsortiumManagerViewRequired, SingleTableView): """Display a list of MemberAgreement objects.""" @@ -222,6 +386,31 @@ class MemberAgreementList(AnVILConsortiumManagerViewRequired, SingleTableView): table_class = tables.MemberAgreementTable +class SignedAgreementStatusUpdate( + AnVILConsortiumManagerEditRequired, SuccessMessageMixin, UpdateView +): + + model = models.SignedAgreement + form_class = forms.SignedAgreementStatusForm + template_name = "cdsa/signedagreement_status_update.html" + agreement_type = None + success_message = "Successfully updated Signed Agreement status." + + def get_object(self, queryset=None): + """Look up the agreement by agreement_type_indicator and CDSA cc_id.""" + queryset = self.get_queryset() + try: + obj = queryset.get( + cc_id=self.kwargs.get("cc_id"), type=self.kwargs.get("agreement_type") + ) + except queryset.model.DoesNotExist: + raise Http404( + "No %(verbose_name)s found matching the query" + % {"verbose_name": queryset.model._meta.verbose_name} + ) + return obj + + class DataAffiliateAgreementDetail(AnVILConsortiumManagerViewRequired, DetailView): """View to show details about a `DataAffiliateAgreement`.""" @@ -239,6 +428,19 @@ def get_object(self, queryset=None): ) return obj + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[ + "show_deprecation_message" + ] = not self.object.signed_agreement.version.major_version.is_valid + edit_permission_codename = "anvil_consortium_manager." + ( + AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + context["show_update_button"] = self.request.user.has_perm( + edit_permission_codename + ) + return context + class DataAffiliateAgreementList(AnVILConsortiumManagerViewRequired, SingleTableView): """Display a list of DataAffiliateAgreement objects.""" @@ -281,6 +483,19 @@ def get_object(self, queryset=None): ) return obj + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context[ + "show_deprecation_message" + ] = not self.object.signed_agreement.version.major_version.is_valid + edit_permission_codename = "anvil_consortium_manager." + ( + AnVILProjectManagerAccess.EDIT_PERMISSION_CODENAME + ) + context["show_update_button"] = self.request.user.has_perm( + edit_permission_codename + ) + return context + class NonDataAffiliateAgreementList( AnVILConsortiumManagerViewRequired, SingleTableView @@ -300,9 +515,9 @@ class SignedAgreementAudit(AnVILConsortiumManagerViewRequired, TemplateView): ) def get(self, request, *args, **kwargs): - try: - self.audit = signed_agreement_audit.SignedAgreementAccessAudit() - except models.ManagedGroup.DoesNotExist: + if not models.ManagedGroup.objects.filter( + name=settings.ANVIL_CDSA_GROUP_NAME + ).exists(): messages.error( self.request, self.ERROR_CDSA_GROUP_DOES_NOT_EXIST.format( @@ -314,12 +529,12 @@ def get(self, request, *args, **kwargs): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - # Run the audit on all SignedAgreements. - self.audit.run_audit() - context["verified_table"] = self.audit.get_verified_table() - context["errors_table"] = self.audit.get_errors_table() - context["needs_action_table"] = self.audit.get_needs_action_table() - context["audit"] = self.audit + audit = signed_agreement_audit.SignedAgreementAccessAudit() + audit.run_audit() + context["verified_table"] = audit.get_verified_table() + context["errors_table"] = audit.get_errors_table() + context["needs_action_table"] = audit.get_needs_action_table() + context["audit"] = audit return context diff --git a/primed/primed_anvil/tables.py b/primed/primed_anvil/tables.py index e58d5a68..6b0f4065 100644 --- a/primed/primed_anvil/tables.py +++ b/primed/primed_anvil/tables.py @@ -5,24 +5,29 @@ from . import models -class BooleanCheckColumn(tables.BooleanColumn): +class BooleanIconColumn(tables.BooleanColumn): # attrs = {"td": {"align": "center"}} # attrs = {"th": {"class": "center"}} + def __init__(self, show_false_icon=False, **kwargs): + super().__init__(**kwargs) + self.show_false_icon = show_false_icon + def render(self, value, record, bound_column): value = self._get_bool_value(record, value, bound_column) if value: - icon = "check-circle-fill" - color = "green" - value = format_html( - """""".format( - icon, color - ) + rendered_value = format_html( + """""" ) else: - value = "" - return value + if self.show_false_icon: + rendered_value = format_html( + """""" + ) + else: + rendered_value = "" + return rendered_value class WorkspaceSharedWithConsortiumTable(tables.Table): @@ -134,6 +139,6 @@ def __init__(self, *args, **kwargs): "name", flat=True ) extra_columns = [ - (x, BooleanCheckColumn(default=False)) for x in available_data_types + (x, BooleanIconColumn(default=False)) for x in available_data_types ] super().__init__(*args, extra_columns=extra_columns, **kwargs) diff --git a/primed/primed_anvil/tests/test_tables.py b/primed/primed_anvil/tests/test_tables.py index 4d41ec6d..208c7d15 100644 --- a/primed/primed_anvil/tests/test_tables.py +++ b/primed/primed_anvil/tests/test_tables.py @@ -162,10 +162,24 @@ def test_adds_two_available_data_column(self): self.assertEqual(table.columns[4].name, "Foo") -class BooleanCheckColumnTest(TestCase): - def test_render_available_data(self): - factories.AvailableDataFactory.create(name="Foo") - self.assertIn( - "bi-check-circle-fill", tables.BooleanCheckColumn().render(True, None, None) - ) - self.assertEqual(tables.BooleanCheckColumn().render(False, None, None), "") +class BooleanIconColumnTest(TestCase): + """Tests for the BooleanIconColumn class.""" + + def test_render_default(self): + """render method with defaults.""" + column = tables.BooleanIconColumn() + value = column.render(True, None, None) + self.assertIn("bi-check-circle-fill", value) + self.assertIn("green", value) + value = column.render(False, None, None) + self.assertEqual(value, "") + + def test_render_show_false_icon(self): + """render method with defaults.""" + column = tables.BooleanIconColumn(show_false_icon=True) + value = column.render(True, None, None) + self.assertIn("bi-check-circle-fill", value) + self.assertIn("green", value) + value = column.render(False, None, None) + self.assertIn("bi-x-circle-fill", value) + self.assertIn("red", value) diff --git a/primed/templates/cdsa/agreementmajorversion_confirm_invalidate.html b/primed/templates/cdsa/agreementmajorversion_confirm_invalidate.html new file mode 100644 index 00000000..b3a3f7e7 --- /dev/null +++ b/primed/templates/cdsa/agreementmajorversion_confirm_invalidate.html @@ -0,0 +1,31 @@ +{% extends "anvil_consortium_manager/base.html" %} +{% load static %} + +{% block title %}Remove Group Account Membership{% endblock %} + +{% block content %} +
+ +
+
+ + +

Invalidate Agreement {{ object }}

+ +

Are you sure you want to invalidate Agreement {{ object }}?

+ +

This will change the status of all "Active" agreements associated with this version to "Lapsed".

+ +
{% csrf_token %} + {{ form }} + + + No, cancel +
+ +
+
+ + +
+{% endblock content %} diff --git a/primed/templates/cdsa/agreementmajorversion_detail.html b/primed/templates/cdsa/agreementmajorversion_detail.html new file mode 100644 index 00000000..9cf7e138 --- /dev/null +++ b/primed/templates/cdsa/agreementmajorversion_detail.html @@ -0,0 +1,72 @@ +{% extends "anvil_consortium_manager/__object_detail.html" %} +{% load django_tables2 %} + +{% block title %}Agreement version v{{ object.version }}{% endblock %} + +{% block pills %} + {% if show_deprecation_message %} + + + Deprecated + + {% endif %} +{% endblock pills %} + +{% block panel %} +
+
Version
v{{ object.version }}
+
Status
{{ object.get_status_display }}
+
Date modified
{{ object.modified }}
+
+{% endblock panel %} + +{% block after_panel %} + +

Agreement versions

+ + +
+
+

+ +

+
+
+ {% render_table tables.0 %} +
+
+
+
+ +

Signed agreements

+ +
+
+

+ +

+
+
+ {% render_table tables.1 %} +
+
+
+
+ +{% endblock after_panel %} + + +{% block action_buttons %} + {% if show_invalidate_button %} + Invalidate this version +

+ {% endif %} +{% endblock action_buttons %} diff --git a/primed/templates/cdsa/agreementversion_detail.html b/primed/templates/cdsa/agreementversion_detail.html new file mode 100644 index 00000000..5db313cb --- /dev/null +++ b/primed/templates/cdsa/agreementversion_detail.html @@ -0,0 +1,47 @@ +{% extends "anvil_consortium_manager/__object_detail.html" %} +{% load django_tables2 %} + +{% block title %}Agreement version {{ object }}{% endblock %} + +{% block pills %} + {% if show_deprecation_message %} + + + Deprecated + + {% endif %} +{% endblock pills %} + +{% block panel %} +
+
Full version
{{ object.full_version }}
+
Major version
+ {{ object.major_version }} +
+ +
Date approved
{{ object.date_approved }}
+
+{% endblock panel %} + +{% block after_panel %} + +

Signed agreements

+ +
+
+

+ +

+
+
+ {% render_table signed_agreement_table %} +
+
+
+
+ +{% endblock after_panel %} diff --git a/primed/templates/cdsa/agreementversion_list.html b/primed/templates/cdsa/agreementversion_list.html new file mode 100644 index 00000000..f3e5d85f --- /dev/null +++ b/primed/templates/cdsa/agreementversion_list.html @@ -0,0 +1,11 @@ +{% extends "anvil_consortium_manager/base.html" %} +{% load render_table from django_tables2 %} + +{% block title %}Agreement Versions{% endblock %} + +{% block content %} +

Agreement Versions

+ +{% render_table table %} + +{% endblock content %} diff --git a/primed/templates/cdsa/cdsaworkspace_audit.html b/primed/templates/cdsa/cdsaworkspace_audit.html index 9f0b1fe7..c4a92fd7 100644 --- a/primed/templates/cdsa/cdsaworkspace_audit.html +++ b/primed/templates/cdsa/cdsaworkspace_audit.html @@ -22,17 +22,18 @@

Audit results

  • Verified includes the following:
    • -
    • CDSA workspaces that have a valid, primary Signed Agreement from the same study, and the PRIMED CDSA group is in the auth domain of the workspace.
    • -
    • CDSA workspaces that do not have a valid, primary Signed Agreement from the same study, and the PRIMED CDSA group is not in the auth domain of the workspace.
    • +
    • CDSA workspaces with an active primary Data Affiliate Agreement, and the PRIMED CDSA group is in the auth domain of the workspace.
    • +
    • CDSA workspaces without an active primary Data Affiliate Agreement, and the PRIMED CDSA group is not in the auth domain of the workspace.
  • Needs action includes the following:
    • -
    • CDSA workspaces that have an associated valid, primary Signed Agreement for the same study, but the PRIMED CDSA group is not in the auth domain of the workspace.
    • +
    • CDSA workspaces with an active primary Data Affiliate Agreement, but the PRIMED CDSA group is not in the auth domain of the workspace.
    • +
    • CDSA workspaces with an inactive primary Data Affiliate Agreement, and the PRIMED CDSA group is in the auth domain of the workspace.
  • Errors
    • -
    • The PRIMED CDSA group is in the auth domain of the workspace, but there is no associated valid, primary Signed Agreement for the study.
    • +
    • The PRIMED CDSA group is in the auth domain of the workspace, but there is no primary Signed Agreement from the study.

    diff --git a/primed/templates/cdsa/dataaffiliateagreement_detail.html b/primed/templates/cdsa/dataaffiliateagreement_detail.html index 28d6f3f4..211b3e92 100644 --- a/primed/templates/cdsa/dataaffiliateagreement_detail.html +++ b/primed/templates/cdsa/dataaffiliateagreement_detail.html @@ -1,6 +1,15 @@ {% extends "anvil_consortium_manager/__object_detail.html" %} {% load django_tables2 %} +{% block pills %} + {% if show_deprecation_message %} + + + Deprecated CDSA version + + {% endif %} +{% endblock pills %} + {% block title %}Data affiliate agreement {{ object }}{% endblock %} {% block panel %} @@ -12,7 +21,10 @@
    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 }}
    +
    Agreement version
    + {{ object.signed_agreement.version }} +
    +
    Status
    {{ object.signed_agreement.get_status_display }}
    Date signed
    {{ object.signed_agreement.date_signed }}
    Study
    @@ -32,3 +44,11 @@ {% endblock panel %} + + +{% block action_buttons %} + {% if show_update_button %} + Update status +

    + {% endif %} +{% endblock action_buttons %} diff --git a/primed/templates/cdsa/memberagreement_detail.html b/primed/templates/cdsa/memberagreement_detail.html index b2078efc..381b1f23 100644 --- a/primed/templates/cdsa/memberagreement_detail.html +++ b/primed/templates/cdsa/memberagreement_detail.html @@ -1,6 +1,15 @@ {% extends "anvil_consortium_manager/__object_detail.html" %} {% load django_tables2 %} +{% block pills %} + {% if show_deprecation_message %} + + + Deprecated CDSA version + + {% endif %} +{% endblock pills %} + {% block title %}Member agreement {{ object }}{% endblock %} {% block panel %} @@ -12,7 +21,10 @@
    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 }}
    +
    Agreement version
    + {{ object.signed_agreement.version }} +
    +
    Status
    {{ object.signed_agreement.get_status_display }}
    Date signed
    {{ object.signed_agreement.date_signed }}
    Study site
    @@ -28,3 +40,11 @@ {% endblock panel %} + + +{% block action_buttons %} + {% if show_update_button %} + Update status +

    + {% endif %} +{% endblock action_buttons %} diff --git a/primed/templates/cdsa/nav_items.html b/primed/templates/cdsa/nav_items.html index 9587b192..13e26408 100644 --- a/primed/templates/cdsa/nav_items.html +++ b/primed/templates/cdsa/nav_items.html @@ -3,39 +3,45 @@ CDSA