diff --git a/app/manage/api.py b/app/manage/api.py index bdbd7ef..28d03c9 100644 --- a/app/manage/api.py +++ b/app/manage/api.py @@ -41,6 +41,7 @@ from projects.models import TeamComment from projects.serializers import HostedFileSerializer, HostedFileDownloadSerializer from projects.models import AGREEMENT_FORM_TYPE_MODEL, AGREEMENT_FORM_TYPE_FILE +from projects.models import InstitutionalOfficial # Get an instance of a logger logger = logging.getLogger(__name__) @@ -1157,6 +1158,54 @@ def grant_view_permission(request, project_key, user_email): participant = project.participant_set.get(user__email=user_email) participant.permission = 'VIEW' participant.save() + + # Check if this project allows institutional signers + if project.institutional_signers: + + # Check if this is a signing official + try: + official = InstitutionalOfficial.objects.get( + project=project, + user=participant.user, + ) + + # Iterate linked members + for member_email in official.member_emails: + + logger.debug(f"Institutional signer/{participant.user.email}: Checking for existing linked member '{member_email}'") + + # Check if a participant exists for this email with no VIEW permission + if Participant.objects.filter(project=project, user__email=member_email).exclude(permission="VIEW").exists(): + + # Fetch them + member_participant = Participant.objects.get(project=project, user__email=member_email) + + # Approve signed agreement forms + for signed_agreement_form in SignedAgreementForm.objects.filter(project=project, user=member_participant.user): + + # If allows institutional signers, auto-approve + if signed_agreement_form.agreement_form.institutional_signers: + + signed_agreement_form.status = "A" + signed_agreement_form.save() + + # Grant this user access immediately if all agreement forms are accepted + for agreement_form in project.agreement_forms.all(): + if not SignedAgreementForm.objects.filter( + agreement_form=agreement_form, + project=project, + user=member_participant.user, + status="A" + ): + break + else: + + # Call this method to process the access + grant_view_permission(request, project_key, member_email) + + except ObjectDoesNotExist: + pass + except Exception as e: logger.exception( '[HYPATIO][DEBUG][grant_view_permission] User {user} could not have permission added to project {project_key}: {e}'.format( @@ -1226,6 +1275,26 @@ def remove_view_permission(request, project_key, user_email): participant = project.participant_set.get(user__email=user_email) participant.permission = None participant.save() + + # Check if this project allows institutional signers + if project.institutional_signers: + + # Check if this is a signing official + try: + official = InstitutionalOfficial.objects.get( + project=project, + user=participant.user, + ) + + # Iterate linked members + for member_email in official.member_emails: + + # Remove their access + remove_view_permission(request, project_key, member_email) + + except ObjectDoesNotExist: + pass + except Exception as e: logger.exception( '[HYPATIO][ERROR][grant_view_permission] User {user} could not have permission remove from project {project_key}: {e}'.format( diff --git a/app/projects/models.py b/app/projects/models.py index 00fd7a7..69c6580 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -2,6 +2,7 @@ import re import importlib from datetime import datetime +from typing import Optional, Tuple import boto3 from botocore.exceptions import ClientError @@ -489,6 +490,37 @@ class Meta: verbose_name_plural = 'Signed Agreement Forms' + def get_institutional_signer_details(self) -> Optional[Tuple[str, str, list[str]]]: + """ + Checks the SignedAgreementForm to see if it has been signed by an institutional official. + If so, it returns the name of the institution and email of the institutional official along with a list of + emails for the members they are representing. + + :returns: A tuple containing the institution name, official email, and a list of member emails if this is an + institutional signer; otherwise tuple of None, None, None + :rtype: Optional[Tuple[str, str, list[str]]], defaults to Tuple[None, None, None] + """ + if self.agreement_form.institutional_signers: + + # Fields should contain whether this is an institutional official or not + if self.fields and self.fields.get("registrant_is", "").lower() == "official": + + # Ensure member emails is a list + member_emails = self.fields.get("member_emails", []) + if isinstance(member_emails, str): + member_emails = [member_emails] + elif not isinstance(member_emails, list): + raise ValidationError(f"Unhandled state of 'member_emails' field: {type(member_emails)}/{member_emails}") + + # Cleanup emails to remove whitespace, if any + member_emails = [email.strip() for email in member_emails] + + # Return values + return self.fields["institute_name"], self.user.email, member_emails + + return None, None, None + + class DataUseReportRequest(models.Model): """ This model describes a request for a participant to report on data use. diff --git a/app/projects/signals.py b/app/projects/signals.py index b37dc9e..c4a290c 100644 --- a/app/projects/signals.py +++ b/app/projects/signals.py @@ -133,16 +133,22 @@ def signed_agreement_form_pre_save_handler(sender, **kwargs): logger.debug(f"Pre-save: {instance}") # Check for specific types of forms that require additional handling - if instance.status == "A" and instance.agreement_form.short_name == "4ce-dua" \ - and instance.fields.get("registrant-is") == "official": - logger.debug(f"Pre-save institutional official: {instance}") - - # Create official and member objects - official = InstitutionalOfficial.objects.create( - user=instance.user, - institution=instance.fields["institute-name"], - project=instance.project, - signed_agreement_form=instance, - member_emails=instance.fields["member-emails"], - ) - official.save() + institute_name, official_email, member_emails = instance.get_institutional_signer_details() + if instance.status == "A" and institute_name and official_email and member_emails: + logger.debug(f"Pre-save institutional official AgreementForm: {instance}") + + # Ensure they don't already exist + if InstitutionalOfficial.objects.filter(user=instance.user, project=instance.project).exists(): + logger.debug(f"InstitutionalOfficial already exists for {instance.user}/{instance.project}") + + else: + + # Create official and member objects + official = InstitutionalOfficial.objects.create( + user=instance.user, + institution=institute_name, + project=instance.project, + signed_agreement_form=instance, + member_emails=member_emails, + ) + official.save()