diff --git a/Dockerfile b/Dockerfile index a8bdbe87..4ca2af17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,10 @@ +# Set arch +ARG BUILD_ARCH=amd64 + FROM hmsdbmitc/dbmisvc:debian12-slim-python3.11-0.6.2 AS builder +ARG BUILD_ARCH + # Install requirements RUN apt-get update \ && apt-get install -y --no-install-recommends \ @@ -10,8 +15,12 @@ RUN apt-get update \ default-libmysqlclient-dev \ libssl-dev \ pkg-config \ + libfontconfig \ && rm -rf /var/lib/apt/lists/* +# Install requirements for PDF generation +ADD phantomjs-2.1.1-${BUILD_ARCH}.tar.gz /tmp/ + # Add requirements ADD requirements.* / @@ -27,6 +36,7 @@ ARG APP_CODENAME="hypatio" ARG VERSION ARG COMMIT ARG DATE +ARG BUILD_ARCH LABEL org.label-schema.schema-version=1.0 \ org.label-schema.vendor="HMS-DBMI" \ @@ -38,12 +48,16 @@ LABEL org.label-schema.schema-version=1.0 \ org.label-schema.vcs-url="https://github.com/hms-dbmi/hypatio-app" \ org.label-schema.vcf-ref=${COMMIT} +# Copy PhantomJS binary +COPY --from=builder /tmp/phantomjs /usr/local/bin/phantomjs + # Copy Python wheels from builder COPY --from=builder /root/wheels /root/wheels # Install requirements RUN apt-get update \ && apt-get install -y --no-install-recommends \ + libfontconfig \ default-libmysqlclient-dev \ libmagic1 \ && rm -rf /var/lib/apt/lists/* diff --git a/app/hypatio/settings.py b/app/hypatio/settings.py index d2344efa..4530b851 100644 --- a/app/hypatio/settings.py +++ b/app/hypatio/settings.py @@ -59,6 +59,7 @@ 'django_jsonfield_backport', 'django_q', 'django_ses', + 'pdf', ] MIDDLEWARE = [ @@ -149,7 +150,9 @@ 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' + +PROJECTS_UPLOADS_PREFIX = "upload" +PROJECTS_DOCUMENTS_PREFIX = "documents" ########## @@ -316,35 +319,12 @@ ) # Output the standard logging configuration -LOGGING = config('HYPATIO', root_level=logging.DEBUG) - -# Disable warning level for 4xx request logging -LOGGING['loggers'].update({ - 'django.request': { - 'handlers': ['console'], - 'level': 'ERROR', - 'propagate': True, - }, - 'boto3': { - 'handlers': ['console'], - 'level': 'INFO', - 'propagate': True, - }, - 'botocore': { - 'handlers': ['console'], - 'level': 'INFO', - 'propagate': True, - }, - 's3transfer': { - 'handlers': ['console'], - 'level': 'INFO', - 'propagate': True, - }, - 'urllib3': { - 'handlers': ['console'], - 'level': 'INFO', - 'propagate': True, - }, +LOGGING = config('HYPATIO', root_level=logging.DEBUG, logger_levels={ + "django.request": "ERROR", + "boto3": "INFO", + "botocore": "INFO", + "s3transfer": "INFO", + "urllib3": "INFO", }) ##################################################################################### diff --git a/app/pdf/__init__.py b/app/pdf/__init__.py new file mode 100644 index 00000000..71c89894 --- /dev/null +++ b/app/pdf/__init__.py @@ -0,0 +1,19 @@ +""" ______ __ + ____ ____/ / __/ ____ ____ ____ ___ _________ _/ /_____ _____ + / __ \/ __ / /_ / __ `/ _ \/ __ \/ _ \/ ___/ __ `/ __/ __ \/ ___/ + / /_/ / /_/ / __/ / /_/ / __/ / / / __/ / / /_/ / /_/ /_/ / / + / .___/\__,_/_/_____\__, /\___/_/ /_/\___/_/ \__,_/\__/\____/_/ +/_/ /_____/____/ +""" + +__title__ = 'PDF Generator' +__version__ = '0.1.3' +__author__ = 'Charles TISSIER' +__license__ = 'MIT' +__copyright__ = 'Copyright 2017 Charles TISSIER' + +# Version synonym +VERSION = __version__ + + +default_app_config = 'pdf.apps.PdfGeneratorConfig' \ No newline at end of file diff --git a/app/pdf/apps.py b/app/pdf/apps.py new file mode 100644 index 00000000..e5f59f7f --- /dev/null +++ b/app/pdf/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class PdfGeneratorConfig(AppConfig): + name = 'pdf' diff --git a/app/pdf/generators.py b/app/pdf/generators.py new file mode 100644 index 00000000..1f418527 --- /dev/null +++ b/app/pdf/generators.py @@ -0,0 +1,80 @@ +import subprocess +import os +import random + +from .settings import pdf_settings +from django.http import HttpResponse +from django.core.files.base import ContentFile + + +class PDFGenerator(object): + def __init__(self, html, paperformat='A4', zoom=1, script=pdf_settings.DEFAULT_RASTERIZE_SCRIPT, + temp_dir=pdf_settings.DEFAULT_TEMP_DIR): + self.script = script + self.temp_dir = temp_dir + self.html = html + self.html_file = self.__get_html_filepath() + self.pdf_file = self.__get_pdf_filepath() + self.paperformat = paperformat + self.zoom = zoom + self.pdf_data = None + + self.__write_html() + self.__generate() + self.__set_pdf_data() + self.__remove_source_file() + + def __write_html(self): + with open(self.html_file, 'w') as f: + f.write(self.html) + f.close() + + def __get_html_filepath(self): + return os.path.join(self.temp_dir, '{}.html'.format(PDFGenerator.get_random_filename(25))) + + def __get_pdf_filepath(self): + return os.path.join(self.temp_dir, '{}.pdf'.format(PDFGenerator.get_random_filename(25))) + + def __generate(self): + """ + call the following command: + phantomjs rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom] + """ + phantomjs_env = os.environ.copy() + phantomjs_env["OPENSSL_CONF"] = "/etc/openssl/" + command = [ + pdf_settings.PHANTOMJS_BIN_PATH, + '--ssl-protocol=any', + '--ignore-ssl-errors=yes', + self.script, + self.html_file, + self.pdf_file, + self.paperformat, + str(self.zoom) + ] + return subprocess.call(command, env=phantomjs_env) + + def __set_pdf_data(self): + with open(self.pdf_file, "rb") as pdf: + self.pdf_data = pdf.read() + + def get_content_file(self, filename): + return ContentFile(self.pdf_data, name=filename) + + def get_data(self): + return self.pdf_data + + def get_http_response(self, filename): + response = HttpResponse(self.pdf_data, content_type='application/pdf') + response['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(filename) + return response + + def __remove_source_file(self): + html_rm = subprocess.call(['rm', self.html_file]) + pdf_rm = subprocess.call(['rm', self.pdf_file]) + return html_rm & pdf_rm + + @staticmethod + def get_random_filename(nb=50): + choices = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return "".join([random.choice(choices) for _ in range(nb)]) diff --git a/app/pdf/rasterize.js b/app/pdf/rasterize.js new file mode 100644 index 00000000..76869caa --- /dev/null +++ b/app/pdf/rasterize.js @@ -0,0 +1,50 @@ +// https://github.com/ariya/phantomjs/blob/master/examples/rasterize.js +"use strict"; +var page = require('webpage').create(), + system = require('system'), + address, output, size, pageWidth, pageHeight; + +if (system.args.length < 3 || system.args.length > 5) { + console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]'); + console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"'); + console.log(' image (png/jpg output) examples: "1920px" entire page, window width 1920px'); + console.log(' "800px*600px" window, clipped to 800x600'); + phantom.exit(1); +} else { + address = system.args[1]; + output = system.args[2]; + page.viewportSize = { width: 600, height: 600 }; + if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") { + size = system.args[3].split('*'); + page.paperSize = size.length === 2 ? { width: size[0], height: size[1], margin: '0px' } + : { format: system.args[3], orientation: 'portrait', margin: '1.5cm' }; + } else if (system.args.length > 3 && system.args[3].substr(-2) === "px") { + size = system.args[3].split('*'); + if (size.length === 2) { + pageWidth = parseInt(size[0], 10); + pageHeight = parseInt(size[1], 10); + page.viewportSize = { width: pageWidth, height: pageHeight }; + page.clipRect = { top: 0, left: 0, width: pageWidth, height: pageHeight }; + } else { + console.log("size:", system.args[3]); + pageWidth = parseInt(system.args[3], 10); + pageHeight = parseInt(pageWidth * 3/4, 10); // it's as good an assumption as any + console.log ("pageHeight:",pageHeight); + page.viewportSize = { width: pageWidth, height: pageHeight }; + } + } + if (system.args.length > 4) { + page.zoomFactor = system.args[4]; + } + page.open(address, function (status) { + if (status !== 'success') { + console.log('Unable to load the address!'); + phantom.exit(1); + } else { + window.setTimeout(function () { + page.render(output); + phantom.exit(); + }, 200); + } + }); +} \ No newline at end of file diff --git a/app/pdf/renderers.py b/app/pdf/renderers.py new file mode 100644 index 00000000..11608254 --- /dev/null +++ b/app/pdf/renderers.py @@ -0,0 +1,12 @@ +from django.template import loader + +from .generators import PDFGenerator + + +def render_pdf(filename, request, template_name, context=None, using=None, options={}): + + # Render to file. + content = loader.render_to_string(template_name, context, request, using=using) + pdf = PDFGenerator(content, **options) + + return pdf.get_http_response(filename) diff --git a/app/pdf/settings.py b/app/pdf/settings.py new file mode 100644 index 00000000..273e30d1 --- /dev/null +++ b/app/pdf/settings.py @@ -0,0 +1,74 @@ +""" +Settings for PDF Generator are all namespaced in the PDF_GENERATOR setting. +For example your project's `settings.py` file might look like this: + +PDF_GENERATOR = { + 'UPLOAD_TO': 'pdfs', +} + +This module provides the `pdf_setting` object, that is used to access +PDF settings, checking for user settings first, then falling +back to the defaults. +""" +from __future__ import unicode_literals +import os +from django.conf import settings +from django.test.signals import setting_changed + + +PDF_GENERATOR_DIR = os.path.dirname(os.path.abspath(__file__)) + +DEFAULTS = { + 'UPLOAD_TO': 'pdfs', + 'PHANTOMJS_BIN_PATH': 'phantomjs', + 'DEFAULT_RASTERIZE_SCRIPT': os.path.join(PDF_GENERATOR_DIR, 'rasterize.js'), + 'DEFAULT_TEMP_DIR': os.path.join(PDF_GENERATOR_DIR, 'temp'), + 'TEMPLATES_DIR': os.path.join(PDF_GENERATOR_DIR, 'templates/pdf_generator') +} + + +class PDFSettings(object): + """ + A settings object, that allows PDF settings to be accessed as properties. + For example: + from pdf_generator.settings import api_settings + print(pdf_settings.UPLOAD_TO) + """ + def __init__(self, user_settings=None, defaults=None): + if user_settings: + self._user_settings = user_settings + self.defaults = defaults or DEFAULTS + + @property + def user_settings(self): + if not hasattr(self, '_user_settings'): + self._user_settings = getattr(settings, 'PDF_GENERATOR', {}) + return self._user_settings + + def __getattr__(self, attr): + if attr not in self.defaults: + raise AttributeError("Invalid PDF Generator setting: '%s'" % attr) + + try: + # Check if present in user settings + val = self.user_settings[attr] + except KeyError: + # Fall back to defaults + val = self.defaults[attr] + + # Cache the result + setattr(self, attr, val) + return val + + +pdf_settings = PDFSettings(None, DEFAULTS) + + +def reload_pdf_settings(*args, **kwargs): + global pdf_settings + setting, value = kwargs['setting'], kwargs['value'] + if setting == 'PDF_GENERATOR': + pdf_settings = PDFSettings(value, DEFAULTS) + + +setting_changed.connect(reload_pdf_settings) diff --git a/app/pdf/temp/empty.txt b/app/pdf/temp/empty.txt new file mode 100755 index 00000000..e69de29b diff --git a/app/pdf/tests.py b/app/pdf/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/app/pdf/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/projects/admin.py b/app/projects/admin.py index cea9fcf3..650e34ac 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -16,6 +16,8 @@ from projects.models import ChallengeTaskSubmission from projects.models import ChallengeTaskSubmissionDownload from projects.models import Bucket +from projects.models import InstitutionalOfficial +from projects.models import InstitutionalMember class GroupAdmin(admin.ModelAdmin): @@ -58,6 +60,14 @@ class InstitutionAdmin(admin.ModelAdmin): list_display = ('name', 'logo_path', 'created', 'modified', ) readonly_fields = ('created', 'modified', ) +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', ) @@ -95,6 +105,8 @@ class ChallengeTaskSubmissionDownloadAdmin(admin.ModelAdmin): admin.site.register(Team, TeamAdmin) 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) diff --git a/app/projects/api.py b/app/projects/api.py index f3f3dd11..63948a7b 100644 --- a/app/projects/api.py +++ b/app/projects/api.py @@ -10,11 +10,14 @@ from django.contrib.auth.models import User from django.core import exceptions from django.core.exceptions import ObjectDoesNotExist +from django.template.exceptions import TemplateDoesNotExist from django.http import JsonResponse from django.http import HttpResponse from django.http import QueryDict from django.shortcuts import get_object_or_404 from django.shortcuts import redirect +from django.template import loader +from django.core.files.base import ContentFile from dal import autocomplete from contact.views import email_send @@ -25,6 +28,9 @@ from projects.templatetags import projects_extras from projects.utils import notify_supervisors_of_task_submission from projects.utils import notify_task_submitters +from pdf.renderers import render_pdf +from projects.panels import DataProjectSignupPanel +from projects.panels import SIGNUP_STEP_CURRENT_STATUS from projects.models import AgreementForm from projects.models import ChallengeTask @@ -37,7 +43,8 @@ from projects.models import Team from projects.models import SIGNED_FORM_REJECTED from projects.models import HostedFileSet -from projects import models +from projects.models import InstitutionalOfficial +from projects.models import InstitutionalMember logger = logging.getLogger(__name__) @@ -618,7 +625,6 @@ def save_signed_agreement_form(request): An HTTP POST endpoint that takes the contents of an agreement form that a user has submitted and saves it to the database. """ - agreement_form_id = request.POST['agreement_form_id'] project_key = request.POST['project_key'] agreement_text = request.POST['agreement_text'] @@ -657,9 +663,62 @@ def save_signed_agreement_form(request): ] # Save form fields - fields = {k:v for k, v in request.POST.items() if k.lower() not in exclusions} + fields = {} + for key, value in dict(request.POST.lists()).items(): + + # Check exclusions + if key.lower() in exclusions: + continue + + # Retain lists + if len(value) > 1: + fields[key] = value + else: + fields[key] = next(iter(value), None) + + # Save fields signed_agreement_form.fields = fields + # Check for a template + if agreement_form.template: + + # Convert hypens to underscore in context + safe_fields = {k.replace("-", "_"):v for k,v in fields.items()} + + # Render content of the agreement form + signed_agreement_form_content = loader.render_to_string( + template_name=agreement_form.form_file_path, + context=safe_fields, + ) + + try: + # Attempt to load PDF template + loader.get_template(agreement_form.template) + + # Set the filename + filename = f"{agreement_form.short_name}-{request.user.email}-{datetime.now().isoformat()}.pdf" + + # Set context + context = { + "content": signed_agreement_form_content + } + + # Submit consent PDF + logger.debug(f"Rendering agreement form with template: {agreement_form.template}") + response = render_pdf(filename, request, agreement_form.template, context=context, options={}) + signed_agreement_form.document = ContentFile(response.content, name=filename) + + except TemplateDoesNotExist: + logger.exception(f"Agreement form template not found: {agreement_form.template}", extra={ + "agreement_form": agreement_form, + "signed_agreement_form": signed_agreement_form, + }) + except Exception as e: + logger.exception(f"Document could not be created: {e}", extra={ + "agreement_form": agreement_form, + "signed_agreement_form": signed_agreement_form, + }) + # Save signed_agreement_form.save() @@ -670,47 +729,6 @@ def save_signed_agreement_form(request): extra={"form": agreement_form.short_name, "fields": request.POST,} ) - # TODO: The following behavior should be removed as soon as it is possible - - # Create a row for storing fields - model_name = f"{agreement_form.short_name.upper()}SignedAgreementFormFields" - if not hasattr(models, model_name): - logger.error( - f"HYP/Projects/API: Cannot persist fields for signed agreement " - f"form: {agreement_form.short_name.upper()}" - ) - - else: - try: - # Create the object - model_class = getattr(models, model_name) - signed_agreement_form_fields = model_class( - signed_agreement_form=signed_agreement_form - ) - - # Save form fields - for key, data in request.POST.items(): - - # Replace dashes with underscore - _field = key.replace("-", "_") - - # Check if field on model - if hasattr(signed_agreement_form_fields, _field): - - # Set it - setattr(signed_agreement_form_fields, _field, data) - - else: - logger.warning(f"HYP/Projects/API: '{model_name}' unhandled field: '{_field}'") - - # Save - signed_agreement_form_fields.save() - except Exception as e: - logger.exception( - f"HYP/Projects/API: Fields error: {e}", - exc_info=True, - extra={"form": agreement_form.short_name, "model": model_name}) - return HttpResponse(status=200) @user_auth_and_jwt @@ -761,45 +779,83 @@ def submit_user_permission_request(request): project_key = request.POST.get('project_key', None) project = DataProject.objects.get(project_key=project_key) except ObjectDoesNotExist: - return HttpResponse(404) + # Create the response. + response = HttpResponse(status=404) + + # Setup the script run. + response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format( + "danger", "The requested project could not be found", "warning-sign" + ) + + return response if project.has_teams or not project.requires_authorization: - return HttpResponse(400) + + # Create the response. + response = HttpResponse(status=400) + + # Setup the script run. + response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format( + "danger", "The action could not be completed", "warning-sign" + ) + + return response # Create a new participant record if one does not exist already. - participant = Participant.objects.get_or_create( + Participant.objects.get_or_create( user=request.user, project=project ) # Check if there are administrators to notify. - if project.project_supervisors is None or project.project_supervisors == "": - return HttpResponse(200) + if project.project_supervisors: - # Convert the comma separated string of emails into a list. - supervisor_emails = project.project_supervisors.split(",") + # Convert the comma separated string of emails into a list. + supervisor_emails = project.project_supervisors.split(",") - subject = "DBMI Data Portal - Access requested to dataset" + subject = "DBMI Data Portal - Access requested to dataset" + + email_context = { + 'subject': subject, + 'project': project, + 'user_email': request.user.email, + 'site_url': settings.SITE_URL + } + + try: + email_success = email_send( + subject=subject, + recipients=supervisor_emails, + email_template='email_access_request_notification', + extra=email_context + ) + except Exception as e: + logger.exception(e) - email_context = { - 'subject': subject, - 'project': project, - 'user_email': request.user.email, - 'site_url': settings.SITE_URL + else: + logger.warning(f"Project '{project}' has not supervisors to alert on access requests") + + # Set context + context = { + "panel": { + "additional_context": { + "requested_access": True, + } + } } - try: - email_success = email_send( - subject=subject, - recipients=supervisor_emails, - email_template='email_access_request_notification', - extra=email_context - ) - except Exception as e: - logger.exception(e) + # Render the panel and return it + content = loader.render_to_string("projects/signup/request-access.html", context=context) - return HttpResponse(200) + # Create the response. + response = HttpResponse(content, status=201) + # Setup the script run. + response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format( + "success", "Your request for access has been submitted", "thumbs-up" + ) + + return response @user_auth_and_jwt @@ -842,3 +898,80 @@ def upload_signed_agreement_form(request): signed_agreement_form.save() return HttpResponse(status=200) + +@user_auth_and_jwt +def update_institutional_members(request): + """ + A view for updating the list of members that an institutional official + providers signing authority for. + """ + # Get the signed agreement form + signed_agreement_form_id = request.POST.get("signed-agreement-form") + official = None + try: + # Fetch the official + official = InstitutionalOfficial.objects.get(user=request.user, signed_agreement_form__id=signed_agreement_form_id) + except ObjectDoesNotExist: + logger.debug(f"No InstitutionalOfficial found for '{request.user.email}'") + + # Create the response. + response = HttpResponse(status=404) + + # Setup the script run. + response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format( + "danger", "An error occurred during the update. Please try again or contact support", "exclamation-sign" + ) + + return response + + # Get the list + member_emails = [m.lower() for m in request.POST.getlist("member-emails", [])] + + # Check for duplicates + if len(set(member_emails)) < len(member_emails): + + # Create the response. + response = HttpResponse(status=400) + + # Setup the script run. + response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format( + "warning", "Duplicate email addresses are not allowed", "exclamation-sign" + ) + + return response + + # Iterate existing members + for member in InstitutionalMember.objects.filter(official=official): + + # Check if in list + if member.email.lower() in member_emails: + logger.debug(f"Update institutional members: Member '{member.email}' already exists") + + # Remove email from list + member_emails.remove(member.email.lower()) + + elif member.email.lower() not in member_emails: + logger.debug(f"Update institutional members: Member '{member.email}' will be deleted") + + # Delete them + member.delete() + + # Create members from remaining email addresses + for member_email in member_emails: + logger.debug(f"Update institutional members: Member '{member_email}' will be created") + + # Create them + InstitutionalMember.objects.create( + official=official, + email=member_email, + ) + + # Create the response. + response = HttpResponse(status=201) + + # Setup the script run. + response['X-IC-Script'] = "notify('{}', '{}', 'glyphicon glyphicon-{}');".format( + "success", "Institutional members updated", "thumbs-up" + ) + + return response diff --git a/app/projects/management/commands/signed_agreement_form_export.py b/app/projects/management/commands/signed_agreement_form_export.py index 40d4e046..416f2e34 100644 --- a/app/projects/management/commands/signed_agreement_form_export.py +++ b/app/projects/management/commands/signed_agreement_form_export.py @@ -67,7 +67,7 @@ def handle(self, *args, **options): try: # Set the key - key = os.path.join(settings.AWS_LOCATION, signed_agreement_form.upload.name) + key = os.path.join(settings.PROJECTS_UPLOADS_PREFIX, signed_agreement_form.upload.name) # Download the file s3.download_file(settings.AWS_STORAGE_BUCKET_NAME, key, os.path.join(archive_root, signed_agreement_form.upload.name)) diff --git a/app/projects/migrations/0096_participant_created_participant_modified.py b/app/projects/migrations/0096_participant_created_participant_modified.py index be54c4e6..5379429a 100644 --- a/app/projects/migrations/0096_participant_created_participant_modified.py +++ b/app/projects/migrations/0096_participant_created_participant_modified.py @@ -2,20 +2,6 @@ from django.db import migrations, models -from projects.models import AgreementForm, SignedAgreementForm, Participant - - -def migrate_agreement_form_model(apps, schema_editor): - """ - Sets the initial value of the created field to the existing value of the - modified field. - """ - for agreement_form in AgreementForm.objects.all(): - - # Set the dates - agreement_form.modified = agreement_form.created - agreement_form.save() - class Migration(migrations.Migration): @@ -109,7 +95,7 @@ class Migration(migrations.Migration): name='modified', field=models.DateTimeField(default="2023-01-01T00:00:00.000Z"), ), - migrations.RunPython(migrate_agreement_form_model), + migrations.RunSQL("UPDATE projects_agreementform SET modified = created"), migrations.AlterField( model_name='agreementform', name='modified', diff --git a/app/projects/migrations/0105_agreementform_template.py b/app/projects/migrations/0105_agreementform_template.py new file mode 100644 index 00000000..a9a333c2 --- /dev/null +++ b/app/projects/migrations/0105_agreementform_template.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.14 on 2024-08-08 14:32 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import projects.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('projects', '0104_group_parent'), + ] + + operations = [ + migrations.AddField( + model_name='agreementform', + name='template', + field=models.CharField(blank=True, max_length=300, null=True), + ), + migrations.AddField( + model_name='signedagreementform', + name='document', + field=models.FileField(blank=True, null=True, upload_to=projects.models.signed_agreement_form_document_path), + ), + migrations.CreateModel( + name='InstitutionalOfficial', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('institution', models.TextField()), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('signed_agreement_form', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='projects.signedagreementform')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='projects.dataproject')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='InstitutionalMember', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.TextField()), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('official', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='projects.institutionalofficial')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index 34e349c9..cd3271e7 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -259,6 +259,7 @@ class AgreementForm(models.Model): " 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." ) + template = models.CharField(max_length=300, blank=True, null=True) # Meta created = models.DateTimeField(auto_now_add=True) @@ -277,6 +278,16 @@ def clean(self): if self.type == AGREEMENT_FORM_TYPE_FILE and not self.content and not self.form_file_path: raise ValidationError("If the form type is file, the content field should be populated with the agreement form's HTML.") + def agreement_form_4ce_dua_status_change(self, signed_agreement_form): + """ + This method handles status changes on signed versions of this agreement + form. + + :param signed_agreement_form: The signed agreement form + :type signed_agreement_form: SignedAgreementForm + """ + logger.debug(f"[4CE-DUA]: Handling status update for SignedAgreementForm/{signed_agreement_form.id}") + pass class DataProject(models.Model): """ @@ -379,6 +390,34 @@ def clean(self): raise ValidationError('A Project cannot share teams if it is using shared teams from another project') + +class InstitutionalOfficial(models.Model): + """ + This represents a signer that represents an institution. + """ + user = models.ForeignKey(User, on_delete=models.PROTECT) + 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) + + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + def validate_pdf_file(value): """ Ensures only a file with a content type of PDF can be persisted @@ -388,8 +427,12 @@ def validate_pdf_file(value): def signed_agreement_form_path(instance, filename): - # file will be uploaded to AWS_LOCATION/__ - return f'{instance.user.email}_{datetime.now().isoformat()}_{filename}' + # file will be uploaded to PROJECTS_UPLOADS_PREFIX/__ + return f'{settings.PROJECTS_UPLOADS_PREFIX}/{instance.user.email}_{datetime.now().isoformat()}_{filename}' + +def signed_agreement_form_document_path(instance, filename): + # file will be uploaded to PROJECTS_DOCUMENTS_PREFIX/ + return f'{settings.PROJECTS_DOCUMENTS_PREFIX}/{filename}' class SignedAgreementForm(models.Model): @@ -405,6 +448,7 @@ class SignedAgreementForm(models.Model): status = models.CharField(max_length=1, null=False, blank=False, default='P', choices=SIGNED_FORM_STATUSES) upload = models.FileField(null=True, blank=True, validators=[validate_pdf_file], upload_to=signed_agreement_form_path) fields = JSONField(null=True, blank=True) + document = models.FileField(null=True, blank=True, upload_to=signed_agreement_form_document_path) # Meta created = models.DateTimeField(auto_now_add=True) @@ -413,6 +457,7 @@ class Meta: verbose_name = 'Signed Agreement Form' verbose_name_plural = 'Signed Agreement Forms' + class Team(models.Model): """ This model describes a team of participants that are competing in a data challenge. diff --git a/app/projects/panels.py b/app/projects/panels.py index c3e4b000..96dc029e 100644 --- a/app/projects/panels.py +++ b/app/projects/panels.py @@ -60,3 +60,24 @@ class DataProjectSharedTeamsPanel(DataProjectPanel): def __init__(self, title, bootstrap_color, template, status, additional_context=None): super().__init__(title, bootstrap_color, template, additional_context) self.status = status + + +class DataProjectInstitutionalOfficialPanel(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, 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 diff --git a/app/projects/signals.py b/app/projects/signals.py index 0e8f0972..afc5247d 100644 --- a/app/projects/signals.py +++ b/app/projects/signals.py @@ -1,11 +1,15 @@ from django.db.models.signals import post_save +from django.db.models.signals import pre_save from django.dispatch import receiver from django.db import transaction from projects.models import DataProject from projects.models import Team from projects.models import Participant +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__) @@ -116,3 +120,39 @@ def sync_teams(project): # Set as deactivated shared_team.status = TEAM_DEACTIVATED shared_team.save() + +@receiver(pre_save, sender=SignedAgreementForm) +def signed_agreement_form_pre_save_handler(sender, **kwargs): + """ + This handler manages any routines that should be executed during the + status changes of a signed agreement form. + + :param sender: The sender of the signal + :type sender: object + """ + instance = kwargs.get("instance") + logger.debug(f"Pre-save: {instance}") + + # Check for specific types of forms that require additional handling + if instance.status == "A" and instance.agreement_form.short_name == "4ce-dua" \ + and instance.fields.get("registrant-is") == "official": + logger.debug(f"Pre-save institutional official: {instance}") + + # Create official and member objects + official = InstitutionalOfficial.objects.create( + user=instance.user, + institution=instance.fields["institute-name"], + project=instance.project, + signed_agreement_form=instance, + ) + official.save() + + # Iterate members + for email in instance.fields["member-emails"]: + + # Create member + member = InstitutionalMember.objects.create( + official=official, + email=email, + ) + member.save() diff --git a/app/projects/urls.py b/app/projects/urls.py index 878bfd62..48690761 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -19,6 +19,7 @@ from projects.api import submit_user_permission_request from projects.api import upload_signed_agreement_form from projects.api import HostedFileSetAutocomplete +from projects.api import update_institutional_members app_name = ProjectsConfig.name @@ -39,5 +40,6 @@ re_path(r'^download_dataset/$', download_dataset, name='download_dataset'), re_path(r'^upload_challengetasksubmission_file/$', upload_challengetasksubmission_file, name="upload_challengetasksubmission_file"), re_path(r'^delete_challengetasksubmission/$', delete_challengetasksubmission, name='delete_challengetasksubmission'), + re_path(r'^update_institutional_members/$', update_institutional_members, name="update_institutional_members"), re_path(r'^(?P[^/]+)/$', DataProjectView.as_view(), name="view-project"), ] diff --git a/app/projects/views.py b/app/projects/views.py index 0d0a708b..7d74c4c2 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -28,6 +28,8 @@ from projects.models import Participant from projects.models import SignedAgreementForm from projects.models import Group +from projects.models import InstitutionalOfficial +from projects.models import InstitutionalMember from projects.panels import SIGNUP_STEP_COMPLETED_STATUS from projects.panels import SIGNUP_STEP_CURRENT_STATUS from projects.panels import SIGNUP_STEP_FUTURE_STATUS @@ -36,6 +38,8 @@ from projects.panels import DataProjectSignupPanel from projects.panels import DataProjectActionablePanel from projects.panels import DataProjectSharedTeamsPanel +from projects.panels import DataProjectInstitutionalOfficialPanel +from projects.panels import DataProjectInstitutionalMemberPanel # Get an instance of a logger logger = logging.getLogger(__name__) @@ -328,6 +332,9 @@ def get_signup_context(self, context): else: + # Add panel for institutional members + self.panel_institutional_member(context) + # Agreement forms step (if needed). self.setup_panel_sign_agreement_forms(context) @@ -367,6 +374,9 @@ def get_participate_context(self, context): # Add a panel for a solution submission form (if needed). self.panel_submit_task_solutions(context) + # Add panel for institutional officials + self.panel_institutional_official(context) + return context def get_manager_context(self, context): @@ -780,6 +790,73 @@ def panel_available_downloads(self, context): context['actionable_panels'].append(panel) + def panel_institutional_official(self, context): + """ + Builds the context needed for the institutional official to manage + the members that they provide signing authority for. + """ + # Setup context + additional_context = {} + + try: + # Check for an institutional official linked to this user + official = InstitutionalOfficial.objects.get(user=self.participant.user) + + # Add to context + additional_context["official"] = official + except ObjectDoesNotExist: + pass + + try: + # Check for an institutional member linked to this user + member = InstitutionalMember.objects.get(user=self.participant.user) + + # Add to context + additional_context["member"] = member + except ObjectDoesNotExist: + pass + + if additional_context: + panel = DataProjectInstitutionalOfficialPanel( + title='Institutional Official', + bootstrap_color='default', + template='projects/participate/institutional-official.html', + additional_context=additional_context + ) + + context['actionable_panels'].append(panel) + + def panel_institutional_member(self, context): + """ + Builds the context needed for the institutional official to manage + the members that they provide signing authority for. + """ + try: + # Check for an institutional member linked to this user + member = InstitutionalMember.objects.get(official__project=self.project, email=self.request.user.email) + + # Add to context + additional_context = { + "member": member, + } + + # This step is never completed. + step_status = self.get_step_status('institutional_member', False) + + # Add the panel + panel = DataProjectInstitutionalMemberPanel( + title='Institutional Member', + bootstrap_color='default', + template='projects/participate/institutional-member.html', + status=step_status, + additional_context=additional_context + ) + + context['setup_panels'].append(panel) + + except ObjectDoesNotExist: + pass + def panel_submit_task_solutions(self, context): """ Builds the context needed for a user to submit solutions for a data @@ -854,6 +931,21 @@ def is_user_granted_access(self, context): considered having been granted access to participate in this DataProject. Returns a boolean. """ + # Check for institutional access + try: + member = InstitutionalMember.objects.get(official__project=self.project, email=self.request.user.email) + logger.debug(f"Institutional member found under official: {member.official.user.email}") + + # Check if official has access + official_participant = Participant.objects.get(project=self.project, user=member.official.user) + if official_participant.permission == "VIEW": + logger.debug(f"Institutional official has access, granting access to member") + return True + else: + logger.debug(f"Institutional official does not have access") + + except ObjectDoesNotExist: + logger.debug(f"No institutional member found") # Does user not have VIEW permissions? if not context['has_view_permission']: diff --git a/app/static/agreementforms/4ce-dua.html b/app/static/agreementforms/4ce-dua.html new file mode 100644 index 00000000..d174c14e --- /dev/null +++ b/app/static/agreementforms/4ce-dua.html @@ -0,0 +1,278 @@ +
+
+

Data User and Access Agreement

+ +

By accessing the 4CE Data Repository, as hosted by President and Fellows of Harvard College on behalf of Harvard Medical School (“Harvard”), you (the “User”) agree to the following terms of this agreement (“Agreement”):

+ +

I. Acceptance of this Agreement

+ +

By downloading or otherwise accessing the data available from the 4CE Data Repository (“Data”), User (i) accepts and agrees to the terms of this Agreement, and (ii) represents that User is duly authorized to enter into this Agreement on behalf of their institution.

+ +

II. Attribution

+ +

The User shall acknowledge the source of the Data in any written, visual, or oral public disclosures concerning the User’s research using the data, as appropriate in accordance with scholarly standards.

+ +

III. Use of the Data

+ +

Subject to User’s compliance with all of the terms and conditions of this Agreement, use of the Data is limited to the User directly querying different LLMs (e.g., ChatGPT, GPT-4, KARL, LAMP, PaLM) to assess their feasibility and potential utility in analyzing clinical notes.

+ +
    +
  1. Representations and Covenants +
      +
    1. User represents and covenants that: +
        +
      1. User is not bound by any pre-existing legal obligations or other applicable laws that prevent User from downloading or using the Data;
      2. +
      3. User will access and use the Data in compliance with all applicable laws, rules, and regulations, as well as all professional and ethical standards applicable to scientific research and User’s research project, including without limitation, all applicable requirements pertaining to human subjects research and animal research;
      4. +
      5. User agrees to establish appropriate administrative, technical, and physical safeguards to prevent unauthorized use of or access to the Data and comply with any other requirements relating to safeguarding of the Data that the 4CE Data Repository may require from time to time;
      6. +
      7. User will only use and download that portion of the Data that is necessary for use in User’s research projects; downloading of the entire 4CE Data Repository or portions beyond what is needed for User’s research projects is strictly prohibited. User will only share the Data will those authorized employees, fellows, students and agents who have a need to access such Data for purposes of working on User’s research projects, and whose obligations of use are consistent with the terms of this Agreement.
      8. +
      9. User will retain control over the Data it accesses and downloads, and shall not disclose, release, sell, rent, lease, loan or otherwise grant access to the Data to any third party, except other users expressly authorized by Harvard to access and use the Data.
      10. +
      11. User will use the Data only for scientific research purposes and shall not use the Data for any commercial activities;
      12. +
      13. User will regularly check the 4CE Data Repository for any updates to Data pertinent to User’s research projects, and will only use the updated version of such Data, and will promptly destroy all outdated Data in User’s possession; and
      14. +
      15. User shall not use the Data except as authorized under this Agreement.
      16. +
      +
    2. +
    3. User further covenants that User will not:
    4. +
        +
      1. obtain information from the Data that results in User or any third party(ies) directly or indirectly identifying any research subjects with the aid of other information acquired elsewhere;
      2. +
      3. produce connections or links among the information included in Harvard’s datasets (including information in the Data), or between the information included in Harvard’s datasets (including information in the Data) and other third-party information that could be used to identify any individuals or organizations, not limited to research subjects;
      4. +
      5. extract information from the Data that could aid User in gaining knowledge about or obtaining any means of contacting any subjects already known to User; or
      6. +
      7. use the Data, either alone or in concert with any other information, to make any effort to identify or contact individuals who are or may be the sources of Data.
      8. +
      +
    +
  2. +
+ +

IV. Disclaimer

+ +

THE DATA IS PROVIDED “AS IS” AND “AS AVAILABLE” AND WITH ALL FAULTS AND DEFECTS. HARVARD MAKES NO, AND HEREBY DISCLAIMS ALL, REPRESENTATIONS AND WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, OR THAT THE USE OF THE DATA WILL NOT INFRINGE OR VIOLATE ANY PATENT, COPYRIGHT, TRADEMARK OR OTHER THIRD-PARTY RIGHT, AND ANY WARRANTIES IMPLIED BY ANY COURSE OF PERFORMANCE OR DEALING.

+ +

WITHOUT LIMITING THE FOREGOING, HARVARD MAKES NO WARRANTY AS TO THEACCURACY, COMPLETENESS, RELIABILITY, ORIGINALITY, AVAILABILITY, OR SECURITY OF THE DATA, THAT THE DATA WILL MEET USER’S REQUIREMENTS OR PROVIDE PARTICULAR BENEFITS, OR THAT THE DATA AND ANY RELATED FILES WILL BE FREE FROM DEFECTS, ERRORS, VIRUSES OR OTHER HARMFUL COMPONENTS. USER’S USE OF THE DATA IS SOLELY AT USER’S OWN RISK.

+ +

V. Limitation of Liability

+ +

IN NO EVENT SHALL HARVARD BE LIABLE UNDER CONTRACT, TORT, STRICT LIABILITY, NEGLIGENCE OR ANY OTHER LEGAL THEORY WITH RESPECT TO THE DATA (I) FOR ANY DIRECT DAMAGES, OR (II) FOR ANY LOST PROFITS OR SPECIAL, INDIRECT, INCIDENTAL, PUNITIVE, OR CONSEQUENTIAL DAMAGES OF ANY KIND WHATSOEVER.

+ +

VI. Liability

+ +

Except to the extent prohibited by law, the User assumes all liability for damages which may arise from its use, storage, download, disclosure, or disposal of the Data. Harvard will not be liable to the User or any other person or entity for any loss, claim, liability, or demand made by the User, or made against the User by any other party, or otherwise relating to or arising in connection with the use of the data by the User, except to the extent permitted by law when caused by the gross negligence or willful misconduct of Harvard. No indemnification for any loss, claim, damage, or liability is intended or provided by either party under this Agreement.

+ +

VII. Governing Law; Venue

+ +

This Agreement shall be governed by and interpreted in accordance with the laws of the Commonwealth of Massachusetts (excluding the conflict of laws and rules thereof). All disputes under this Agreement will be resolved in the applicable state or federal courts of Massachusetts. User consents to the jurisdiction of such courts and waives any jurisdictional or venue defenses otherwise available.

+ +

VIII. Integration and Severability

+ +

This Agreement represents the entire agreement between User and Harvard with respect to the downloading and use of the Data, and supersedes all prior or contemporaneous communications and proposals (whether oral, written or electronic) between User and Harvard with respect to downloading or using the Data. If any provision of this Agreement is found to be unenforceable or invalid, that provision will be limited or eliminated to the minimum extent necessary so that the Agreement will otherwise remain in full force and effect and enforceable.

+ +

IX. Reporting Requirement

+ +

Should the User (i) inadvertently receives identifiable information or otherwise identifies a subject, or (ii) becomes aware of any use or disclosure of the Data not provided for or permitted by this Agreement, the User shall immediately notify Harvard via email to XXXXXX@hms.harvard.edu, and follow Harvard’s reasonable written instructions, which may include return or destruction of Data.

+ +

X. Ownership

+ +

Harvard shall retain ownership of any rights it may have in the Data, and User does not obtain any rights in the Data other than as set forth in this Agreement.

+ +

XI. Use of Harvard Name

+ +

Except as expressly provided in this Agreement, User shall not use or register Harvard’s name (alone or as part of another name) or any logos, seals, insignia or other words, names, symbols or devices that identify Harvard, including any Harvard school, unit, division or affiliate (“Harvard Names”) for any purpose in connection with this Agreement except with the prior written approval of, and in accordance with restrictions required by, Harvard. The foregoing notwithstanding, User may respond to legitimate business inquiries with factual information regarding the existence and purpose of the relationship that is the subject of this Agreement, without written permission from Harvard.  Without limiting the foregoing, User shall cease all use of Harvard Names permitted under this Agreement on the termination or expiration of this Agreement except as otherwise approved by Harvard.

+ +

XII. Term and Termination

+ +

User may use the Data for the duration of their project as allowed under Article III of this Agreement. Either party may terminate this agreement for any reason at any time, however, the User's obligations shall remain in effect even after termination. Upon termination, User shall immediately cease use of the Data and destroy Data in its possession and, if required by Harvard, certify to Harvard as to its destruction.

+ +

Harvard may limit, suspend or terminate User’s access to Data at any time if Harvard believes User has violated the terms of this Agreement or that continued use of or access to the Data by User otherwise presents acted negligently with respect to the Data.

+ +

XIII. Miscellaneous

+ +

Neither party may assign, transfer or delegate any of its rights and obligations hereunder without consent of the other party. No agency, partnership, joint venture, or employment relationship is created as a result of the Agreement. Neither Party shall have authority to make any statements, representations or commitments of any kind on behalf of the other Party, or to take any action which shall be binding on the other Party.

+ +

This Agreement represents the entire agreement between the parties with respect to the subject matter hereof, any any prior or contemporaneous representations or understandings, either oral or written, are hereby superseded. No modification or waiver of any provision of this Agreement shall be valid unless in writing and executed by duly-authorized representatives of both Parties. A failure by one of the Parties to this Agreement to assert its rights hereunder shall not be deemed a waiver of such rights. No such failure or waiver in writing by any one of the Parties hereto with respect to any rights shall extend to or affect any subsequent breach or impair any right consequent thereon. If any provision of this Agreement is or becomes invalid or is ruled invalid by any court of competent jurisdiction or is deemed unenforceable, it is the intention of the parties that the remainder of this Agreement shall not be affected.

+
+
+
+
+
+

I am a (select one):

+ +
+ + + +
+ +
+ +
+ +
+

Institutional Member

+ +
+ + +
+
+ +
+

Institutional Official

+ +

Institution Details

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Member Details

+
+ {% for email in member_emails %} +
+ +
+ {% empty %} +
+
+ +
+ +
+
+
+ {% endfor %} +
+
+ +

Contact Details

+
+ + +
+
+ + +
+
+ + +
+

Signature

+
+ + +
+
+ + +
+
+
+
+ diff --git a/app/static/agreementforms/hms-dbmi-portal-pdf-template.html b/app/static/agreementforms/hms-dbmi-portal-pdf-template.html new file mode 100644 index 00000000..4b1f78c8 --- /dev/null +++ b/app/static/agreementforms/hms-dbmi-portal-pdf-template.html @@ -0,0 +1,82 @@ + + + + + Consent Approval Process + + + + +
+ +
+ {% autoescape off %}{{ content }}{% endautoescape %} +
+
+ + diff --git a/app/static/agreementforms/institutional-official.html b/app/static/agreementforms/institutional-official.html new file mode 100644 index 00000000..c308ba72 --- /dev/null +++ b/app/static/agreementforms/institutional-official.html @@ -0,0 +1,149 @@ +
+

I am a (select one):

+ +
+ + + +
+ + + + + + +
+ + diff --git a/app/templates/projects/participate/institutional-member.html b/app/templates/projects/participate/institutional-member.html new file mode 100644 index 00000000..869ec082 --- /dev/null +++ b/app/templates/projects/participate/institutional-member.html @@ -0,0 +1,3 @@ +

You've been specified as a member of the institution {{ panel.additional_context.member.official.institution }} by an institutional official with the email {{ panel.additional_context.member.official.user.email }}.

+

The institutional official has already signed the data use agreement, which is pending review and acceptance/rejection by administrators.

+

Once your institutional official's agreement has been approved, return to this page and your access will be granted automatically.

diff --git a/app/templates/projects/participate/institutional-official.html b/app/templates/projects/participate/institutional-official.html new file mode 100644 index 00000000..72f18690 --- /dev/null +++ b/app/templates/projects/participate/institutional-official.html @@ -0,0 +1,83 @@ +{% if panel.additional_context.official %} +

You're the official for the institution '{{ panel.additional_context.official.institution }}'.

+

Use the following form to manage members of your institution that you represent with your signing authority:

+

Member Details

+
+ +
+ {% csrf_token %} + {% for member in panel.additional_context.official.institutionalmember_set.all %} +
+
+ +
+ {% if forloop.counter0 == 0 %} + + {% else %} + + {% endif %} +
+
+
+ {% endfor %} +
+
+
+ +
+
+
+{% elif panel.additional_context.member %} +

You're a member of the the institution '{{ panel.additional_context.member.official.institution }}'. The official that represents you and your institution is {{ panel.additional_context.member.official.user.email }}.

+{% endif %} + + diff --git a/app/templates/projects/participate/view-signed-agreement-form.html b/app/templates/projects/participate/view-signed-agreement-form.html index 8c806340..06c7ab3d 100644 --- a/app/templates/projects/participate/view-signed-agreement-form.html +++ b/app/templates/projects/participate/view-signed-agreement-form.html @@ -40,6 +40,10 @@

Signed Form Content {% endif %} + {% elif signed_form.document %} +
+ +
{% else %} {{ signed_form.agreement_text|linebreaks }} {% endif %} diff --git a/app/templates/projects/project.html b/app/templates/projects/project.html index d59674eb..5a192145 100644 --- a/app/templates/projects/project.html +++ b/app/templates/projects/project.html @@ -242,7 +242,7 @@

"); + $(submit_button).html("Form Saved  ").prop("disabled", "disabled").toggleClass("btn-primary btn-success"); // Refresh the page so the user does not see this button again setTimeout(function(){ @@ -297,7 +297,7 @@ "); + $(submit_button).html("Form Saved  ").prop("disabled", "disabled").toggleClass("btn-primary btn-success"); // Refresh the page so the user does not see this button again setTimeout(function(){ diff --git a/app/templates/projects/signup/request-access.html b/app/templates/projects/signup/request-access.html index 46e81010..3eb23933 100644 --- a/app/templates/projects/signup/request-access.html +++ b/app/templates/projects/signup/request-access.html @@ -3,7 +3,12 @@ {% if panel.additional_context.requested_access %} Your request is being reviewed. Please use the 'Contact Us' link for any further questions. {% else %} -
+
@@ -16,9 +21,9 @@
- - -
+
@@ -26,24 +31,3 @@ {% csrf_token %} {% endif %} - - diff --git a/app/templates/projects/signup/sign-agreement-form.html b/app/templates/projects/signup/sign-agreement-form.html index 44b62764..359fecb7 100644 --- a/app/templates/projects/signup/sign-agreement-form.html +++ b/app/templates/projects/signup/sign-agreement-form.html @@ -7,7 +7,7 @@
{% endif %} -
+
{# Check source of agreement form content #} {% if panel.additional_context.agreement_form.type == 'MODEL' %} {{ panel.additional_context.agreement_form.content | safe }} diff --git a/app/templates/projects/signup/upload-agreement-form.html b/app/templates/projects/signup/upload-agreement-form.html index 17199f2f..7409c515 100644 --- a/app/templates/projects/signup/upload-agreement-form.html +++ b/app/templates/projects/signup/upload-agreement-form.html @@ -5,7 +5,7 @@
-
+
{# Check source of agreement form content #} {% if panel.additional_context.agreement_form.content %} diff --git a/docker-entrypoint-init.d/43-pdf.sh b/docker-entrypoint-init.d/43-pdf.sh new file mode 100644 index 00000000..a7489c6d --- /dev/null +++ b/docker-entrypoint-init.d/43-pdf.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Ensure scratch space for the PDF generator exists and is writable +mkdir -p $DBMI_APP_ROOT/pdf/temp +chown -R $DBMI_NGINX_USER:$DBMI_NGINX_USER $DBMI_APP_ROOT/pdf/temp +chmod -R 775 $DBMI_APP_ROOT/pdf/temp + +echo "$DBMI_APP_ROOT/pdf/temp is ready!" diff --git a/phantomjs-2.1.1-amd64.tar.gz b/phantomjs-2.1.1-amd64.tar.gz new file mode 100644 index 00000000..a85519ee Binary files /dev/null and b/phantomjs-2.1.1-amd64.tar.gz differ diff --git a/phantomjs-2.1.1-arm64.tar.gz b/phantomjs-2.1.1-arm64.tar.gz new file mode 100644 index 00000000..936e0fae Binary files /dev/null and b/phantomjs-2.1.1-arm64.tar.gz differ diff --git a/requirements.txt b/requirements.txt index 713f870e..7849434f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,23 +18,23 @@ asgiref==3.8.1 \ # via # django # django-countries -awscli==1.33.33 \ - --hash=sha256:19e0c0eec367776c2bdf1c77421d2ca19f4b18eeb290ad80dc431baf1444d974 \ - --hash=sha256:8062a454dea01cb4bef334924927691dfb24ffb1dff82cb91a8db3eb1452830c +awscli==1.34.2 \ + --hash=sha256:695d6542b475e1b66b3461997c1dcb390122eadd120c518c41fb4e5cc6d7dd5b \ + --hash=sha256:ede310d824251dfbbff8295c479c9cd550d2517b6326b188b738f5455c8094cb # via -r requirements.in blessed==1.20.0 \ --hash=sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058 \ --hash=sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680 # via django-q -boto3==1.34.151 \ - --hash=sha256:30498a76b6f651ee2af7ae8edc1704379279ab8b91f1a8dd1f4ddf51259b0bc2 \ - --hash=sha256:35bc76faacf1667d3fbb66c1966acf2230ef26206557efc26d9d9d79337bef43 +boto3==1.35.2 \ + --hash=sha256:c2f0837a259002489e59d1c30008791e3b3bb59e30e48c64e1d2d270147a4549 \ + --hash=sha256:cbf197ce28f04bc1ffa1db0aa26a1903d9bfa57a490f70537932e84367cdd15b # via # -r requirements.in # django-ses -botocore==1.34.151 \ - --hash=sha256:0d0968e427a94378f295b49d59170dad539938487ec948de3d030f06092ec6dc \ - --hash=sha256:9018680d7d4a8060c26d127ceec5ab5b270879f423ea39b863d8a46f3e34c404 +botocore==1.35.2 \ + --hash=sha256:92b168d8be79055bb25754aa34d699866d8aa66abc69f8ce99b0c191bd9c6e70 \ + --hash=sha256:96c8eb6f0baed623a1b57ca9f24cb21d5508872cf0dfebb55527a85b6dbc76ba # via # awscli # boto3 @@ -45,59 +45,74 @@ certifi==2024.7.4 \ # via # requests # sentry-sdk -cffi==1.16.0 \ - --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ - --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ - --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ - --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ - --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ - --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ - --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ - --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ - --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ - --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ - --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ - --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ - --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ - --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ - --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ - --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ - --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ - --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ - --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ - --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ - --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ - --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ - --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ - --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ - --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ - --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ - --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ - --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ - --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ - --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ - --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ - --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ - --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ - --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ - --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ - --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ - --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ - --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ - --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ - --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ - --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ - --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ - --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ - --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ - --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ - --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ - --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ - --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ - --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ - --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ - --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ - --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 +cffi==1.17.0 \ + --hash=sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f \ + --hash=sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab \ + --hash=sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499 \ + --hash=sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058 \ + --hash=sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693 \ + --hash=sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb \ + --hash=sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377 \ + --hash=sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885 \ + --hash=sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2 \ + --hash=sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401 \ + --hash=sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4 \ + --hash=sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b \ + --hash=sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59 \ + --hash=sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f \ + --hash=sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c \ + --hash=sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555 \ + --hash=sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa \ + --hash=sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424 \ + --hash=sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb \ + --hash=sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2 \ + --hash=sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8 \ + --hash=sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e \ + --hash=sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9 \ + --hash=sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82 \ + --hash=sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828 \ + --hash=sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759 \ + --hash=sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc \ + --hash=sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118 \ + --hash=sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf \ + --hash=sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932 \ + --hash=sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a \ + --hash=sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29 \ + --hash=sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206 \ + --hash=sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2 \ + --hash=sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c \ + --hash=sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c \ + --hash=sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0 \ + --hash=sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a \ + --hash=sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195 \ + --hash=sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6 \ + --hash=sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9 \ + --hash=sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc \ + --hash=sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb \ + --hash=sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0 \ + --hash=sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7 \ + --hash=sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb \ + --hash=sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a \ + --hash=sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492 \ + --hash=sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720 \ + --hash=sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42 \ + --hash=sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7 \ + --hash=sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d \ + --hash=sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d \ + --hash=sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb \ + --hash=sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4 \ + --hash=sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2 \ + --hash=sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b \ + --hash=sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8 \ + --hash=sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e \ + --hash=sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204 \ + --hash=sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3 \ + --hash=sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150 \ + --hash=sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4 \ + --hash=sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76 \ + --hash=sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e \ + --hash=sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb \ + --hash=sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91 # via cryptography charset-normalizer==3.3.2 \ --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ @@ -224,9 +239,9 @@ cryptography==43.0.0 \ --hash=sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5 \ --hash=sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0 # via django-dbmi-client -django==4.2.14 \ - --hash=sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240 \ - --hash=sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96 +django==4.2.15 \ + --hash=sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30 \ + --hash=sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a # via # -r requirements.in # django-autocomplete-light @@ -259,9 +274,9 @@ django-countries==7.6.1 \ --hash=sha256:1ed20842fe0f6194f91faca21076649513846a8787c9eb5aeec3cbe1656b8acc \ --hash=sha256:c772d4e3e54afcc5f97a018544e96f246c6d9f1db51898ab0c15cd57e19437cf # via -r requirements.in -django-dbmi-client==2.2.0 \ - --hash=sha256:10aaf61b580a287fbb11162f3328cd2be42cdd8ad66b43dcdbafe6d05f021c90 \ - --hash=sha256:1fbff33f6e7fc5b9ee186e5010e53919a2d47cf0da899e090df8cf0ffe4ca8bd +django-dbmi-client==2.3.1 \ + --hash=sha256:3e7d8ede7cfdc8b52547d30dcb84ecb2e105bc67dd72e6bcec232ab9b128b446 \ + --hash=sha256:d47a69ae057b263489f6708c1313e6d86c39712ae050feb07285dabd15c557a2 # via -r requirements.in django-health-check==3.18.3 \ --hash=sha256:18b75daca4551c69a43f804f9e41e23f5f5fb9efd06cf6a313b3d5031bb87bd0 \ @@ -443,9 +458,9 @@ pydantic-core==2.20.1 \ --hash=sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a \ --hash=sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27 # via pydantic -pyjwt==2.8.0 \ - --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ - --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 +pyjwt==2.9.0 \ + --hash=sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850 \ + --hash=sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c # via django-dbmi-client python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ @@ -464,58 +479,60 @@ pytz==2024.1 \ # via # django-dbmi-client # django-ses -pyyaml==6.0.1 \ - --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ - --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ - --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 # via awscli redis==3.5.3 \ --hash=sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2 \ @@ -537,9 +554,9 @@ s3transfer==0.10.2 \ # via # awscli # boto3 -sentry-sdk[django]==2.12.0 \ - --hash=sha256:7a8d5163d2ba5c5f4464628c6b68f85e86972f7c636acc78aed45c61b98b7a5e \ - --hash=sha256:8763840497b817d44c49b3fe3f5f7388d083f2337ffedf008b2cdb63b5c86dc6 +sentry-sdk[django]==2.13.0 \ + --hash=sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6 \ + --hash=sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260 # via django-dbmi-client six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ @@ -553,9 +570,9 @@ sqlparse==0.5.1 \ --hash=sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4 \ --hash=sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e # via django -types-python-dateutil==2.9.0.20240316 \ - --hash=sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202 \ - --hash=sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b +types-python-dateutil==2.9.0.20240821 \ + --hash=sha256:9649d1dcb6fef1046fb18bebe9ea2aa0028b160918518c34589a46045f6ebd98 \ + --hash=sha256:f5889fcb4e63ed4aaa379b44f93c32593d50b9a94c9a60a0c854d8cc3511cd57 # via arrow typing-extensions==4.12.2 \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \