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

feat(projects): Setup ability to use a Form class to manage rendering… #700

Merged
merged 1 commit into from
Aug 29, 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
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