Skip to content

Commit

Permalink
Merge pull request #700 from hms-dbmi/feature-4ce-dua
Browse files Browse the repository at this point in the history
feat(projects): Setup ability to use a Form class to manage rendering…
  • Loading branch information
b32147 authored Aug 29, 2024
2 parents 2e35e76 + c60b9e0 commit 88f30c9
Show file tree
Hide file tree
Showing 6 changed files with 373 additions and 85 deletions.
99 changes: 65 additions & 34 deletions app/projects/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import json
import logging

from hypatio.auth0authenticate import user_auth_and_jwt

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import User
Expand All @@ -20,6 +18,7 @@
from django.core.files.base import ContentFile
from dal import autocomplete

from hypatio.auth0authenticate import user_auth_and_jwt
from contact.views import email_send
from hypatio import file_services as fileservice
from hypatio.file_services import get_download_url
Expand Down Expand Up @@ -644,43 +643,75 @@ def save_signed_agreement_form(request):
logger.debug('%s already has signed the agreement form "%s" for project "%s".', request.user.email, agreement_form.name, project.project_key)
return HttpResponse(status=400)

# Check if this agreement form has a specified form class
fields = {}
if agreement_form.form_class:
try:
form = agreement_form.form(
request=request,
project=project,
data=request.POST,
)

# Check validity
if not form.is_valid():
logger.debug(form.errors.as_json())

# Setup the script run.
response = HttpResponse(content=form.errors.as_json(), status=400)
response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format(
"warning", f"The agreement form contained errors, please review", "warning-sign"
)
return response

# Use the data from the form
fields = form.cleaned_data

except Exception as e:
logger.exception(f"Agreement form error: {e}", exc_info=True)
return HttpResponse(status=500)

else:
try:
# Set fields that we do not need to persist here
exclusions = [
"csrfmiddlewaretoken", "project_key", "agreement_form_id",
"agreement_text"
]

# Save form fields
for key, value in dict(request.POST.lists()).items():

# Check exclusions
if key.lower() in exclusions:
continue

# Retain lists
if len(value) > 1:

# 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), "")

except Exception as e:
logger.exception(
f"HYP/Projects/API: Fields error: {e}",
exc_info=True,
extra={"form": agreement_form.short_name, "fields": request.POST,}
)

signed_agreement_form = SignedAgreementForm(
user=request.user,
agreement_form=agreement_form,
project=project,
date_signed=datetime.now(),
agreement_text=agreement_text
agreement_text=agreement_text,
fields=fields,
)
signed_agreement_form.save()

# Persist fields to JSON field on object
try:
# Set fields that we do not need to persist here
exclusions = [
"csrfmiddlewaretoken", "project_key", "agreement_form_id",
"agreement_text"
]

# Save form fields
fields = {}
for key, value in dict(request.POST.lists()).items():

# Check exclusions
if key.lower() in exclusions:
continue

# Retain lists
if len(value) > 1:

# 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), "")

# Save fields
signed_agreement_form.fields = fields

# Check for a template
if agreement_form.template:

Expand Down Expand Up @@ -721,16 +752,16 @@ def save_signed_agreement_form(request):
"signed_agreement_form": signed_agreement_form,
})

# Save
signed_agreement_form.save()

except Exception as e:
logger.exception(
f"HYP/Projects/API: Fields error: {e}",
exc_info=True,
extra={"form": agreement_form.short_name, "fields": request.POST,}
)

# Save the agreement form
signed_agreement_form.save()

return HttpResponse(status=200)

@user_auth_and_jwt
Expand Down
202 changes: 202 additions & 0 deletions app/projects/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
from django import forms
from django.http import HttpRequest
import django.forms.fields as fields
import django.forms.widgets as widgets
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

from hypatio.scireg_services import get_current_user_profile
from projects.models import DataProject, AgreementForm


class MultiValueValidationError(ValidationError):
def __init__(self, errors):
clean_errors = [
f"{message} (item {key})" for message, key, value in errors
]
super().__init__(clean_errors)
self.error_detail = errors


class MultiValueFieldWidget(widgets.Input):

def __init__(self, param_name: str) -> None:
super().__init__()
self.param_name: str = param_name

def value_from_datadict(self, data, *args):
return data.getlist(self.param_name)


class MultiValueField(fields.Field):

def __init__(self,
subfield: fields.Field,
param_name: str,
*args, **kwargs) -> None:
print(kwargs)
super().__init__(
widget=MultiValueFieldWidget(param_name),
*args, **kwargs,
)
self.error_messages["required"] = _(
"Please specify one or more '{}' arguments."
).format(param_name)
self.subfield = subfield

def clean(self, values):
if len(values) == 0 and self.required:
raise ValidationError(self.error_messages["required"])
result = []
errors = []
for i, value in enumerate(values):
try:
result.append(self.subfield.clean(value))
except ValidationError as e:
if self.required:
errors.append((e.message, i, value))
if len(errors):
raise MultiValueValidationError(errors)
return result


class AgreementFormForm(forms.Form):

def __init__(
self,
request: HttpRequest,
project: DataProject,
agreement_form: AgreementForm,
*args,
**kwargs
):
# Get initial data
if not kwargs.get("initial"):
kwargs["initial"] = self.set_initial(request, project, agreement_form)
super().__init__(*args, **kwargs)

def set_initial(self, request: HttpRequest, project: DataProject, agreement_form: AgreementForm) -> dict[str, object]:
"""
Returns initial data for when the form is rendered.
:param request: The current request
:type request: HttpRequest
:param project: The current data project
:type project: DataProject
:param agreement_form: The current agreement form this form is for
:type agreement_form: AgreementForm
:return: A dictionary of fields mapped to values
:rtype: dict[str, object]
"""
return {}

def get_data(self, request: HttpRequest, project: DataProject, agreement_form: AgreementForm) -> dict[str, object]:
"""
Returns the data to be set on the agreement form object.
:param request: The request
:type request: HttpRequest
:param project: The data project
:type project: DataProject
:param agreement_form: The agreement form this form is for
:type agreement_form: AgreementForm
:return: A dictionary of fields mapped to values
:rtype: dict[str, object]
"""
return self.cleaned_data


class AgreementForm4CEDUAForm(AgreementFormForm):

REGISTRANT_INDIVIDUAL = "individual"
REGISTRANT_MEMBER = "member"
REGISTRANT_OFFICIAL = "official"
REGISTRANT_CHOICES = (
(REGISTRANT_INDIVIDUAL, "an individual, requesting Data under this Agreement on behalf of themself"),
(REGISTRANT_MEMBER, "an institutional member, requesting Data under this Agreement signed by a representing institutional official"),
(REGISTRANT_OFFICIAL, "an institutional official, requesting Data under this Agreement on behalf of their institution and its agents and employees"),
)
registrant_is = forms.ChoiceField(label="I am a", choices=REGISTRANT_CHOICES)
signer_name = forms.CharField(label="Name/Title", max_length=300, required=False)
signer_phone = forms.CharField(label="Phone", max_length=300, required=False)
signer_email = forms.CharField(label="E-mail", max_length=300, required=False)
signer_signature = forms.CharField(label="Electronic Signature (Full Name)", max_length=300, required=False)
date = forms.CharField(label="Date", max_length=50, required=False)
institute_name = forms.CharField(label="Institution Name", max_length=300, required=False)
institute_address = forms.CharField(label="Institution Address", max_length=300, required=False)
institute_city = forms.CharField(label="Institution City", max_length=300, required=False)
institute_state = forms.CharField(label="Institution State", max_length=300, required=False)
institute_zip = forms.CharField(label="Institution Zip", max_length=300, required=False)
member_emails = MultiValueField(forms.EmailField(), "member_emails", required=False)

def clean(self):
cleaned_data = super().clean()

# Handle conditional requirements
if cleaned_data.get("registrant_is") in [self.REGISTRANT_INDIVIDUAL, self.REGISTRANT_OFFICIAL]:

# Set required fields under these conditions
required_fields = [
"signer_name",
"signer_phone",
"signer_email",
"signer_signature",
"date",
]

# Check required fields
for field in required_fields:
if not cleaned_data.get(field):
self.add_error(field, "This is a required field")

if cleaned_data.get("registrant_is") == self.REGISTRANT_OFFICIAL:

# Set required fields under these conditions
required_fields = [
"institute_name",
"institute_address",
"institute_city",
"institute_state",
"institute_zip",
"member_emails",
]

# Check required fields
for field in required_fields:
if not cleaned_data.get(field):
self.add_error(field, "This is a required field")

def set_initial(self, request: HttpRequest, project: DataProject, agreement_form: AgreementForm) -> dict[str, object]:
"""
Returns initial data for when the form is rendered.
:param request: The current request
:type request: HttpRequest
:param project: The current data project
:type project: DataProject
:param agreement_form: The current agreement form this form is for
:type agreement_form: AgreementForm
:return: A dictionary of fields mapped to values
:rtype: dict[str, object]
"""
initial = {}
# TODO: Disabled until we get the HTML updated to use form values
'''
# Set initial data for this form
user_jwt = request.COOKIES.get("DBMI_JWT", None)
profile = next(iter(get_current_user_profile(user_jwt).get("results", [])), {})
# Build dictionary
initial = {
"signer_name": f"{profile['first_name']} {profile['last_name']}",
"signer_email": request.user.email,
"signer_phone": profile.get("phone_number"),
"institute_name": profile.get("institution"),
"institute_address": profile.get("street_address1"),
"institute_city": profile.get("city"),
"institute_state": profile.get("state"),
"institute_zip": profile.get("zipcode"),
"institute_country": profile.get("country"),
}
'''
return initial
18 changes: 18 additions & 0 deletions app/projects/migrations/0107_agreementform_form_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2024-08-27 12:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('projects', '0106_agreementform_institutional_signers_and_more'),
]

operations = [
migrations.AddField(
model_name='agreementform',
name='form_class',
field=models.CharField(blank=True, max_length=300, null=True),
),
]
Loading

0 comments on commit 88f30c9

Please sign in to comment.