Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(projects): Refactored how institutional officials/members are sto… #699

Merged
merged 1 commit into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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