Skip to content

Commit cbaa48d

Browse files
authored
Merge pull request #699 from hms-dbmi/feature-4ce-dua
fix(projects): Refactored how institutional officials/members are sto…
2 parents 6bd5c7d + 88fa76e commit cbaa48d

File tree

12 files changed

+202
-169
lines changed

12 files changed

+202
-169
lines changed

app/projects/admin.py

-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from projects.models import ChallengeTaskSubmissionDownload
1818
from projects.models import Bucket
1919
from projects.models import InstitutionalOfficial
20-
from projects.models import InstitutionalMember
2120

2221

2322
class GroupAdmin(admin.ModelAdmin):
@@ -64,10 +63,6 @@ class InstitutionalOfficialAdmin(admin.ModelAdmin):
6463
list_display = ('user', 'institution', 'project', 'created', 'modified', )
6564
readonly_fields = ('created', 'modified', )
6665

67-
class InstitutionalMemberAdmin(admin.ModelAdmin):
68-
list_display = ('email', 'official', 'user', 'created', 'modified', )
69-
readonly_fields = ('created', 'modified', )
70-
7166
class HostedFileAdmin(admin.ModelAdmin):
7267
list_display = ('long_name', 'project', 'hostedfileset', 'file_name', 'file_location', 'order', 'created', 'modified',)
7368
list_filter = ('project', )
@@ -106,7 +101,6 @@ class ChallengeTaskSubmissionDownloadAdmin(admin.ModelAdmin):
106101
admin.site.register(Participant, ParticipantAdmin)
107102
admin.site.register(Institution, InstitutionAdmin)
108103
admin.site.register(InstitutionalOfficial, InstitutionalOfficialAdmin)
109-
admin.site.register(InstitutionalMember, InstitutionalMemberAdmin)
110104
admin.site.register(HostedFile, HostedFileAdmin)
111105
admin.site.register(HostedFileSet, HostedFileSetAdmin)
112106
admin.site.register(HostedFileDownload, HostedFileDownloadAdmin)

app/projects/api.py

+78-24
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
from projects.models import SIGNED_FORM_REJECTED
4545
from projects.models import HostedFileSet
4646
from projects.models import InstitutionalOfficial
47-
from projects.models import InstitutionalMember
4847

4948
logger = logging.getLogger(__name__)
5049

@@ -672,9 +671,12 @@ def save_signed_agreement_form(request):
672671

673672
# Retain lists
674673
if len(value) > 1:
675-
fields[key] = value
674+
675+
# Only retain valid values
676+
valid_values = [v for v in value if v]
677+
fields[key] = valid_values if valid_values else ""
676678
else:
677-
fields[key] = next(iter(value), None)
679+
fields[key] = next(iter(value), "")
678680

679681
# Save fields
680682
signed_agreement_form.fields = fields
@@ -802,13 +804,52 @@ def submit_user_permission_request(request):
802804
return response
803805

804806
# Create a new participant record if one does not exist already.
805-
Participant.objects.get_or_create(
807+
participant, created = Participant.objects.get_or_create(
806808
user=request.user,
807809
project=project
808810
)
809811

812+
# Check if this project allows institutional signers
813+
if project.institutional_signers:
814+
815+
# Check if this is a member
816+
try:
817+
official = InstitutionalOfficial.objects.get(
818+
project=project,
819+
member_emails__contains=request.user.email,
820+
)
821+
822+
# Check if they have access
823+
official_participant = Participant.objects.get(user=official.user)
824+
if official_participant.permission == "VIEW":
825+
826+
# Approve signed agreement forms
827+
for signed_agreement_form in SignedAgreementForm.objects.filter(project=project, user=request.user):
828+
829+
# If allows institutional signers, auto-approve
830+
if signed_agreement_form.agreement_form.institutional_signers:
831+
832+
signed_agreement_form.status = "A"
833+
signed_agreement_form.save()
834+
835+
# Grant this user access immediately if all agreement forms are accepted
836+
for agreement_form in project.agreement_forms.all():
837+
if not SignedAgreementForm.objects.filter(
838+
agreement_form=agreement_form,
839+
project=project,
840+
user=request.user,
841+
status="A"
842+
):
843+
break
844+
else:
845+
participant.permission = "VIEW"
846+
participant.save()
847+
848+
except ObjectDoesNotExist:
849+
pass
850+
810851
# Check if there are administrators to notify.
811-
if project.project_supervisors:
852+
if project.project_supervisors and not participant.permission:
812853

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

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

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

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

860907

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

977+
# Get deletions and additions
978+
deleted_member_emails = list(set(official.member_emails) - set(member_emails))
979+
added_member_emails = list(set(member_emails) - set(official.member_emails))
980+
930981
# Check for duplicates
931982
if len(set(member_emails)) < len(member_emails):
932983

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

941992
return response
942993

943-
# Iterate existing members
944-
for member in InstitutionalMember.objects.filter(official=official):
994+
# Save the official with updated emails
995+
official.member_emails = member_emails
996+
official.save()
945997

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

950-
# Remove email from list
951-
member_emails.remove(member.email.lower())
1003+
# Revoke it if found
1004+
participant.permission = None
1005+
participant.save()
9521006

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

956-
# Delete them
957-
member.delete()
1010+
# Iterate added emails and add access if waiting
1011+
for email in added_member_emails:
1012+
try:
1013+
official_participant = Participant.objects.get(project=official.project, user=official.user)
1014+
participant = Participant.objects.get(project=official.project, user__email=email)
9581015

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

963-
# Create them
964-
InstitutionalMember.objects.create(
965-
official=official,
966-
email=member_email,
967-
)
1020+
except ObjectDoesNotExist:
1021+
pass
9681022

9691023
# Create the response.
9701024
response = HttpResponse(status=201)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Generated by Django 4.2.14 on 2024-08-22 18:11
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('projects', '0105_agreementform_template'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='agreementform',
15+
name='institutional_signers',
16+
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.'),
17+
),
18+
migrations.AddField(
19+
model_name='dataproject',
20+
name='institutional_signers',
21+
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.'),
22+
),
23+
migrations.AddField(
24+
model_name='institutionalofficial',
25+
name='member_emails',
26+
field=models.JSONField(default=[]),
27+
preserve_default=False,
28+
),
29+
migrations.DeleteModel(
30+
name='InstitutionalMember',
31+
),
32+
]

app/projects/models.py

+6-13
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ class AgreementForm(models.Model):
260260
" the person who they submitted their signed agreement form to."
261261
)
262262
template = models.CharField(max_length=300, blank=True, null=True)
263+
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.")
263264

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

367+
# Automate approval of members covered by an already-approved institutional signer
368+
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.")
369+
370+
366371
# Meta
367372
created = models.DateTimeField(auto_now_add=True)
368373
modified = models.DateTimeField(auto_now=True)
@@ -399,19 +404,7 @@ class InstitutionalOfficial(models.Model):
399404
project = models.ForeignKey(DataProject, on_delete=models.PROTECT)
400405
institution = models.TextField(null=False, blank=False)
401406
signed_agreement_form = models.ForeignKey("SignedAgreementForm", on_delete=models.PROTECT)
402-
403-
# Meta
404-
created = models.DateTimeField(auto_now_add=True)
405-
modified = models.DateTimeField(auto_now=True)
406-
407-
408-
class InstitutionalMember(models.Model):
409-
"""
410-
This represents a member of an institution.
411-
"""
412-
official = models.ForeignKey(InstitutionalOfficial, on_delete=models.PROTECT)
413-
email = models.TextField(null=False, blank=False)
414-
user = models.ForeignKey(User, on_delete=models.PROTECT, null=True, blank=True)
407+
member_emails = models.JSONField(null=False, blank=False, editable=True)
415408

416409
# Meta
417410
created = models.DateTimeField(auto_now_add=True)

app/projects/panels.py

-11
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,3 @@ class DataProjectInstitutionalOfficialPanel(DataProjectPanel):
7070

7171
def __init__(self, title, bootstrap_color, template, additional_context=None):
7272
super().__init__(title, bootstrap_color, template, additional_context)
73-
74-
75-
class DataProjectInstitutionalMemberPanel(DataProjectPanel):
76-
"""
77-
This class holds information needed to display panels on the DataProject
78-
page that outline institutional officials and the members they are representing.
79-
"""
80-
81-
def __init__(self, title, bootstrap_color, template, status, additional_context=None):
82-
super().__init__(title, bootstrap_color, template, additional_context)
83-
self.status = status

app/projects/signals.py

+1-11
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from projects.models import SignedAgreementForm
1010
from projects.models import TEAM_ACTIVE, TEAM_DEACTIVATED, TEAM_READY
1111
from projects.models import InstitutionalOfficial
12-
from projects.models import InstitutionalMember
1312

1413
import logging
1514
logger = logging.getLogger(__name__)
@@ -144,15 +143,6 @@ def signed_agreement_form_pre_save_handler(sender, **kwargs):
144143
institution=instance.fields["institute-name"],
145144
project=instance.project,
146145
signed_agreement_form=instance,
146+
member_emails=instance.fields["member-emails"],
147147
)
148148
official.save()
149-
150-
# Iterate members
151-
for email in instance.fields["member-emails"]:
152-
153-
# Create member
154-
member = InstitutionalMember.objects.create(
155-
official=official,
156-
email=email,
157-
)
158-
member.save()

app/projects/templatetags/projects_extras.py

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
def get_html_form_file_contents(form_file_path):
2020
return render_to_string(form_file_path)
2121

22+
@register.simple_tag
23+
def get_agreement_form_template(form_file_path, context={}):
24+
return render_to_string(form_file_path, context=context)
25+
2226
@register.filter
2327
def get_login_url(current_uri):
2428

0 commit comments

Comments
 (0)