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

+
+ + +
+
+ + +
+ +
+
+
+ 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 %}