From 060f04f8209f7ee7cfb96a7b1b4efd47edf36954 Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Fri, 11 Oct 2024 08:42:56 -0600 Subject: [PATCH 1/4] feat(projects): Data Use Reporting implementation --- app/manage/urls.py | 2 + app/manage/views.py | 136 +++++++++++++++++- app/projects/admin.py | 5 + app/projects/api.py | 17 ++- app/projects/forms.py | 29 +++- ...ccess_reporting_agreement_form_and_more.py | 29 ++++ .../migrations/0109_datausereportrequest.py | 29 ++++ .../migrations/0110_agreementform_handler.py | 18 +++ app/projects/models.py | 30 ++++ app/projects/tasks.py | 111 ++++++++++++++ app/projects/urls.py | 2 + app/projects/views.py | 31 +++- app/static/agreementforms/4ce-report.html | 74 ++++++++++ 13 files changed, 509 insertions(+), 4 deletions(-) create mode 100644 app/projects/migrations/0108_dataproject_access_reporting_agreement_form_and_more.py create mode 100644 app/projects/migrations/0109_datausereportrequest.py create mode 100644 app/projects/migrations/0110_agreementform_handler.py create mode 100644 app/projects/tasks.py create mode 100644 app/static/agreementforms/4ce-report.html diff --git a/app/manage/urls.py b/app/manage/urls.py index 82b579ca..337a637a 100644 --- a/app/manage/urls.py +++ b/app/manage/urls.py @@ -9,6 +9,7 @@ from manage.views import team_notification from manage.views import UploadSignedAgreementFormView from manage.views import UploadSignedAgreementFormFileView +from manage.views import ProjectDataUseReportParticipants from manage.api import set_dataproject_details from manage.api import set_dataproject_registration_status @@ -62,6 +63,7 @@ re_path(r'^remove-view-permission/(?P[^/]+)/(?P[^/]+)/$', remove_view_permission, name='remove-view-permission'), re_path(r'^get-project-participants/(?P[^/]+)/$', ProjectParticipants.as_view(), name='get-project-participants'), re_path(r'^get-project-pending-participants/(?P[^/]+)/$', ProjectPendingParticipants.as_view(), name='get-project-pending-participants'), + re_path(r'^get-project-data-use-reporting-participants/(?P[^/]+)/$', ProjectDataUseReportParticipants.as_view(), name='get-project-data-use-reporting-participants'), re_path(r'^upload-signed-agreement-form/(?P[^/]+)/(?P[^/]+)/$', UploadSignedAgreementFormView.as_view(), name='upload-signed-agreement-form'), re_path(r'^upload-signed-agreement-form-file/(?P[^/]+)/$', UploadSignedAgreementFormFileView.as_view(), name='upload-signed-agreement-form-file'), re_path(r'^(?P[^/]+)/$', DataProjectManageView.as_view(), name='manage-project'), diff --git a/app/manage/views.py b/app/manage/views.py index c531ae1a..c930b4ce 100644 --- a/app/manage/views.py +++ b/app/manage/views.py @@ -340,8 +340,13 @@ def get(self, request, project_key, *args, **kwargs): signed_agreement_forms = [] signed_accepted_agreement_forms = 0 + # Get all agreement forms + agreement_forms = list(project.agreement_forms.all()) + if project.data_use_report_agreement_form: + agreement_forms.append(project.data_use_report_agreement_form) + # For each of the available agreement forms for this project, display only latest version completed by the user - for agreement_form in project.agreement_forms.all(): + for agreement_form in agreement_forms: # Check if this project uses shared agreement forms if project.shares_agreement_forms: @@ -460,6 +465,13 @@ def get(self, request, project_key, *args, **kwargs): # Filter participants_waiting_access = participants_waiting_access.filter(agreement_form_query) + # Do not include users whose access was removed due to data use reporting requirements + if project.data_use_report_agreement_form: + + # Ensure there exists no data use reporting request for this user + data_use_report_query = Q(datausereportrequest__isnull=True) + participants_waiting_access = participants_waiting_access.filter(data_use_report_query) + # Secondly, we want Participants with at least one pending SignedAgreementForm participants_awaiting_approval = Participant.objects.filter(Q(project=project, permission__isnull=True)).filter( Q( @@ -566,6 +578,128 @@ def get(self, request, project_key, *args, **kwargs): return JsonResponse(data=data) +@method_decorator(user_auth_and_jwt, name='dispatch') +class ProjectDataUseReportParticipants(View): + + def get(self, request, project_key, *args, **kwargs): + + # Pull the project + try: + project = DataProject.objects.get(project_key=project_key) + except DataProject.NotFound: + logger.exception('DataProject for key "{}" not found'.format(project_key)) + return HttpResponse(status=404) + + # Get needed params + draw = int(request.GET['draw']) + start = int(request.GET['start']) + length = int(request.GET['length']) + order_column = int(request.GET['order[0][column]']) + order_direction = request.GET['order[0][dir]'] + + # Check for a search value + search = request.GET['search[value]'] + + # Check what we're sorting by and in what direction + if order_column == 0: + sort_order = ['email'] if order_direction == 'asc' else ['-email'] + elif order_column == 3 and not project.has_teams or order_column == 4 and project.has_teams: + sort_order = ['modified', '-email'] if order_direction == 'asc' else ['-modified', 'email'] + else: + sort_order = ['modified', '-email'] if order_direction == 'asc' else ['-modified', 'email'] + + # Build the query + + # Find users with all access but pending data use report agreement forms + participants_waiting_access = Participant.objects.filter( + Q( + project=project, + user__signedagreementform__agreement_form=project.data_use_report_agreement_form, + user__signedagreementform__status="P", + ) + ) + + # Add search if necessary + if search: + participants_waiting_access = participants_waiting_access.filter(user__email__icontains=search) + + # We only want distinct Participants belonging to the users query + # Django won't sort on a related field after this union so we annotate each queryset with the user's email to sort on + query_set = participants_waiting_access.annotate(email=F("user__email")) \ + .order_by(*sort_order) + + # Setup paginator + paginator = Paginator( + query_set, + length, + ) + + # Determine page index (1-index) from DT parameters + page = start / length + 1 + participant_page = paginator.page(page) + + participants = [] + for participant in participant_page: + + signed_agreement_forms = [] + signed_accepted_agreement_forms = 0 + + # Get all agreement forms + agreement_forms = list(project.agreement_forms.all()) + [project.data_use_report_agreement_form] + + # Fetch only for this project + signed_forms = SignedAgreementForm.objects.filter( + user__email=participant.user.email, + project=project, + agreement_form__in=agreement_forms, + ) + for signed_form in signed_forms: + if signed_form is not None: + signed_agreement_forms.append(signed_form) + + # Collect how many forms are approved to craft language for status + if signed_form.status == 'A': + signed_accepted_agreement_forms += 1 + + # Build the row of the table for this participant + participant_row = [ + participant.user.email.lower(), + 'Access granted' if participant.permission == 'VIEW' else 'No access', + [ + { + 'status': f.status, + 'id': f.id, + 'name': f.agreement_form.short_name, + 'project': f.project.project_key, + } for f in signed_agreement_forms + ], + { + 'access': True if participant.permission == 'VIEW' else False, + 'email': participant.user.email.lower(), + 'signed': signed_accepted_agreement_forms, + 'team': True if project.has_teams else False, + 'required': project.agreement_forms.count() + }, + participant.modified, + ] + + # If project has teams, add that + if project.has_teams: + participant_row.insert(1, participant.team.team_leader.email.lower() if participant.team and participant.team.team_leader else '') + + participants.append(participant_row) + + # Build DataTables response data + data = { + 'draw': draw, + 'recordsTotal': query_set.count(), + 'recordsFiltered': paginator.count, + 'data': participants, + 'error': None, + } + + return JsonResponse(data=data) + @user_auth_and_jwt def team_notification(request, project_key=None): """ diff --git a/app/projects/admin.py b/app/projects/admin.py index d677b212..63be4b22 100644 --- a/app/projects/admin.py +++ b/app/projects/admin.py @@ -17,6 +17,7 @@ from projects.models import ChallengeTaskSubmissionDownload from projects.models import Bucket from projects.models import InstitutionalOfficial +from projects.models import DataUseReportRequest class GroupAdmin(admin.ModelAdmin): @@ -91,6 +92,9 @@ class ChallengeTaskSubmissionDownloadAdmin(admin.ModelAdmin): list_display = ('user', 'submission', 'download_date') search_fields = ('user__email', ) +class DataUseReportRequestAdmin(admin.ModelAdmin): + list_display = ('participant', 'data_project', 'created', 'modified') + search_fields = ('participant__user__email', ) admin.site.register(Group, GroupAdmin) admin.site.register(Bucket, BucketAdmin) @@ -107,3 +111,4 @@ class ChallengeTaskSubmissionDownloadAdmin(admin.ModelAdmin): admin.site.register(ChallengeTask, ChallengeTaskAdmin) admin.site.register(ChallengeTaskSubmission, ChallengeTaskSubmissionAdmin) admin.site.register(ChallengeTaskSubmissionDownload, ChallengeTaskSubmissionDownloadAdmin) +admin.site.register(DataUseReportRequest, DataUseReportRequestAdmin) diff --git a/app/projects/api.py b/app/projects/api.py index 6c64b150..e5bfc42a 100644 --- a/app/projects/api.py +++ b/app/projects/api.py @@ -1,6 +1,7 @@ from copy import copy from datetime import datetime import json +import importlib import logging from django.conf import settings @@ -663,7 +664,7 @@ def save_signed_agreement_form(request): "warning", f"The agreement form contained errors, please review", "warning-sign" ) return response - + # Use the data from the form fields = form.cleaned_data @@ -762,6 +763,20 @@ def save_signed_agreement_form(request): # Save the agreement form signed_agreement_form.save() + # Check for a handler + if agreement_form.handler: + try: + # Build function components + module_name, handler_name = agreement_form.handler.rsplit('.', 1) + module = importlib.import_module(module_name) + + # Call handler + getattr(module, handler_name)(signed_agreement_form) + logger.debug(f"Handler '{agreement_form.handler}' called for SignedAgreementForm") + + except Exception as e: + logger.exception(f"Error calling handler: {e}", exc_info=True) + return HttpResponse(status=200) @user_auth_and_jwt diff --git a/app/projects/forms.py b/app/projects/forms.py index d673aa7a..d759b452 100644 --- a/app/projects/forms.py +++ b/app/projects/forms.py @@ -6,7 +6,10 @@ from django.utils.translation import gettext_lazy as _ from hypatio.scireg_services import get_current_user_profile -from projects.models import DataProject, AgreementForm +from projects.models import DataProject +from projects.models import AgreementForm +from projects.models import SignedAgreementForm +from projects.models import Participant class MultiValueValidationError(ValidationError): @@ -200,3 +203,27 @@ def set_initial(self, request: HttpRequest, project: DataProject, agreement_form } ''' return initial + + +def data_use_report_handler(signed_agreement_form: SignedAgreementForm): + """ + Handler the result of the data use report. This will be determining + whether the user's access is ended or paused. + + :param signed_agreement_form: The saved data use report agreement form + :type signed_agreement_form: SignedAgreementForm + """ + # The name of the field we are interesed in + USING_DATA = "using_data" + + # Check value + if signed_agreement_form.fields.get(USING_DATA) in ["No", "no"]: + + # End this user's access immediately + participant = Participant.objects.get(project=signed_agreement_form.project, user=signed_agreement_form.user) + participant.permission = None + participant.save() + + # Auto-approve this signed agreement form since no review is necessary + signed_agreement_form.status = "A" + signed_agreement_form.save() diff --git a/app/projects/migrations/0108_dataproject_access_reporting_agreement_form_and_more.py b/app/projects/migrations/0108_dataproject_access_reporting_agreement_form_and_more.py new file mode 100644 index 00000000..61875024 --- /dev/null +++ b/app/projects/migrations/0108_dataproject_access_reporting_agreement_form_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.16 on 2024-09-27 16:22 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0107_agreementform_form_class'), + ] + + operations = [ + migrations.AddField( + model_name='dataproject', + name='data_use_report_agreement_form', + field=models.ForeignKey(blank=True, help_text='The agreement form that will be filled out by a user with access during periodic access and data use reporting', null=True, on_delete=django.db.models.deletion.SET_NULL, to='projects.agreementform'), + ), + migrations.AddField( + model_name='dataproject', + name='data_use_report_grace_period', + field=models.IntegerField(blank=True, help_text='The number of days in which a user is allotted to complete their access and data use reporting before their access is revoked', null=True), + ), + migrations.AddField( + model_name='dataproject', + name='data_use_report_period', + field=models.IntegerField(blank=True, help_text='The number of days after access being granted in which emails will be sent prompting users to report on the status of their access and use of data', null=True), + ), + ] diff --git a/app/projects/migrations/0109_datausereportrequest.py b/app/projects/migrations/0109_datausereportrequest.py new file mode 100644 index 00000000..75ad0745 --- /dev/null +++ b/app/projects/migrations/0109_datausereportrequest.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.16 on 2024-10-09 12:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0108_dataproject_access_reporting_agreement_form_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='DataUseReportRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('data_project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.dataproject')), + ('participant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='projects.participant')), + ('signed_agreement_form', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='projects.signedagreementform')), + ], + options={ + 'verbose_name': 'Data Use Report Request', + 'verbose_name_plural': 'Data Use Report Requests', + }, + ), + ] diff --git a/app/projects/migrations/0110_agreementform_handler.py b/app/projects/migrations/0110_agreementform_handler.py new file mode 100644 index 00000000..83c6780a --- /dev/null +++ b/app/projects/migrations/0110_agreementform_handler.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-10-10 13:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0109_datausereportrequest'), + ] + + operations = [ + migrations.AddField( + model_name='agreementform', + name='handler', + field=models.CharField(blank=True, help_text="Set an absolute function's path to be called after the SignedAgreementForm has successfully saved", max_length=512, null=True), + ), + ] diff --git a/app/projects/models.py b/app/projects/models.py index a7684fe5..00fd7a7d 100644 --- a/app/projects/models.py +++ b/app/projects/models.py @@ -266,6 +266,8 @@ class AgreementForm(models.Model): institutional_signers = models.BooleanField(default=False, help_text="Allows institutional signers to sign for their members. This will auto-approve this agreement form for members whose institutional official has had their agreement form approved.") form_class = models.CharField(max_length=300, null=True, blank=True) + handler = models.CharField(max_length=512, null=True, blank=True, help_text="Set an absolute function's path to be called after the SignedAgreementForm has successfully saved") + # Meta created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) @@ -392,6 +394,17 @@ class DataProject(models.Model): # Automate approval of members covered by an already-approved institutional signer institutional_signers = models.BooleanField(default=False, help_text="Allows institutional signers to sign for their members. This will auto-approve agreement forms for members whose institutional official has had their agreement forms approved.") + # Set period for automated check-ins for users with access + data_use_report_period = models.IntegerField(blank=True, null=True, help_text="The number of days after access being granted in which emails will be sent prompting users to report on the status of their access and use of data") + data_use_report_agreement_form = models.ForeignKey( + to=AgreementForm, + on_delete=models.SET_NULL, + blank=True, + null=True, + help_text="The agreement form that will be filled out by a user with access during periodic access and data use reporting", + ) + data_use_report_grace_period = models.IntegerField(blank=True, null=True, help_text="The number of days in which a user is allotted to complete their access and data use reporting before their access is revoked") + # Meta created = models.DateTimeField(auto_now_add=True) @@ -476,6 +489,23 @@ class Meta: verbose_name_plural = 'Signed Agreement Forms' +class DataUseReportRequest(models.Model): + """ + This model describes a request for a participant to report on data use. + """ + + participant = models.ForeignKey("Participant", on_delete=models.PROTECT) + data_project = models.ForeignKey(DataProject, on_delete=models.CASCADE) + signed_agreement_form = models.ForeignKey("SignedAgreementForm", null=True, blank=True, on_delete=models.PROTECT) + + # Meta + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + class Meta: + verbose_name = 'Data Use Report Request' + verbose_name_plural = 'Data Use Report Requests' + + class Team(models.Model): """ This model describes a team of participants that are competing in a data challenge. diff --git a/app/projects/tasks.py b/app/projects/tasks.py new file mode 100644 index 00000000..6fdc04a9 --- /dev/null +++ b/app/projects/tasks.py @@ -0,0 +1,111 @@ +import uuid +import os +import shutil +import json +import requests +import tempfile +from datetime import datetime, timedelta, timezone +from django.conf import settings +from django.urls import reverse +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from furl import furl + +from projects.models import DataProject +from projects.models import Participant +from projects.models import DataUseReportRequest + +import logging +logger = logging.getLogger(__name__) + + +def send_data_use_report_requests(): + """ + Runs on a daily basis and will send emails requesting data user reports be + completed by users with access to DataProjects that have data use reporting + requirements. + """ + logger.debug(f"### Send Data Use Report Requests ###") + + # Iterate projects that require data use reporting + for data_project in DataProject.objects.filter(data_use_report_agreement_form__isnull=False): + logger.debug(f"Checking data use report requests for '{data_project.project_key}'") + + # Fetch users with access + for participant in Participant.objects.filter(project=data_project, permission="VIEW"): + logger.debug(f"Checking data use report requests for '{data_project.project_key}' / '{participant}'") + + # Check for existing request that is incomplete + existing_request = DataUseReportRequest.objects.filter(data_project=data_project, participant=participant, signed_agreement_form__isnull=True).first() + if existing_request: + + # Calculate how many days left for request + request_delta = datetime.now(timezone.utc) - existing_request.modified + logger.debug(f"Data use request already sent for '{data_project.project_key}' / '{participant}' {request_delta.days} ago") + + # If less than three days left, send a reminder + if data_project.data_use_report_grace_period - request_delta.days == 3: + + subject = '[FINAL NOTICE] DBMI Portal - Data Use Report' + if send_data_use_report_request(data_use_report_request=existing_request, subject=subject, days_left=3): + logger.debug(f"Data use request reminder sent for '{data_project.project_key}' / '{participant}'") + else: + logger.error(f"Data use report request reminder not sent") + + elif data_project.data_use_report_grace_period - request_delta.days < 0: + + # Revoke access + logger.debug(f"Data use request not heeded '{data_project.project_key}' / '{participant}', revoking access") + participant.permission = None + participant.save() + + else: + # Create the request + data_use_report_request = DataUseReportRequest.objects.create(data_project=data_project, participant=participant) + + # Get access granted date and compare it to report period + access_delta = datetime.now(timezone.utc) - participant.modified + if access_delta.days >= data_project.data_use_report_period: + + subject = '[ACTION REQUIRED] DBMI Portal - Data Use Report' + if send_data_use_report_request(data_use_report_request=data_use_report_request, subject=subject): + logger.debug(f"Data use request sent for '{data_project.project_key}' / '{participant}'") + else: + logger.error(f"Data use report request not sent") + + +def send_data_use_report_request(data_use_report_request, subject='[ACTION REQUIRED] DBMI Portal - Data Use Report', days_left=None): + + try: + # Form the context. + data_use_report_url = furl(settings.SITE_URL) / reverse("projects:data_use_report", kwargs={"request_id": data_use_report_request.id}) + + # If not passed, use the grace period for number of days left to comply + if not days_left: + days_left = data_use_report_request.data_project.data_use_report_grace_period + + context = { + 'project': data_use_report_request.data_project, + 'data_use_report_url': data_use_report_url.url, + 'grace_period_days': days_left, + } + + # Render templates + body_html = render_to_string('email/email_data_use_report.html', context) + body = render_to_string('email/email_data_use_report.txt', context) + + email = EmailMultiAlternatives( + subject=subject, + body=body, + from_email=settings.EMAIL_FROM_ADDRESS, + reply_to=(settings.EMAIL_REPLY_TO_ADDRESS, ), + to=[data_use_report_request.participant.user.email] + ) + email.attach_alternative(body_html, "text/html") + email.send() + + return True + + except Exception as e: + logger.exception(f"Error sending request: {e}", exc_info=True) + return False diff --git a/app/projects/urls.py b/app/projects/urls.py index 48690761..5cd8403e 100644 --- a/app/projects/urls.py +++ b/app/projects/urls.py @@ -2,6 +2,7 @@ from projects.apps import ProjectsConfig from projects.views import list_data_projects +from projects.views import data_use_report from projects.views import signed_agreement_form from projects.views import DataProjectView @@ -36,6 +37,7 @@ re_path(r'^reject_team_join/$', reject_team_join, name='reject_team_join'), re_path(r'^create_team/$', create_team, name='create_team'), re_path(r'^finalize_team/$', finalize_team, name='finalize_team'), + re_path(r'^data_use_report/(?P[^/]+)/?$', data_use_report, name='data_use_report'), re_path(r'^signed_agreement_form/$', signed_agreement_form, name='signed_agreement_form'), re_path(r'^download_dataset/$', download_dataset, name='download_dataset'), re_path(r'^upload_challengetasksubmission_file/$', upload_challengetasksubmission_file, name="upload_challengetasksubmission_file"), diff --git a/app/projects/views.py b/app/projects/views.py index fe98bd5a..90ab5977 100644 --- a/app/projects/views.py +++ b/app/projects/views.py @@ -1,6 +1,7 @@ import logging from datetime import datetime import dateutil.parser +from furl import furl from django.conf import settings from django.core.exceptions import ObjectDoesNotExist @@ -10,6 +11,9 @@ from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.generic import TemplateView +from django.urls import reverse +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string from hypatio.sciauthz_services import SciAuthZ from hypatio.dbmiauthz_services import DBMIAuthz @@ -26,9 +30,11 @@ from projects.models import DataProject from projects.models import HostedFile from projects.models import Participant +from projects.models import AgreementForm from projects.models import SignedAgreementForm from projects.models import Group from projects.models import InstitutionalOfficial +from projects.models import DataUseReportRequest from projects.panels import SIGNUP_STEP_COMPLETED_STATUS from projects.panels import SIGNUP_STEP_CURRENT_STATUS from projects.panels import SIGNUP_STEP_FUTURE_STATUS @@ -43,6 +49,29 @@ logger = logging.getLogger(__name__) +@user_auth_and_jwt +def data_use_report(request, request_id): + + # Get the original request + data_use_report_request = get_object_or_404(DataUseReportRequest, id=request_id) + + user_jwt = request.COOKIES.get("DBMI_JWT", None) + sciauthz = SciAuthZ(user_jwt, request.user.email) + is_manager = sciauthz.user_has_manage_permission(data_use_report_request.data_project.project_key) + participant = data_use_report_request.participant + + template_name = "projects/participate/data-use-report.html" + + return render(request, template_name, { + "user": request.user, + "is_manager": is_manager, + "agreement_form": data_use_report_request.data_project.data_use_report_agreement_form, + "participant": participant, + "project": participant.project, + "form_context": {}, + }) + + @user_auth_and_jwt def signed_agreement_form(request): @@ -581,7 +610,7 @@ def setup_panel_sign_agreement_forms(self, context): template = 'projects/signup/sign-external-agreement-form.html' else: raise Exception("Agreement form type Not implemented") - + # Check if this agreement form has a specified form class form = None if agreement_form.form_class: diff --git a/app/static/agreementforms/4ce-report.html b/app/static/agreementforms/4ce-report.html new file mode 100644 index 00000000..a74038ed --- /dev/null +++ b/app/static/agreementforms/4ce-report.html @@ -0,0 +1,74 @@ +
+
+
+
+
+ +
+
+ +
+
+ +
+

  Important

+ Under conditions of the signed Data Use Agreement you are obligated to destroy your copy of the Data when no longer in use +
+ +
+
+ + +
+
+ +
+ +

Signature

+
+ + +
+
+ + +
+ +
+
+
+ From 8c4e22ff591fafbd5b13910069cea32355a2de68 Mon Sep 17 00:00:00 2001 From: b32147 <15659860+b32147@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:10:06 +0000 Subject: [PATCH 2/4] fix(requirements): Updated Python requirements --- requirements.txt | 233 +++++++++++++++++++++++++---------------------- 1 file changed, 124 insertions(+), 109 deletions(-) diff --git a/requirements.txt b/requirements.txt index 45610040..230bb8f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,23 +18,23 @@ asgiref==3.8.1 \ # via # django # django-countries -awscli==1.34.24 \ - --hash=sha256:466a41e85f15957af2d5e9d601c8c4808bea44547ac3a7e082186df0f932534e \ - --hash=sha256:9641ae4edbb241a5d4646fa7e7f9b2b5ff45f36f77e6ffdd5c401cfecf6af317 +awscli==1.35.5 \ + --hash=sha256:cf826082e7e1b6f8163f6a67e34431342ba7a933bcb54bcdd03a7d86a0ead75c \ + --hash=sha256:e3a1801bbb5772423549dfce2bd55395292fc84dcfaf115a7268463462a6c3af # via -r requirements.in blessed==1.20.0 \ --hash=sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058 \ --hash=sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680 # via django-q -boto3==1.35.24 \ - --hash=sha256:97fcc1a14cbc759e4ba9535ced703a99fcf652c9c4b8dfcd06f292c80551684b \ - --hash=sha256:be7807f30f26d6c0057e45cfd09dad5968e664488bf4f9138d0bb7a0f6d8ed40 +boto3==1.35.39 \ + --hash=sha256:5970b62c1ec8177501e02520f0d41839ca5fc549b30bac4e8c0c0882ae776217 \ + --hash=sha256:670f811c65e3c5fe4ed8c8d69be0b44b1d649e992c0fc16de43816d1188f88f1 # via # -r requirements.in # django-ses -botocore==1.35.24 \ - --hash=sha256:1e59b0f14f4890c4f70bd6a58a634b9464bed1c4c6171f87c8795d974ade614b \ - --hash=sha256:eb9ccc068255cc3d24c36693fda6aec7786db05ae6c2b13bcba66dce6a13e2e3 +botocore==1.35.39 \ + --hash=sha256:781c547eb6a79c0e4b0bedd87b81fbfed957816b4841d33e20c8f1989c7c19ce \ + --hash=sha256:cb7f851933b5ccc2fba4f0a8b846252410aa0efac5bfbe93b82d10801f5f8e90 # via # awscli # boto3 @@ -114,97 +114,112 @@ cffi==1.17.1 \ --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b # via cryptography -charset-normalizer==3.3.2 \ - --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ - --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ - --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ - --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ - --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ - --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ - --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ - --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ - --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ - --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ - --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ - --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ - --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ - --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ - --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ - --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ - --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ - --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ - --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ - --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ - --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ - --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ - --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ - --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ - --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ - --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ - --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ - --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ - --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ - --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ - --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ - --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ - --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ - --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ - --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ - --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ - --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ - --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ - --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ - --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ - --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ - --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ - --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ - --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ - --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ - --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ - --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ - --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ - --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ - --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ - --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ - --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ - --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ - --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ - --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ - --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ - --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ - --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ - --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ - --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ - --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ - --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ - --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ - --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ - --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ - --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ - --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ - --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ - --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ - --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ - --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ - --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ - --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ - --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ - --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ - --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ - --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ - --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ - --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ - --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ - --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ - --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ - --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ - --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ - --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ - --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ - --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ - --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ - --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ - --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 +charset-normalizer==3.4.0 \ + --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ + --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ + --hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \ + --hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \ + --hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \ + --hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \ + --hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \ + --hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \ + --hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \ + --hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \ + --hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \ + --hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \ + --hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \ + --hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \ + --hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \ + --hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \ + --hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \ + --hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \ + --hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \ + --hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \ + --hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \ + --hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \ + --hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \ + --hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \ + --hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \ + --hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \ + --hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \ + --hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \ + --hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \ + --hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \ + --hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \ + --hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \ + --hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \ + --hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \ + --hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \ + --hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \ + --hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \ + --hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \ + --hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \ + --hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \ + --hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \ + --hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \ + --hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \ + --hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \ + --hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \ + --hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \ + --hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \ + --hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \ + --hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \ + --hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \ + --hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \ + --hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \ + --hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \ + --hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \ + --hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \ + --hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \ + --hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \ + --hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \ + --hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \ + --hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \ + --hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \ + --hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \ + --hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \ + --hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \ + --hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \ + --hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \ + --hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \ + --hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \ + --hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \ + --hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \ + --hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \ + --hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \ + --hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \ + --hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \ + --hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \ + --hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \ + --hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \ + --hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \ + --hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \ + --hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \ + --hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \ + --hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \ + --hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \ + --hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \ + --hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \ + --hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \ + --hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \ + --hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \ + --hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \ + --hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \ + --hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \ + --hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \ + --hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \ + --hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \ + --hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \ + --hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \ + --hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \ + --hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \ + --hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \ + --hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \ + --hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \ + --hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \ + --hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \ + --hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \ + --hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482 # via requests colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ @@ -548,15 +563,15 @@ rsa==4.7.2 \ --hash=sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2 \ --hash=sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9 # via awscli -s3transfer==0.10.2 \ - --hash=sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6 \ - --hash=sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69 +s3transfer==0.10.3 \ + --hash=sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d \ + --hash=sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c # via # awscli # boto3 -sentry-sdk[django]==2.14.0 \ - --hash=sha256:1e0e2eaf6dad918c7d1e0edac868a7bf20017b177f242cefe2a6bcd47955961d \ - --hash=sha256:b8bc3dc51d06590df1291b7519b85c75e2ced4f28d9ea655b6d54033503b5bf4 +sentry-sdk[django]==2.16.0 \ + --hash=sha256:49139c31ebcd398f4f6396b18910610a0c1602f6e67083240c33019d1f6aa30c \ + --hash=sha256:90f733b32e15dfc1999e6b7aca67a38688a567329de4d6e184154a73f96c6892 # via django-dbmi-client six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ @@ -570,9 +585,9 @@ sqlparse==0.5.1 \ --hash=sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4 \ --hash=sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e # via django -types-python-dateutil==2.9.0.20240906 \ - --hash=sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6 \ - --hash=sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e +types-python-dateutil==2.9.0.20241003 \ + --hash=sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d \ + --hash=sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446 # via arrow typing-extensions==4.12.2 \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ From 23f41c54749cb359da9cc56672b250f5f6f21a5f Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Mon, 14 Oct 2024 08:31:37 -0600 Subject: [PATCH 3/4] feat(projects): Data Use Report templates --- .../email/email_data_use_report.html | 19 ++ app/templates/email/email_data_use_report.txt | 19 ++ app/templates/manage/project-base.html | 195 +++++++++++++++++- .../projects/participate/data-use-report.html | 98 +++++++++ 4 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 app/templates/email/email_data_use_report.html create mode 100644 app/templates/email/email_data_use_report.txt create mode 100644 app/templates/projects/participate/data-use-report.html diff --git a/app/templates/email/email_data_use_report.html b/app/templates/email/email_data_use_report.html new file mode 100644 index 00000000..52eb452e --- /dev/null +++ b/app/templates/email/email_data_use_report.html @@ -0,0 +1,19 @@ +{% extends "email/email_base.html" %} + +{% block title %}DBMI Data Portal - Data Use Report{% endblock %} + +{% block content %} + +

Hi,

+ +

This is a request to complete your annual data use report as a condition of the signed Data Use Agreement for {{ project.name }}

+ +

Please click here or use the following link to complete your data use report

+ +

{{ data_use_report_url }}

+ +

Important: If a report is not completed within {{ grace_period_days }} days from the date this email was sent, your access to the Data will be automatically terminated.

+ +

Thank you

+ +{% endblock %} diff --git a/app/templates/email/email_data_use_report.txt b/app/templates/email/email_data_use_report.txt new file mode 100644 index 00000000..c09c31fd --- /dev/null +++ b/app/templates/email/email_data_use_report.txt @@ -0,0 +1,19 @@ +{% extends "email/email_base.html" %} + +{% block title %}DBMI Data Portal - Data Use Report{% endblock %} + +{% block content %} + +Hi, + +This is a request to complete your annual data use report as a condition of the signed Data Use Agreement for {{ project.name }} + +Please use the following link to complete your data use report: + +{{ data_use_report_url }} + +Important: If a report is not completed within {{ grace_period_days }} days from the date this email was sent, your access to the Data will be automatically terminated. + +Thank you + +{% endblock %} diff --git a/app/templates/manage/project-base.html b/app/templates/manage/project-base.html index d7468d9c..8ab6f4e2 100644 --- a/app/templates/manage/project-base.html +++ b/app/templates/manage/project-base.html @@ -369,6 +369,43 @@

+{% if project.data_use_report_agreement_form %} +
+
+
+ +
+
+
+ + + + + {% if project.has_teams %} + + {% endif %} + + + + + + + + {# Loaded via DataTables.js #} + +
EmailTeamAccessFormsActionsRequest Date
+
+
+
+
+
+
+{% endif %} +
@@ -675,6 +712,7 @@ "dom": 'Blfrtip', "processing": true, "serverSide": true, + "bAutoWidth": false, "ajax": "{% url 'manage:get-project-pending-participants' project_key=project_key %}", "initComplete": function(settings, json) { var textBox = $('#pending-participant-table_filter label input'); @@ -750,7 +788,7 @@ // Return the button to revoke return ''; @@ -762,7 +800,7 @@ // Return button to approve user return ''; @@ -817,6 +855,7 @@ // Order by the Access column to prioritize pending access. "dom": 'Blfrtip', "searching": true, + "bAutoWidth": false, "search": { "return": true }, @@ -904,11 +943,11 @@ // Return the button to revoke return ''; - } else if(data['signed'] == data['required'] && !data['team']) { + } else if(data['signed'] >= data['required'] && !data['team']) { // Set the URL for approving access var buttonUrl = '{% url "manage:grant-view-permission" project_key=project_key user_email='USER_EMAIL' %}'.replace('USER_EMAIL', data['email']); @@ -916,11 +955,11 @@ // Return button to approve user return ''; - } else if(data['signed'] == data['required'] && data['team']) { + } else if(data['signed'] >= data['required'] && data['team']) { return 'Grant approval via team management.'; @@ -998,6 +1037,150 @@ } }] }); + + var dataUseReportingParticipantTable = $('#data-use-reporting-participant-table').DataTable({ + // Order by the Access column to prioritize pending access. + "dom": 'Blfrtip', + "processing": true, + "serverSide": true, + "bAutoWidth": false, + "ajax": "{% url 'manage:get-project-data-use-reporting-participants' project_key=project_key %}", + "initComplete": function(settings, json) { + var textBox = $('#data-use-reporting-participant-table_filter label input'); + textBox.unbind(); + textBox.bind('keyup input', function(e) { + if(e.keyCode == 8 && !textBox.val() || e.keyCode == 46 && !textBox.val()) { + // do nothing ¯\_(ツ)_/¯ + } else if(e.keyCode == 13 || !textBox.val()) { + pendingParticipantTable.search(this.value).draw(); + } + }); + }, + "order": [[{% if project.has_teams %}5{% else %}4{% endif %}, "desc"]], + "columnDefs": [ + { + "targets": [{% if project.has_teams %}1, 2, 3{% else %}1, 2{% endif %}], + "orderable": false + }, + { + "targets": {% if project.has_teams %}3{% else %}2{% endif %}, + "render": function(data, type, full, meta) { + + // Build HTML render + var html = ''; + + // Data is an array of forms + for (index = 0; index < data.length; index++) { + + // Get the form + var form = data[index]; + + // Set the base URL for the forms + var formUrl = '{% url "projects:signed_agreement_form" %}?project_key=' + form['project'] + '&signed_form_id=' + form['id']; + + // Set form type and icon classes + var formClasses = (function(formStatus) { + switch(formStatus) { + case 'P': + return ['btn-warning', 'glyphicon-alert']; + case 'A': + return ['btn-default', 'glyphicon-ok']; + case 'R': + return ['btn-danger', 'glyphicon-ban-circle']; + default: + return ['btn-warning', 'glyphicon-alert']; + } + })(form['status']); + + // Set link HTML with placeholders for button classes + var link = '' + form['name'] + + ' '; + + // Append it to the other elements + html += link + } + + return html; + } + }, + { + "targets": {% if project.has_teams %}4{% else %}3{% endif %}, + "render": function(data, type, full, meta) { + + // Determine output based on access, forms signed, etc. + if(data['access']) { + + // Set the URL for revoking access + var buttonUrl = '{% url "manage:remove-view-permission" project_key=project_key user_email='USER_EMAIL' %}'.replace('USER_EMAIL', data['email']); + + // Return the button to revoke + return ''; + + } else if(data['signed'] + 1 == data['required'] && !data['team']) { + + // Set the URL for approving access + var buttonUrl = '{% url "manage:grant-view-permission" project_key=project_key user_email='USER_EMAIL' %}'.replace('USER_EMAIL', data['email']); + + // Return button to approve user + return ''; + + } else if(data['signed'] >= data['required'] && data['team']) { + + return 'Grant approval via team management.'; + + } else { + + return 'Forms incomplete or awaiting your approval'; + } + } + }, + { + "targets": [{% if project.has_teams %}5{% else %}4{% endif %}], + "render": function (data, type, full, meta) { + + // Return raw dates for sorting + if (type === 'sort' || type === 'type') + return data; + + // Parse date and get components + var date = new Date(data); + var y = date.getFullYear(); + var m = date.getMonth() + 1; + var d = date.getDate(); + var h = date.getHours(); + var min = ("0" + date.getMinutes()).slice(-2); + + return m + '-' + d + '-' + y + ' ' + h + ':' + min; + } + } + ], + "drawCallback": function( settings ) { + + // Collect all Intercooler links + $('#data-use-reporting-participant-table [ic-get-from], #data-use-reporting-participant-table [ic-post-to], ' + + '#data-use-reporting-participant-table [ic-put-to], #data-use-reporting-participant-table [ic-patch-to], ' + + '#data-use-reporting-participant-table [ic-delete-from]').each(function() { + + // Check attribute + if( !$(this).attr('ic-id') ) { + + // Process it + Intercooler.processNodes($(this)); + } + }); + }, + }); }); diff --git a/app/templates/projects/participate/data-use-report.html b/app/templates/projects/participate/data-use-report.html new file mode 100644 index 00000000..b0635e49 --- /dev/null +++ b/app/templates/projects/participate/data-use-report.html @@ -0,0 +1,98 @@ +{% extends 'sub-base.html' %} +{% load projects_extras %} +{% load bootstrap3 %} + +{% block headscripts %} +{% endblock %} + +{% block tab_name %}{{ project.name }}{% endblock %} +{% block title %}{{ project.name }}{% endblock %} +{% block subtitle %}{{ project.short_description }}{% endblock %} + +{% block subcontent %} + +{% if messages %} + {% include 'messages.html' %} +{% endif %} + +
+
+ +
+
+ {% if agreement_form.description is not None and agreement_form.description != "" %} + + {% endif %} + +
+ {# Check source of agreement form content #} + {% if agreement_form.type == 'MODEL' %} + {{ agreement_form.content | safe }} + {% else %} + {% get_agreement_form_template agreement_form.form_file_path form_context %} + {% endif %} +
+ + + + +
+ +
+
+ +
+
+ + {% csrf_token %} +
+ +
+
+
+{% endblock %} + +{% block footerscripts %} + +{% endblock %} From 2555be7e90809056a19595e87ad7579ddb9c9aed Mon Sep 17 00:00:00 2001 From: Bryan Larson Date: Thu, 17 Oct 2024 19:59:09 +0000 Subject: [PATCH 4/4] chore(release): 1.2.0-rc.1 [skip ci] # [1.2.0-rc.1](https://github.com/hms-dbmi/hypatio-app/compare/v1.1.2...v1.2.0-rc.1) (2024-10-17) ### Bug Fixes * **requirements:** Updated Python requirements ([8c4e22f](https://github.com/hms-dbmi/hypatio-app/commit/8c4e22ff591fafbd5b13910069cea32355a2de68)) ### Features * **projects:** Data Use Report templates ([23f41c5](https://github.com/hms-dbmi/hypatio-app/commit/23f41c54749cb359da9cc56672b250f5f6f21a5f)) * **projects:** Data Use Reporting implementation ([060f04f](https://github.com/hms-dbmi/hypatio-app/commit/060f04f8209f7ee7cfb96a7b1b4efd47edf36954)) --- CHANGELOG.md | 13 +++++++++++++ pyproject.toml | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20531e58..c15e035f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [1.2.0-rc.1](https://github.com/hms-dbmi/hypatio-app/compare/v1.1.2...v1.2.0-rc.1) (2024-10-17) + + +### Bug Fixes + +* **requirements:** Updated Python requirements ([8c4e22f](https://github.com/hms-dbmi/hypatio-app/commit/8c4e22ff591fafbd5b13910069cea32355a2de68)) + + +### Features + +* **projects:** Data Use Report templates ([23f41c5](https://github.com/hms-dbmi/hypatio-app/commit/23f41c54749cb359da9cc56672b250f5f6f21a5f)) +* **projects:** Data Use Reporting implementation ([060f04f](https://github.com/hms-dbmi/hypatio-app/commit/060f04f8209f7ee7cfb96a7b1b4efd47edf36954)) + ## [1.1.2](https://github.com/hms-dbmi/hypatio-app/compare/v1.1.1...v1.1.2) (2024-09-24) diff --git a/pyproject.toml b/pyproject.toml index 13e436ef..c06f40f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dbmi-data-portal" -version = "1.1.2" +version = "1.2.0-rc.1" description = "A portal for hosting and managing access to DBMI-provided datasets" readme = "README.md" requires-python = ">=3.9"