Skip to content

Commit b86c167

Browse files
authored
Merge pull request #629 from hms-dbmi/development
Development
2 parents 1c0a619 + 9bbd8a5 commit b86c167

18 files changed

+531
-163
lines changed

app/contact/views.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ def contact_form(request, project_key=None):
4646
if project.project_supervisors != '' and project.project_supervisors is not None:
4747
recipients = project.project_supervisors.split(',')
4848
else:
49-
recipients = settings.CONTACT_FORM_RECIPIENTS.split(',')
49+
recipients = settings.CONTACT_FORM_RECIPIENTS
5050

5151
except ObjectDoesNotExist:
52-
recipients = settings.CONTACT_FORM_RECIPIENTS.split(',')
52+
recipients = settings.CONTACT_FORM_RECIPIENTS
5353

5454
# Send it out.
5555
success = email_send(subject='DBMI Portal - Contact Inquiry Received',
@@ -116,7 +116,8 @@ def email_send(subject=None, recipients=None, email_template=None, extra=None):
116116
try:
117117
msg = EmailMultiAlternatives(subject=subject,
118118
body=msg_plain,
119-
from_email=settings.DEFAULT_FROM_EMAIL,
119+
from_email=settings.EMAIL_FROM_ADDRESS,
120+
reply_to=(settings.EMAIL_REPLY_TO_ADDRESS, ),
120121
to=recipients)
121122
msg.attach_alternative(msg_html, "text/html")
122123
msg.send()

app/hypatio/settings.py

+25-16
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@
135135
SSL_SETTING = "https"
136136
VERIFY_REQUESTS = True
137137

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

141141
RECAPTCHA_KEY = environment.get_str('RECAPTCHA_KEY', required=True)
142142
RECAPTCHA_CLIENT_ID = environment.get_str('RECAPTCHA_CLIENT_ID', required=True)
@@ -146,6 +146,7 @@
146146
S3_BUCKET = environment.get_str('S3_BUCKET', required=True)
147147

148148
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
149+
AWS_S3_SIGNATURE_VERSION = 's3v4'
149150
AWS_STORAGE_BUCKET_NAME = environment.get_str('S3_BUCKET', required=True)
150151
AWS_LOCATION = 'upload'
151152

@@ -240,21 +241,29 @@
240241

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

244-
# SMTP Email configuration
245-
EMAIL_SMTP = EMAIL_BACKEND == "django.core.mail.backends.smtp.EmailBackend"
246-
EMAIL_USE_SSL = environment.get_bool("EMAIL_USE_SSL", default=EMAIL_SMTP)
247-
EMAIL_HOST = environment.get_str("EMAIL_HOST", required=EMAIL_SMTP)
248-
EMAIL_HOST_USER = environment.get_str("EMAIL_HOST_USER", required=False)
249-
EMAIL_HOST_PASSWORD = environment.get_str("EMAIL_HOST_PASSWORD", required=False)
250-
EMAIL_PORT = environment.get_str("EMAIL_PORT", required=EMAIL_SMTP)
251-
252-
# AWS SES Email configuration
253-
EMAIL_SES = EMAIL_BACKEND == "django_ses.SESBackend"
254-
AWS_SES_SOURCE_ARN=environment.get_str("DBMI_SES_IDENTITY", required=EMAIL_SES)
255-
AWS_SES_FROM_ARN=environment.get_str("DBMI_SES_IDENTITY", required=EMAIL_SES)
256-
AWS_SES_RETURN_PATH_ARN=environment.get_str("DBMI_SES_IDENTITY", required=EMAIL_SES)
257-
USE_SES_V2 = True
246+
# SMTP Email configuration
247+
EMAIL_USE_SSL = environment.get_bool("EMAIL_USE_SSL", default=True)
248+
EMAIL_HOST = environment.get_str("EMAIL_HOST", required=True)
249+
EMAIL_HOST_USER = environment.get_str("EMAIL_HOST_USER", required=False)
250+
EMAIL_HOST_PASSWORD = environment.get_str("EMAIL_HOST_PASSWORD", required=False)
251+
EMAIL_PORT = environment.get_str("EMAIL_PORT", required=True)
252+
253+
elif EMAIL_BACKEND == "django_ses.SESBackend":
254+
255+
# AWS SES Email configuration
256+
AWS_SES_SOURCE_ARN = environment.get_str("DBMI_SES_IDENTITY", required=True)
257+
AWS_SES_FROM_ARN = environment.get_str("DBMI_SES_IDENTITY", required=True)
258+
AWS_SES_RETURN_PATH_ARN = environment.get_str("DBMI_SES_IDENTITY", required=True)
259+
USE_SES_V2 = True
260+
261+
else:
262+
raise SystemError(f"Email backend '{EMAIL_BACKEND}' is not supported for this application")
263+
264+
# Set default from address
265+
EMAIL_FROM_ADDRESS = environment.get_str("EMAIL_FROM_ADDRESS", required=True)
266+
EMAIL_REPLY_TO_ADDRESS = environment.get_str("EMAIL_REPLY_TO_ADDRESS", default=EMAIL_FROM_ADDRESS)
258267

259268
#####################################################################################
260269

app/manage/forms.py

+4
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,7 @@ def __init__(self, *args, **kwargs):
7979
# Limit agreement form choices to those related to the passed project
8080
if project_key:
8181
self.fields['agreement_form'].queryset = DataProject.objects.get(project_key=project_key).agreement_forms.all()
82+
83+
84+
class UploadSignedAgreementFormFileForm(forms.Form):
85+
file = forms.FileField(label="Signed Agreement Form PDF", required=True)

app/manage/urls.py

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from manage.views import ProjectPendingParticipants
99
from manage.views import team_notification
1010
from manage.views import UploadSignedAgreementFormView
11+
from manage.views import UploadSignedAgreementFormFileView
1112

1213
from manage.api import set_dataproject_details
1314
from manage.api import set_dataproject_registration_status
@@ -62,6 +63,7 @@
6263
re_path(r'^get-project-participants/(?P<project_key>[^/]+)/$', ProjectParticipants.as_view(), name='get-project-participants'),
6364
re_path(r'^get-project-pending-participants/(?P<project_key>[^/]+)/$', ProjectPendingParticipants.as_view(), name='get-project-pending-participants'),
6465
re_path(r'^upload-signed-agreement-form/(?P<project_key>[^/]+)/(?P<user_email>[^/]+)/$', UploadSignedAgreementFormView.as_view(), name='upload-signed-agreement-form'),
66+
re_path(r'^upload-signed-agreement-form-file/(?P<signed_agreement_form_id>[^/]+)/$', UploadSignedAgreementFormFileView.as_view(), name='upload-signed-agreement-form-file'),
6567
re_path(r'^(?P<project_key>[^/]+)/$', DataProjectManageView.as_view(), name='manage-project'),
6668
re_path(r'^(?P<project_key>[^/]+)/(?P<team_leader>[^/]+)/$', manage_team, name='manage-team'),
6769
]

app/manage/views.py

+88-1
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
from django.core.mail import EmailMultiAlternatives
2020
from django.template.loader import render_to_string
2121
from dbmi_client import fileservice
22+
from django.shortcuts import get_object_or_404
2223

2324
from hypatio.sciauthz_services import SciAuthZ
2425
from hypatio.scireg_services import get_user_profile, get_distinct_countries_participating
2526

2627
from manage.forms import NotificationForm
2728
from manage.models import ChallengeTaskSubmissionExport
2829
from manage.forms import UploadSignedAgreementFormForm
30+
from manage.forms import UploadSignedAgreementFormFileForm
2931
from projects.models import AgreementForm, ChallengeTaskSubmission
3032
from projects.models import DataProject
3133
from projects.models import Participant
@@ -603,7 +605,8 @@ def team_notification(request, project_key=None):
603605
try:
604606
msg = EmailMultiAlternatives(subject=subject,
605607
body=msg_plain,
606-
from_email=settings.DEFAULT_FROM_EMAIL,
608+
from_email=settings.EMAIL_FROM_ADDRESS,
609+
reply_to=(settings.EMAIL_REPLY_TO_ADDRESS, ),
607610
to=[team.team_leader.email])
608611
msg.attach_alternative(msg_html, "text/html")
609612
msg.send()
@@ -908,3 +911,87 @@ def post(self, request, project_key, user_email, *args, **kwargs):
908911
response['X-IC-Script'] += "$('#page-modal').modal('hide');"
909912

910913
return response
914+
915+
916+
@method_decorator([user_auth_and_jwt], name='dispatch')
917+
class UploadSignedAgreementFormFileView(View):
918+
"""
919+
View to upload signed agreement form files for participants.
920+
921+
* Requires token authentication.
922+
* Only admin users are able to access this view.
923+
"""
924+
def get(self, request, signed_agreement_form_id, *args, **kwargs):
925+
"""
926+
Return the upload form template
927+
"""
928+
user = request.user
929+
user_jwt = request.COOKIES.get("DBMI_JWT", None)
930+
931+
signed_agreement_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id)
932+
933+
sciauthz = SciAuthZ(user_jwt, user.email)
934+
is_manager = sciauthz.user_has_manage_permission(signed_agreement_form.project.project_key)
935+
936+
if not is_manager:
937+
logger.debug('User {email} does not have MANAGE permissions for item {project_key}.'.format(
938+
email=user.email,
939+
project_key=signed_agreement_form.project.project_key
940+
))
941+
return HttpResponse(403)
942+
943+
# Return file upload form
944+
form = UploadSignedAgreementFormFileForm()
945+
946+
# Set context
947+
context = {
948+
"form": form,
949+
"signed_agreement_form_id": signed_agreement_form_id,
950+
}
951+
952+
# Render html
953+
return render(request, "manage/upload-signed-agreement-form-file.html", context)
954+
955+
def post(self, request, signed_agreement_form_id, *args, **kwargs):
956+
"""
957+
Process the form
958+
"""
959+
user = request.user
960+
user_jwt = request.COOKIES.get("DBMI_JWT", None)
961+
962+
signed_agreement_form = get_object_or_404(SignedAgreementForm, id=signed_agreement_form_id)
963+
964+
sciauthz = SciAuthZ(user_jwt, user.email)
965+
is_manager = sciauthz.user_has_manage_permission(signed_agreement_form.project.project_key)
966+
967+
if not is_manager:
968+
logger.debug('User {email} does not have MANAGE permissions for item {project_key}.'.format(
969+
email=user.email,
970+
project_key=signed_agreement_form.project.project_key
971+
))
972+
return HttpResponse(403)
973+
974+
# Assembles the form and run validation.
975+
form = UploadSignedAgreementFormFileForm(data=request.POST, files=request.FILES)
976+
if not form.is_valid():
977+
logger.warning('Form failed: {}'.format(form.errors.as_json()))
978+
return HttpResponse(status=400)
979+
980+
logger.debug(f"[upload_signed_agreement_form_file] Data -> {form.cleaned_data}")
981+
982+
# Set the file and save
983+
signed_agreement_form.upload = form.cleaned_data['file']
984+
signed_agreement_form.save()
985+
986+
# Create the response.
987+
response = HttpResponse(status=201)
988+
989+
# Setup the script run.
990+
response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format(
991+
"success", "Signed agreement form file successfully uploaded", "thumbs-up"
992+
)
993+
994+
# Close the modal
995+
response['X-IC-Script'] += "$('#page-modal').modal('hide');"
996+
997+
return response
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.4 on 2023-09-12 16:20
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('projects', '0101_challengetask_submission_instructions'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='agreementform',
15+
name='skippable',
16+
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.'),
17+
),
18+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.1 on 2023-06-28 10:32
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('projects', '0102_agreementform_skippable'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='dataproject',
15+
name='commercial_only',
16+
field=models.BooleanField(default=False, help_text='Commercial only projects are for commercial entities only'),
17+
),
18+
]

app/projects/models.py

+9
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@ class AgreementForm(models.Model):
253253
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.")
254254
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")
255255
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")
256+
skippable = models.BooleanField(
257+
default=False,
258+
help_text="Allow participants to skip this step in instances where they have submitted the agreement form via"
259+
" email or some other means. They will be required to include the name and contact information of"
260+
" the person who they submitted their signed agreement form to."
261+
)
256262

257263
# Meta
258264
created = models.DateTimeField(auto_now_add=True)
@@ -323,6 +329,9 @@ class DataProject(models.Model):
323329
)
324330
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")
325331

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

328337
order = models.IntegerField(blank=True, null=True, help_text="Indicate an order (lowest number = highest order) for how the DataProjects should be listed.")

app/static/js/portal.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
/**
3+
* Finds all 'button' elements contained in a form and toggles their 'disabled' proeprty.
4+
* @param {String} formSelector The jQuery selector of the form to disable buttons for.
5+
*/
6+
function toggleFormButtons(formSelector) {
7+
8+
// Toggle disabled state of all buttons in form
9+
$(formSelector).find("button").each(function() {
10+
$(this).prop("disabled", !$(this).prop("disabled"));
11+
});
12+
}

app/templates/base.html

+49-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
<script type='text/javascript' src="{% static 'plugins/bootstrap-notify/bootstrap-notify.min.js' %}"></script>
4848
<script type="text/javascript" src="{% static 'plugins/intercooler/intercooler.min.js' %}"></script>
4949

50+
<!-- Include portal specific Javascript -->
51+
<script type='text/javascript' src="{% static 'js/portal.js' %}"></script>
52+
5053
<title>{% block tab_name %}DBMI Portal{% endblock %}</title>
5154

5255
<script type="application/javascript">
@@ -226,12 +229,14 @@
226229
<a class="dropdown-toggle" data-toggle="dropdown" href="#" id="download">{{ request.user.email }}<span class="caret"></span></a>
227230
<ul class="dropdown-menu" aria-labelledby="downloadn">
228231
<li><a class="nav-link" href="{% url 'profile:profile' %}">Profile <span class="fas fa-sm fa-user"></span></a></li>
229-
230232
{% is_project_manager request as is_manager %}
231233
{% if is_manager %}
232-
<li><a class="nav-link" href="{% url 'manage:manage-projects' %}">Manage Projects</a></li>
234+
<li><a class="nav-link" href="{% url 'manage:manage-projects' %}">Manage Projects</a></li>
233235
{% endif %}
234-
236+
{% if dbmiuser and dbmiuser.jwt %}
237+
<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>
238+
{% endif %}
239+
</a></li>
235240
<li><a class="nav-link" href="{% url 'profile:signout' %}">Sign Out</a></li>
236241
</ul>
237242
</li>
@@ -255,4 +260,45 @@
255260

256261
{# Allow for some javascript to be added per page #}
257262
{% block javascript %}{% endblock %}
263+
264+
<script type="text/javascript" src="{% static 'js/clipboard.min.js' %}"></script>
265+
<script type="application/javascript">
266+
$(document).ready(function(){
267+
268+
// Initialize tooltips
269+
$('[data-toggle="tooltip"]').tooltip();
270+
271+
// Reset tooltips
272+
$('[data-toggle="tooltip"][data-tooltip-title]').on('hidden.bs.tooltip', function(){
273+
$(this).attr('data-original-title', $(this).attr('data-tooltip-title'));
274+
});
275+
276+
// Setup copy button
277+
var clipboards = new ClipboardJS(".clipboard-copy");
278+
clipboards.on('success', function(e) {
279+
280+
// Update tooltip
281+
$(e.trigger).attr('data-original-title', "Copied!")
282+
.tooltip('fixTitle')
283+
.tooltip('setContent')
284+
.tooltip('show');
285+
286+
e.clearSelection();
287+
});
288+
289+
clipboards.on('error', function(e) {
290+
291+
// Update tooltip
292+
$(e.trigger).attr('data-original-title', "Error!")
293+
.tooltip('fixTitle')
294+
.tooltip('setContent')
295+
.tooltip('show');
296+
297+
// Log it
298+
console.log('Copy error:' + e.toString());
299+
300+
e.clearSelection();
301+
});
302+
});
303+
</script>
258304
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{% load bootstrap3 %}
2+
3+
<form id="upload-signed-agreement-form-file-form" class="file-upload-form" method="post" enctype="multipart/form-data"
4+
ic-post-to="{% url 'manage:upload-signed-agreement-form-file' signed_agreement_form_id %}"
5+
ic-on-beforeSend="toggleFormButtons('#upload-signed-agreement-form-file-form')"
6+
ic-on-complete="toggleFormButtons('#upload-signed-agreement-form-file-form')">
7+
<div class="modal-header modal-header-primary">
8+
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
9+
<h3 class="modal-title">Signed Agreement Form File Upload</h3>
10+
</div>
11+
<div class="modal-body">
12+
{% csrf_token %}
13+
{% bootstrap_form form inline=True %}
14+
</div>
15+
<div class="modal-footer">
16+
{% buttons %}
17+
<button id="file-upload-close-button" type="button" class="btn btn-default" data-dismiss="modal">Close</button>
18+
<button id="file-upload-submit-button" type="submit" class="btn btn-primary">Submit
19+
<span id="file-upload-form-indicator" style="display: none; margin-left: 5px;" class="ic-indicator fa fa-spinner fa-spin"></span>
20+
</button>
21+
{% endbuttons %}
22+
</div>
23+
</form>

0 commit comments

Comments
 (0)