Skip to content

Commit

Permalink
Merge pull request #629 from hms-dbmi/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
b32147 authored Sep 19, 2023
2 parents 4728c82 + 9d35af7 commit 60d67ba
Show file tree
Hide file tree
Showing 18 changed files with 531 additions and 163 deletions.
7 changes: 4 additions & 3 deletions app/contact/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ def contact_form(request, project_key=None):
if project.project_supervisors != '' and project.project_supervisors is not None:
recipients = project.project_supervisors.split(',')
else:
recipients = settings.CONTACT_FORM_RECIPIENTS.split(',')
recipients = settings.CONTACT_FORM_RECIPIENTS

except ObjectDoesNotExist:
recipients = settings.CONTACT_FORM_RECIPIENTS.split(',')
recipients = settings.CONTACT_FORM_RECIPIENTS

# Send it out.
success = email_send(subject='DBMI Portal - Contact Inquiry Received',
Expand Down Expand Up @@ -116,7 +116,8 @@ def email_send(subject=None, recipients=None, email_template=None, extra=None):
try:
msg = EmailMultiAlternatives(subject=subject,
body=msg_plain,
from_email=settings.DEFAULT_FROM_EMAIL,
from_email=settings.EMAIL_FROM_ADDRESS,
reply_to=(settings.EMAIL_REPLY_TO_ADDRESS, ),
to=recipients)
msg.attach_alternative(msg_html, "text/html")
msg.send()
Expand Down
41 changes: 25 additions & 16 deletions app/hypatio/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@
SSL_SETTING = "https"
VERIFY_REQUESTS = True

CONTACT_FORM_RECIPIENTS="[email protected]"
DEFAULT_FROM_EMAIL="[email protected]"
# Pass a list of email addresses
CONTACT_FORM_RECIPIENTS = environment.get_list('CONTACT_FORM_RECIPIENTS', required=True)

RECAPTCHA_KEY = environment.get_str('RECAPTCHA_KEY', required=True)
RECAPTCHA_CLIENT_ID = environment.get_str('RECAPTCHA_CLIENT_ID', required=True)
Expand All @@ -146,6 +146,7 @@
S3_BUCKET = environment.get_str('S3_BUCKET', required=True)

DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_S3_SIGNATURE_VERSION = 's3v4'
AWS_STORAGE_BUCKET_NAME = environment.get_str('S3_BUCKET', required=True)
AWS_LOCATION = 'upload'

Expand Down Expand Up @@ -240,21 +241,29 @@

# Determine email backend
EMAIL_BACKEND = environment.get_str("EMAIL_BACKEND", required=True)
if EMAIL_BACKEND == "django.core.mail.backends.smtp.EmailBackend":

# SMTP Email configuration
EMAIL_SMTP = EMAIL_BACKEND == "django.core.mail.backends.smtp.EmailBackend"
EMAIL_USE_SSL = environment.get_bool("EMAIL_USE_SSL", default=EMAIL_SMTP)
EMAIL_HOST = environment.get_str("EMAIL_HOST", required=EMAIL_SMTP)
EMAIL_HOST_USER = environment.get_str("EMAIL_HOST_USER", required=False)
EMAIL_HOST_PASSWORD = environment.get_str("EMAIL_HOST_PASSWORD", required=False)
EMAIL_PORT = environment.get_str("EMAIL_PORT", required=EMAIL_SMTP)

# AWS SES Email configuration
EMAIL_SES = EMAIL_BACKEND == "django_ses.SESBackend"
AWS_SES_SOURCE_ARN=environment.get_str("DBMI_SES_IDENTITY", required=EMAIL_SES)
AWS_SES_FROM_ARN=environment.get_str("DBMI_SES_IDENTITY", required=EMAIL_SES)
AWS_SES_RETURN_PATH_ARN=environment.get_str("DBMI_SES_IDENTITY", required=EMAIL_SES)
USE_SES_V2 = True
# SMTP Email configuration
EMAIL_USE_SSL = environment.get_bool("EMAIL_USE_SSL", default=True)
EMAIL_HOST = environment.get_str("EMAIL_HOST", required=True)
EMAIL_HOST_USER = environment.get_str("EMAIL_HOST_USER", required=False)
EMAIL_HOST_PASSWORD = environment.get_str("EMAIL_HOST_PASSWORD", required=False)
EMAIL_PORT = environment.get_str("EMAIL_PORT", required=True)

elif EMAIL_BACKEND == "django_ses.SESBackend":

# AWS SES Email configuration
AWS_SES_SOURCE_ARN = environment.get_str("DBMI_SES_IDENTITY", required=True)
AWS_SES_FROM_ARN = environment.get_str("DBMI_SES_IDENTITY", required=True)
AWS_SES_RETURN_PATH_ARN = environment.get_str("DBMI_SES_IDENTITY", required=True)
USE_SES_V2 = True

else:
raise SystemError(f"Email backend '{EMAIL_BACKEND}' is not supported for this application")

# Set default from address
EMAIL_FROM_ADDRESS = environment.get_str("EMAIL_FROM_ADDRESS", required=True)
EMAIL_REPLY_TO_ADDRESS = environment.get_str("EMAIL_REPLY_TO_ADDRESS", default=EMAIL_FROM_ADDRESS)

#####################################################################################

Expand Down
4 changes: 4 additions & 0 deletions app/manage/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,7 @@ def __init__(self, *args, **kwargs):
# Limit agreement form choices to those related to the passed project
if project_key:
self.fields['agreement_form'].queryset = DataProject.objects.get(project_key=project_key).agreement_forms.all()


class UploadSignedAgreementFormFileForm(forms.Form):
file = forms.FileField(label="Signed Agreement Form PDF", required=True)
2 changes: 2 additions & 0 deletions app/manage/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from manage.views import ProjectPendingParticipants
from manage.views import team_notification
from manage.views import UploadSignedAgreementFormView
from manage.views import UploadSignedAgreementFormFileView

from manage.api import set_dataproject_details
from manage.api import set_dataproject_registration_status
Expand Down Expand Up @@ -62,6 +63,7 @@
re_path(r'^get-project-participants/(?P<project_key>[^/]+)/$', ProjectParticipants.as_view(), name='get-project-participants'),
re_path(r'^get-project-pending-participants/(?P<project_key>[^/]+)/$', ProjectPendingParticipants.as_view(), name='get-project-pending-participants'),
re_path(r'^upload-signed-agreement-form/(?P<project_key>[^/]+)/(?P<user_email>[^/]+)/$', UploadSignedAgreementFormView.as_view(), name='upload-signed-agreement-form'),
re_path(r'^upload-signed-agreement-form-file/(?P<signed_agreement_form_id>[^/]+)/$', UploadSignedAgreementFormFileView.as_view(), name='upload-signed-agreement-form-file'),
re_path(r'^(?P<project_key>[^/]+)/$', DataProjectManageView.as_view(), name='manage-project'),
re_path(r'^(?P<project_key>[^/]+)/(?P<team_leader>[^/]+)/$', manage_team, name='manage-team'),
]
89 changes: 88 additions & 1 deletion app/manage/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from dbmi_client import fileservice
from django.shortcuts import get_object_or_404

from hypatio.sciauthz_services import SciAuthZ
from hypatio.scireg_services import get_user_profile, get_distinct_countries_participating

from manage.forms import NotificationForm
from manage.models import ChallengeTaskSubmissionExport
from manage.forms import UploadSignedAgreementFormForm
from manage.forms import UploadSignedAgreementFormFileForm
from projects.models import AgreementForm, ChallengeTaskSubmission
from projects.models import DataProject
from projects.models import Participant
Expand Down Expand Up @@ -603,7 +605,8 @@ def team_notification(request, project_key=None):
try:
msg = EmailMultiAlternatives(subject=subject,
body=msg_plain,
from_email=settings.DEFAULT_FROM_EMAIL,
from_email=settings.EMAIL_FROM_ADDRESS,
reply_to=(settings.EMAIL_REPLY_TO_ADDRESS, ),
to=[team.team_leader.email])
msg.attach_alternative(msg_html, "text/html")
msg.send()
Expand Down Expand Up @@ -908,3 +911,87 @@ def post(self, request, project_key, user_email, *args, **kwargs):
response['X-IC-Script'] += "$('#page-modal').modal('hide');"

return response


@method_decorator([user_auth_and_jwt], name='dispatch')
class UploadSignedAgreementFormFileView(View):
"""
View to upload signed agreement form files for participants.
* Requires token authentication.
* Only admin users are able to access this view.
"""
def get(self, request, signed_agreement_form_id, *args, **kwargs):
"""
Return the upload form template
"""
user = request.user
user_jwt = request.COOKIES.get("DBMI_JWT", None)

signed_agreement_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id)

sciauthz = SciAuthZ(user_jwt, user.email)
is_manager = sciauthz.user_has_manage_permission(signed_agreement_form.project.project_key)

if not is_manager:
logger.debug('User {email} does not have MANAGE permissions for item {project_key}.'.format(
email=user.email,
project_key=signed_agreement_form.project.project_key
))
return HttpResponse(403)

# Return file upload form
form = UploadSignedAgreementFormFileForm()

# Set context
context = {
"form": form,
"signed_agreement_form_id": signed_agreement_form_id,
}

# Render html
return render(request, "manage/upload-signed-agreement-form-file.html", context)

def post(self, request, signed_agreement_form_id, *args, **kwargs):
"""
Process the form
"""
user = request.user
user_jwt = request.COOKIES.get("DBMI_JWT", None)

signed_agreement_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id)

sciauthz = SciAuthZ(user_jwt, user.email)
is_manager = sciauthz.user_has_manage_permission(signed_agreement_form.project.project_key)

if not is_manager:
logger.debug('User {email} does not have MANAGE permissions for item {project_key}.'.format(
email=user.email,
project_key=signed_agreement_form.project.project_key
))
return HttpResponse(403)

# Assembles the form and run validation.
form = UploadSignedAgreementFormFileForm(data=request.POST, files=request.FILES)
if not form.is_valid():
logger.warning('Form failed: {}'.format(form.errors.as_json()))
return HttpResponse(status=400)

logger.debug(f"[upload_signed_agreement_form_file] Data -> {form.cleaned_data}")

# Set the file and save
signed_agreement_form.upload = form.cleaned_data['file']
signed_agreement_form.save()

# Create the response.
response = HttpResponse(status=201)

# Setup the script run.
response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format(
"success", "Signed agreement form file successfully uploaded", "thumbs-up"
)

# Close the modal
response['X-IC-Script'] += "$('#page-modal').modal('hide');"

return response
18 changes: 18 additions & 0 deletions app/projects/migrations/0102_agreementform_skippable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.4 on 2023-09-12 16:20

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('projects', '0101_challengetask_submission_instructions'),
]

operations = [
migrations.AddField(
model_name='agreementform',
name='skippable',
field=models.BooleanField(default=False, help_text='Allow participants to skip this step in instances where they have submitted the agreement form via email or some other means. They will be required to include the name and contact information of the person who they submitted their signed agreement form to.'),
),
]
18 changes: 18 additions & 0 deletions app/projects/migrations/0103_dataproject_commercial_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.1 on 2023-06-28 10:32

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('projects', '0102_agreementform_skippable'),
]

operations = [
migrations.AddField(
model_name='dataproject',
name='commercial_only',
field=models.BooleanField(default=False, help_text='Commercial only projects are for commercial entities only'),
),
]
9 changes: 9 additions & 0 deletions app/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ class AgreementForm(models.Model):
order = models.IntegerField(default=50, help_text="Indicate an order (lowest number = first listing) for how the Agreement Forms should be listed during registration workflows.")
content = models.TextField(blank=True, null=True, help_text="If Agreement Form type is set to 'MODEL', the HTML set here will be rendered for the user")
internal = models.BooleanField(default=False, help_text="Internal agreement forms are never presented to participants and are only submitted by administrators on behalf of participants")
skippable = models.BooleanField(
default=False,
help_text="Allow participants to skip this step in instances where they have submitted the agreement form via"
" email or some other means. They will be required to include the name and contact information of"
" the person who they submitted their signed agreement form to."
)

# Meta
created = models.DateTimeField(auto_now_add=True)
Expand Down Expand Up @@ -323,6 +329,9 @@ class DataProject(models.Model):
)
teams_source_message = models.TextField(default="Teams approved there will be automatically added to this project but will need still need approval for this project.", blank=True, null=True, verbose_name="Teams Source Message")

# Set this to show badging to indicate that only commercial entities should apply for access
commercial_only = models.BooleanField(default=False, blank=False, null=False, help_text="Commercial only projects are for commercial entities only")

show_jwt = models.BooleanField(default=False, blank=False, null=False)

order = models.IntegerField(blank=True, null=True, help_text="Indicate an order (lowest number = highest order) for how the DataProjects should be listed.")
Expand Down
12 changes: 12 additions & 0 deletions app/static/js/portal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

/**
* Finds all 'button' elements contained in a form and toggles their 'disabled' proeprty.
* @param {String} formSelector The jQuery selector of the form to disable buttons for.
*/
function toggleFormButtons(formSelector) {

// Toggle disabled state of all buttons in form
$(formSelector).find("button").each(function() {
$(this).prop("disabled", !$(this).prop("disabled"));
});
}
52 changes: 49 additions & 3 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
<script type='text/javascript' src="{% static 'plugins/bootstrap-notify/bootstrap-notify.min.js' %}"></script>
<script type="text/javascript" src="{% static 'plugins/intercooler/intercooler.min.js' %}"></script>

<!-- Include portal specific Javascript -->
<script type='text/javascript' src="{% static 'js/portal.js' %}"></script>

<title>{% block tab_name %}DBMI Portal{% endblock %}</title>

<script type="application/javascript">
Expand Down Expand Up @@ -226,12 +229,14 @@
<a class="dropdown-toggle" data-toggle="dropdown" href="#" id="download">{{ request.user.email }}<span class="caret"></span></a>
<ul class="dropdown-menu" aria-labelledby="downloadn">
<li><a class="nav-link" href="{% url 'profile:profile' %}">Profile <span class="fas fa-sm fa-user"></span></a></li>

{% is_project_manager request as is_manager %}
{% if is_manager %}
<li><a class="nav-link" href="{% url 'manage:manage-projects' %}">Manage Projects</a></li>
<li><a class="nav-link" href="{% url 'manage:manage-projects' %}">Manage Projects</a></li>
{% endif %}

{% if dbmiuser and dbmiuser.jwt %}
<li><a id="jwt-copy" class="nav-link clipboard-copy" data-clipboard-text="{{ dbmiuser.jwt|default:"<empty>" }}" data-toggle="tooltip" style="cursor: pointer;" title="Copy API Key" data-tooltip-title="Copy API Key">API Key <i class="fa fa-wrench" aria-hidden="true"></i>
{% endif %}
</a></li>
<li><a class="nav-link" href="{% url 'profile:signout' %}">Sign Out</a></li>
</ul>
</li>
Expand All @@ -255,4 +260,45 @@

{# Allow for some javascript to be added per page #}
{% block javascript %}{% endblock %}

<script type="text/javascript" src="{% static 'js/clipboard.min.js' %}"></script>
<script type="application/javascript">
$(document).ready(function(){

// Initialize tooltips
$('[data-toggle="tooltip"]').tooltip();

// Reset tooltips
$('[data-toggle="tooltip"][data-tooltip-title]').on('hidden.bs.tooltip', function(){
$(this).attr('data-original-title', $(this).attr('data-tooltip-title'));
});

// Setup copy button
var clipboards = new ClipboardJS(".clipboard-copy");
clipboards.on('success', function(e) {

// Update tooltip
$(e.trigger).attr('data-original-title', "Copied!")
.tooltip('fixTitle')
.tooltip('setContent')
.tooltip('show');

e.clearSelection();
});

clipboards.on('error', function(e) {

// Update tooltip
$(e.trigger).attr('data-original-title', "Error!")
.tooltip('fixTitle')
.tooltip('setContent')
.tooltip('show');

// Log it
console.log('Copy error:' + e.toString());

e.clearSelection();
});
});
</script>
</html>
23 changes: 23 additions & 0 deletions app/templates/manage/upload-signed-agreement-form-file.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% load bootstrap3 %}

<form id="upload-signed-agreement-form-file-form" class="file-upload-form" method="post" enctype="multipart/form-data"
ic-post-to="{% url 'manage:upload-signed-agreement-form-file' signed_agreement_form_id %}"
ic-on-beforeSend="toggleFormButtons('#upload-signed-agreement-form-file-form')"
ic-on-complete="toggleFormButtons('#upload-signed-agreement-form-file-form')">
<div class="modal-header modal-header-primary">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-title">Signed Agreement Form File Upload</h3>
</div>
<div class="modal-body">
{% csrf_token %}
{% bootstrap_form form inline=True %}
</div>
<div class="modal-footer">
{% buttons %}
<button id="file-upload-close-button" type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button id="file-upload-submit-button" type="submit" class="btn btn-primary">Submit
<span id="file-upload-form-indicator" style="display: none; margin-left: 5px;" class="ic-indicator fa fa-spinner fa-spin"></span>
</button>
{% endbuttons %}
</div>
</form>
Loading

0 comments on commit 60d67ba

Please sign in to comment.