Skip to content

Commit

Permalink
Merge pull request #699 from hms-dbmi/feature-4ce-dua
Browse files Browse the repository at this point in the history
fix(projects): Refactored how institutional officials/members are sto…
  • Loading branch information
b32147 authored Aug 27, 2024
2 parents 6bd5c7d + 88fa76e commit cbaa48d
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 169 deletions.
6 changes: 0 additions & 6 deletions app/projects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from projects.models import ChallengeTaskSubmissionDownload
from projects.models import Bucket
from projects.models import InstitutionalOfficial
from projects.models import InstitutionalMember


class GroupAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -64,10 +63,6 @@ class InstitutionalOfficialAdmin(admin.ModelAdmin):
list_display = ('user', 'institution', 'project', 'created', 'modified', )
readonly_fields = ('created', 'modified', )

class InstitutionalMemberAdmin(admin.ModelAdmin):
list_display = ('email', 'official', 'user', 'created', 'modified', )
readonly_fields = ('created', 'modified', )

class HostedFileAdmin(admin.ModelAdmin):
list_display = ('long_name', 'project', 'hostedfileset', 'file_name', 'file_location', 'order', 'created', 'modified',)
list_filter = ('project', )
Expand Down Expand Up @@ -106,7 +101,6 @@ class ChallengeTaskSubmissionDownloadAdmin(admin.ModelAdmin):
admin.site.register(Participant, ParticipantAdmin)
admin.site.register(Institution, InstitutionAdmin)
admin.site.register(InstitutionalOfficial, InstitutionalOfficialAdmin)
admin.site.register(InstitutionalMember, InstitutionalMemberAdmin)
admin.site.register(HostedFile, HostedFileAdmin)
admin.site.register(HostedFileSet, HostedFileSetAdmin)
admin.site.register(HostedFileDownload, HostedFileDownloadAdmin)
Expand Down
102 changes: 78 additions & 24 deletions app/projects/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
from projects.models import SIGNED_FORM_REJECTED
from projects.models import HostedFileSet
from projects.models import InstitutionalOfficial
from projects.models import InstitutionalMember

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -672,9 +671,12 @@ def save_signed_agreement_form(request):

# Retain lists
if len(value) > 1:
fields[key] = value

# Only retain valid values
valid_values = [v for v in value if v]
fields[key] = valid_values if valid_values else ""
else:
fields[key] = next(iter(value), None)
fields[key] = next(iter(value), "")

# Save fields
signed_agreement_form.fields = fields
Expand Down Expand Up @@ -802,13 +804,52 @@ def submit_user_permission_request(request):
return response

# Create a new participant record if one does not exist already.
Participant.objects.get_or_create(
participant, created = Participant.objects.get_or_create(
user=request.user,
project=project
)

# Check if this project allows institutional signers
if project.institutional_signers:

# Check if this is a member
try:
official = InstitutionalOfficial.objects.get(
project=project,
member_emails__contains=request.user.email,
)

# Check if they have access
official_participant = Participant.objects.get(user=official.user)
if official_participant.permission == "VIEW":

# Approve signed agreement forms
for signed_agreement_form in SignedAgreementForm.objects.filter(project=project, user=request.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=request.user,
status="A"
):
break
else:
participant.permission = "VIEW"
participant.save()

except ObjectDoesNotExist:
pass

# Check if there are administrators to notify.
if project.project_supervisors:
if project.project_supervisors and not participant.permission:

# Convert the comma separated string of emails into a list.
supervisor_emails = project.project_supervisors.split(",")
Expand All @@ -832,6 +873,8 @@ def submit_user_permission_request(request):
except Exception as e:
logger.exception(e)

elif participant.permission:
logger.debug(f"Request has been auto-approved due to an institutional signer")
else:
logger.warning(f"Project '{project}' has not supervisors to alert on access requests")

Expand All @@ -855,6 +898,10 @@ def submit_user_permission_request(request):
"success", "Your request for access has been submitted", "thumbs-up"
)

# Reload page if approved
if participant.permission == "VIEW":
response['X-IC-Script'] += "setTimeout(function() { location.reload(); }, 2000);"

return response


Expand Down Expand Up @@ -927,6 +974,10 @@ def update_institutional_members(request):
# Get the list
member_emails = [m.lower() for m in request.POST.getlist("member-emails", [])]

# Get deletions and additions
deleted_member_emails = list(set(official.member_emails) - set(member_emails))
added_member_emails = list(set(member_emails) - set(official.member_emails))

# Check for duplicates
if len(set(member_emails)) < len(member_emails):

Expand All @@ -940,31 +991,34 @@ def update_institutional_members(request):

return response

# Iterate existing members
for member in InstitutionalMember.objects.filter(official=official):
# Save the official with updated emails
official.member_emails = member_emails
official.save()

# Check if in list
if member.email.lower() in member_emails:
logger.debug(f"Update institutional members: Member '{member.email}' already exists")
# Iterate removed emails and remove access
for email in deleted_member_emails:
try:
participant = Participant.objects.get(project=official.project, user__email=email, permission="VIEW")

# Remove email from list
member_emails.remove(member.email.lower())
# Revoke it if found
participant.permission = None
participant.save()

elif member.email.lower() not in member_emails:
logger.debug(f"Update institutional members: Member '{member.email}' will be deleted")
except ObjectDoesNotExist:
pass

# Delete them
member.delete()
# Iterate added emails and add access if waiting
for email in added_member_emails:
try:
official_participant = Participant.objects.get(project=official.project, user=official.user)
participant = Participant.objects.get(project=official.project, user__email=email)

# Create members from remaining email addresses
for member_email in member_emails:
logger.debug(f"Update institutional members: Member '{member_email}' will be created")
# Add access if found
participant.permission = official_participant.permission
participant.save()

# Create them
InstitutionalMember.objects.create(
official=official,
email=member_email,
)
except ObjectDoesNotExist:
pass

# Create the response.
response = HttpResponse(status=201)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.14 on 2024-08-22 18:11

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('projects', '0105_agreementform_template'),
]

operations = [
migrations.AddField(
model_name='agreementform',
name='institutional_signers',
field=models.BooleanField(default=False, help_text='Allows institutional signers to sign for their members. This will auto-approve this agreement form for members whose institutional official has had their agreement form approved.'),
),
migrations.AddField(
model_name='dataproject',
name='institutional_signers',
field=models.BooleanField(default=False, help_text='Allows institutional signers to sign for their members. This will auto-approve agreement forms for members whose institutional official has had their agreement forms approved.'),
),
migrations.AddField(
model_name='institutionalofficial',
name='member_emails',
field=models.JSONField(default=[]),
preserve_default=False,
),
migrations.DeleteModel(
name='InstitutionalMember',
),
]
19 changes: 6 additions & 13 deletions app/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ class AgreementForm(models.Model):
" the person who they submitted their signed agreement form to."
)
template = models.CharField(max_length=300, blank=True, null=True)
institutional_signers = models.BooleanField(default=False, help_text="Allows institutional signers to sign for their members. This will auto-approve this agreement form for members whose institutional official has had their agreement form approved.")

# Meta
created = models.DateTimeField(auto_now_add=True)
Expand Down Expand Up @@ -363,6 +364,10 @@ class DataProject(models.Model):
help_text="Set this to a specific bucket where this project's files should be stored."
)

# Automate approval of members covered by an already-approved institutional signer
institutional_signers = models.BooleanField(default=False, help_text="Allows institutional signers to sign for their members. This will auto-approve agreement forms for members whose institutional official has had their agreement forms approved.")


# Meta
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
Expand Down Expand Up @@ -399,19 +404,7 @@ class InstitutionalOfficial(models.Model):
project = models.ForeignKey(DataProject, on_delete=models.PROTECT)
institution = models.TextField(null=False, blank=False)
signed_agreement_form = models.ForeignKey("SignedAgreementForm", on_delete=models.PROTECT)

# Meta
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)


class InstitutionalMember(models.Model):
"""
This represents a member of an institution.
"""
official = models.ForeignKey(InstitutionalOfficial, on_delete=models.PROTECT)
email = models.TextField(null=False, blank=False)
user = models.ForeignKey(User, on_delete=models.PROTECT, null=True, blank=True)
member_emails = models.JSONField(null=False, blank=False, editable=True)

# Meta
created = models.DateTimeField(auto_now_add=True)
Expand Down
11 changes: 0 additions & 11 deletions app/projects/panels.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,3 @@ class DataProjectInstitutionalOfficialPanel(DataProjectPanel):

def __init__(self, title, bootstrap_color, template, additional_context=None):
super().__init__(title, bootstrap_color, template, additional_context)


class DataProjectInstitutionalMemberPanel(DataProjectPanel):
"""
This class holds information needed to display panels on the DataProject
page that outline institutional officials and the members they are representing.
"""

def __init__(self, title, bootstrap_color, template, status, additional_context=None):
super().__init__(title, bootstrap_color, template, additional_context)
self.status = status
12 changes: 1 addition & 11 deletions app/projects/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from projects.models import SignedAgreementForm
from projects.models import TEAM_ACTIVE, TEAM_DEACTIVATED, TEAM_READY
from projects.models import InstitutionalOfficial
from projects.models import InstitutionalMember

import logging
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -144,15 +143,6 @@ def signed_agreement_form_pre_save_handler(sender, **kwargs):
institution=instance.fields["institute-name"],
project=instance.project,
signed_agreement_form=instance,
member_emails=instance.fields["member-emails"],
)
official.save()

# Iterate members
for email in instance.fields["member-emails"]:

# Create member
member = InstitutionalMember.objects.create(
official=official,
email=email,
)
member.save()
4 changes: 4 additions & 0 deletions app/projects/templatetags/projects_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
def get_html_form_file_contents(form_file_path):
return render_to_string(form_file_path)

@register.simple_tag
def get_agreement_form_template(form_file_path, context={}):
return render_to_string(form_file_path, context=context)

@register.filter
def get_login_url(current_uri):

Expand Down
Loading

0 comments on commit cbaa48d

Please sign in to comment.