def notify_supervisors_of_task_submission(project, participant, task, submission_info_json): """ Sends an email notification to supervisors of a data project when someone has submitted a task. """ # Convert the comma separated string of emails into a list. supervisor_emails = project.project_supervisors.split(",") # The email subject. subject = 'DBMI Portal - A {project} solution was submitted.'.format( project=project.project_key) context = { 'submission_info': submission_info_json, 'project': project, 'task': task, 'submitter': participant.user.email, 'site_url': settings.SITE_URL } try: email_success = email_send( subject=subject, recipients=supervisor_emails, email_template='email_submission_uploaded_to_supervisors', extra=context) except Exception as e: logger.exception(e)
def notify_task_submitters(project, participant, task, submission_info_json): """ Sends an email notification to individuals involved in a submission, which may be a single person or a group of people under a team. """ # Send an email notification to the team or individual who submitted. if participant.team is not None: # Get the submissions for this task already submitted by the team. total_submissions = ChallengeTaskSubmission.objects.filter( challenge_task=task, participant__in=participant.team.participant_set.all(), deleted=False).count() # Send an email notification to team members about the submission. emails = [ member.user.email for member in participant.team.participant_set.all() ] # The email subject. subject = 'DBMI Portal - {project} solution submitted by your team'.format( project=project.project_key) else: # Get the submissions for this task already submitted by the team. total_submissions = ChallengeTaskSubmission.objects.filter( challenge_task=task, participant=participant, deleted=False).count() # Send an email notification to team members about the submission. emails = [participant.user.email] # The email subject. subject = 'DBMI Portal - {project} solution submitted.'.format( project=project.project_key) context = { 'submission_info': submission_info_json, 'project': project, 'task': task, 'submitter': participant.user.email, 'max_submissions': task.max_submissions, 'submission_count': total_submissions } try: email_success = email_send(subject=subject, recipients=emails, email_template='email_submission_uploaded', extra=context) except Exception as e: logger.exception(e)
def submit_user_permission_request(request): """ An HTTP POST endpoint to handle a request by a user that wants to access a project. Should only be used for projects that do not require teams but do require authorization. """ try: project_key = request.POST.get('project_key', None) project = DataProject.objects.get(project_key=project_key) except ObjectDoesNotExist: return HttpResponse(404) if project.has_teams or not project.requires_authorization: return HttpResponse(400) # Create a new participant record if one does not exist already. participant = Participant.objects.get_or_create( user=request.user, project=project ) # Check if there are administrators to notify. if project.project_supervisors is None or project.project_supervisors == "": return HttpResponse(200) # Convert the comma separated string of emails into a list. supervisor_emails = project.project_supervisors.split(",") subject = "DBMI Data Portal - Access requested to dataset" email_context = { 'subject': subject, 'project': project, 'user_email': request.user.email, 'site_url': settings.SITE_URL } try: email_success = email_send( subject=subject, recipients=supervisor_emails, email_template='email_access_request_notification', extra=email_context ) except Exception as e: logger.exception(e) return HttpResponse(200)
def email_report(self, project_key, recipient, report, *args, **options): """ Sends a report of the results of the operation. """ # Set context context = { "operation": self.help, "project": project_key, "message": report, } # Send it out. success = email_send(subject=f'DBMI Portal - Operation Report', recipients=[recipient], email_template='email_operation_report', extra=context) if success: self.stdout.write( self.style.SUCCESS(f"Report sent to: {recipient}")) else: self.stdout.write( self.style.ERROR(f"Report failed to send to: {recipient}"))
def finalize_team(request): """ An HTTP POST endpoint for team leaders to mark their team as ready and send an email notification to contest managers. """ project_key = request.POST.get("project_key") team = request.POST.get("team") project = DataProject.objects.get(project_key=project_key) team = Team.objects.get(team_leader__email=team, data_project=project) if request.user.email != team.team_leader.email: logger.debug( "User {email} is not a team leader.".format( email=request.user.email ) ) return HttpResponse("Error: permissions.", status=403) team.status = 'Ready' team.save() # Convert the comma separated string of emails into a list. supervisor_emails = project.project_supervisors.split(",") context = {'team_leader': team, 'project': project_key, 'site_url': settings.SITE_URL} email_success = email_send( subject='DBMI Portal - Finalized Team', recipients=supervisor_emails, email_template='email_finalized_team_notification', extra=context ) return HttpResponse(200)
def export_task_submissions(project_id, requester): """ This method fetches all current submissions for a challenge task and prepares an export of all files along with associated metadata. :param project_id: The ID of the DataProject to export submissions for :type project_id: str :param requester: The email of the admin requesting the export :type requester: str :return: Whether the operation succeeded or not :rtype: bool """ try: # Get project and challenge task project = DataProject.objects.get(id=project_id) # Get all submissions made by this team for this project. submissions = ChallengeTaskSubmission.objects.filter( challenge_task__in=project.challengetask_set.all(), deleted=False) # Create a temporary directory export_uuid = export_location = None with tempfile.TemporaryDirectory() as directory: # Create directory to put submissions in submissions_directory_path = os.path.join( directory, f"{project.project_key}_submissions_{datetime.now().isoformat()}" ) # For each submission, create a directory for the file and its metadata file submission_file_response = None for submission in submissions: try: # Set the name of the containing directory submission_directory_path = os.path.join( submissions_directory_path, f"{submission.participant.user.email}_{submission.uuid}" ) # Create a record of the user downloading the file. ChallengeTaskSubmissionDownload.objects.create( user=User.objects.get(email=requester), submission=submission) # Create a temporary directory to hold the files specific to this submission that need to be zipped together. if not os.path.exists(submission_directory_path): os.makedirs(submission_directory_path) # Create a json file with the submission info string. info_file_name = "submission_info.json" with open(os.path.join(submission_directory_path, info_file_name), mode="w") as f: f.write(submission.submission_info) # Determine filename try: submission_file_name = json.loads( submission.submission_info).get("filename") if not submission_file_name: # Check fileservice submission_file_name = fileservice.get_archivefile( submission.uuid)["filename"] except Exception as e: logger.exception( f"Could not determine filename for submission", exc_info=True, extra={ "submission": submission, "archivefile_uuid": submission.uuid, "submission_info": submission.submission_info, }) # Use a default filename submission_file_name = "submission_file.zip" # Get the submission file's byte contents from S3. submission_file_download_url = fileservice.get_archivefile_proxy_url( uuid=submission.uuid) headers = { "Authorization": f"{settings.FILESERVICE_AUTH_HEADER_PREFIX} {settings.FILESERVICE_SERVICE_TOKEN}" } with requests.get(submission_file_download_url, headers=headers, stream=True) as submission_file_response: submission_file_response.raise_for_status() # Write the submission file's bytes to a zip file. with open(os.path.join(submission_directory_path, submission_file_name), mode="wb") as f: shutil.copyfileobj(submission_file_response.raw, f) except requests.exceptions.HTTPError as e: logger.exception( f"{project.project_key}: Could not download submission '{submission.uuid}': {e}", extra={ "submission": submission, "archivefile_uuid": submission.uuid, "response": submission_file_response.content, "status_code": submission_file_response.status_code }) except Exception as e: logger.exception( f"{project.project_key}: Could not export submission '{submission.uuid}': {e}", exc_info=True) # Set the archive name archive_basename = f"{project.project_key}_submissions" # Archive the directory archive_path = shutil.make_archive(archive_basename, "zip", submissions_directory_path) # Perform the request to upload the file with open(archive_path, "rb") as file: # Build upload request response = None files = {"file": file} try: # Create the file in Fileservice metadata = { "project": project.project_key, "type": "export", } tags = [ "hypatio", "export", "submissions", project.project_key, requester ] export_uuid, upload_data = fileservice.create_archivefile_upload( os.path.basename(archive_path), metadata, tags) # Get the location export_location = upload_data["locationid"] # Upload to S3 response = requests.post( upload_data["post"]["url"], data=upload_data["post"]["fields"], files=files) response.raise_for_status() # Mark the upload as complete fileservice.uploaded_archivefile(export_uuid, export_location) except KeyError as e: logger.error( f'{project.project_key}: Failed export post generation: {upload_data}', exc_info=True) raise e except requests.exceptions.HTTPError as e: logger.exception( f'{project.project_key}: Failed export upload: {upload_data}', extra={ "response": response.content, "status_code": response.status_code }) raise e except Exception as e: logger.exception( f"{project.project_key}: Could not export submissions: {e}", exc_info=True, ) raise e # Create the model entry for the export export = ChallengeTaskSubmissionExport.objects.create( data_project=project, requester=User.objects.get(email=requester), uuid=export_uuid, location=export_location, ) # Set many to many fields export.challenge_tasks.set(project.challengetask_set.all()) export.challenge_task_submissions.set(submissions) export.save() # Notify requester email_send(subject='DBMI Portal - Challenge Task Submissions Export', recipients=[requester], email_template='email_submissions_export_notification', extra={ "site_url": settings.SITE_URL, "project": project }) except Exception as e: logger.exception(f"Export challenge task submissions error: {e}", exc_info=True, extra={ "project_id": project_id, "requester": requester, }) raise e
def delete_challengetasksubmission(request): """ An HTTP POST endpoint that marks a ChallengeTaskSubmission as deleted so it will not be counted against their total submission count for a contest. """ if request.method == 'POST': # Check that user has permissions to be viewing files for this project. user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) submission_uuid = request.POST.get('submission_uuid') submission = ChallengeTaskSubmission.objects.get(uuid=submission_uuid) project = submission.participant.project logger.debug( '[delete_challengetasksubmission] - %s is trying to delete submission %s', request.user.email, submission_uuid ) user_is_submitter = submission.participant.user == request.user user_is_manager = sciauthz.user_has_manage_permission(project.project_key) if project.has_teams: team = submission.participant.team user_is_team_leader = team.team_leader == request.user.email # Check that the user is either the team leader, the original submitter, or a manager. if not user_is_submitter and not user_is_team_leader and not user_is_manager: logger.debug( "[delete_challengetasksubmission] - No Access for user %s", request.user.email ) return HttpResponse("Only the original submitter, team leader, or challenge manager may delete this.", status=403) # Prepare the email notification to send. emails = [member.user.email for member in team.participant_set.all()] subject = 'DBMI Portal - Team Submission Deleted' else: # Check that the user is either the the original submitter or a manager. if not user_is_submitter and not user_is_manager: logger.debug( "[delete_challengetasksubmission] - No Access for user %s", request.user.email ) return HttpResponse("Only the original submitter, team leader, or challenge manager may delete this.", status=403) # Prepare the email notification to send. emails = [submission.participant.user.email] subject = 'DBMI Portal - Submission Deleted' submission.deleted = True submission.save() # Do not give away the admin's email when notifying the team if user_is_manager: deleted_by = "an admin" else: deleted_by = request.user.email # Send an email notification to the team context = { 'deleted_by': deleted_by, 'project': project.project_key, 'submission_uuid': submission_uuid } email_success = email_send( subject=subject, recipients=emails, email_template='email_submission_deleted_notification', extra=context ) return HttpResponse(status=200) return HttpResponse(status=500)
def join_team(request): """ An HTTP POST endpoint for a user to try to join a team by entering the team leader's email. """ project_key = request.POST.get("project_key", None) try: project = DataProject.objects.get(project_key=project_key) except ObjectDoesNotExist: logger.error("[HYPATIO][join_team] User {email} hit the join_team api method without a project key provided.".format( email=request.user.email )) return redirect('/') team_leader = request.POST.get("team_leader", None) logger.debug("[HYPATIO][join_team] User {email} is requesting to join team {team} for project {project}.".format( email=request.user.email, team=team_leader, project=project_key )) try: participant = Participant.objects.get(user=request.user, project=project) except ObjectDoesNotExist: participant = Participant(user=request.user, project=project) participant.save() try: # If this team leader has already created a team, add the person to the team in a pending status team = Team.objects.get(team_leader__email__iexact=team_leader, data_project=project) # Only allow a new participant to join a team that is still in a pending or ready state if team.status in ['Pending', 'Ready']: participant.team = team participant.team_pending = True participant.save() # Otherwise, let them know why they can't join the team else: msg = "The team you are trying to join has already been finalized and is not accepting new members. " + \ "If you would like to join this team, please have the team leader contact the challenge " + \ "administrators for help." messages.error(request, msg) return redirect('/projects/' + request.POST.get('project_key') + '/') except ObjectDoesNotExist: # If this team leader has not yet created a team, mark the person as waiting participant.team_wait_on_leader_email = team_leader participant.team_wait_on_leader = True participant.save() team = None # Send email to team leader informing them of a pending member if team is not None: context = {'member_email': request.user.email, 'project': project, 'site_url': settings.SITE_URL} email_success = email_send(subject='DBMI Portal - Pending Member', recipients=[team_leader], email_template='email_pending_member_notification', extra=context) # Create record to allow leader access to profile. sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.create_profile_permission(team_leader, project_key) return redirect('/projects/' + request.POST.get('project_key') + '/')
def delete_team(request): """ Delete a team and notify members. """ project_key = request.POST.get("project") team_leader = request.POST.get("team") administrator_message = request.POST.get("administrator_message") logger.debug( '[HYPATIO][DEBUG][delete_team] User {email} is deleting team {team} for project {project_key}.' .format(email=request.user.email, team=team_leader, project_key=project_key)) project = DataProject.objects.get(project_key=project_key) user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: logger.debug( '[HYPATIO][DEBUG][delete_team] User {email} does not have MANAGE permissions for item {project_key}.' .format(email=user.email, project_key=project.project_key)) return HttpResponse("Error: permissions.", status=403) team = Team.objects.get(team_leader__email=team_leader, data_project=project) logger.debug( '[HYPATIO][delete_team] Removing all VIEW permissions for team members.' ) # First revoke all VIEW permissions for member in team.participant_set.all(): sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission(project_key, member.user.email) # Remove permission from Participant member.permission = None member.save() logger.debug( '[HYPATIO][delete_team] Sending a notification to team members.') # Then send a notification to the team members context = { 'administrator_message': administrator_message, 'project': project, 'site_url': settings.SITE_URL } emails = [member.user.email for member in team.participant_set.all()] email_success = email_send( subject='DBMI Portal - Team Deleted', recipients=emails, email_template='email_team_deleted_notification', extra=context) logger.debug('[HYPATIO][delete_team] Deleting the team from the database.') # Then delete the team team.delete() logger.debug('[HYPATIO][delete_team] Team ' + team_leader + ' for project ' + project_key + ' successfully deleted.') return HttpResponse(200)
def set_team_status(request): """ An HTTP POST endpoint to set a team's status, assign the correct permissions, and notify team members. """ project_key = request.POST.get("project") team_leader = request.POST.get("team") status = request.POST.get("status") project = DataProject.objects.get(project_key=project_key) user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) logger.debug( '[HYPATIO][DEBUG][set_team_status] User {email} is attempting to set team {team} to status of {status} for project {project_key}.' .format(email=request.user.email, team=team_leader, status=status, project_key=project.project_key)) if not is_manager: logger.debug( '[HYPATIO][DEBUG][set_team_status] User {email} does not have MANAGE permissions for item {project_key}.' .format(email=user.email, project_key=project.project_key)) return HttpResponse("Error: permissions.", status=403) team = Team.objects.get(team_leader__email=team_leader, data_project=project) # First change the team's status if status == "pending": team.status = "Pending" team.save() elif status == "ready": team.status = "Ready" team.save() elif status == "active": team.status = "Active" team.save() elif status == "deactivated": team.status = "Deactivated" team.save() else: logger.debug('[HYPATIO][set_team_status] Given status "' + status + '" not one of allowed statuses.') return HttpResponse(500) # If setting to Active, grant each team member access permissions. if status == "active": for member in team.participant_set.all(): sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.create_view_permission(project_key, member.user.email) # Add permission to Participant member.permission = 'VIEW' member.save() # If setting to Deactivated, revoke each team member's permissions. elif status == "deactivated": for member in team.participant_set.all(): sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission(project_key, member.user.email) # Remove permission from Participant member.permission = None member.save() # Send an email notification to the team context = { 'status': status, 'project': project, 'site_url': settings.SITE_URL } # Email list emails = [member.user.email for member in team.participant_set.all()] email_success = email_send( subject='DBMI Portal - Team Status Changed', recipients=emails, email_template='email_new_team_status_notification', extra=context) return HttpResponse(200)
def change_signed_form_status(request): """ An HTTP POST endpoint for changing a signed form's status and notifying the user. """ status = request.POST.get("status") form_id = request.POST.get("form_id") administrator_message = request.POST.get("administrator_message") logger.debug('[HYPATIO][change_signed_form_status] ' + request.user.email + ' try to change status for signed form ' + form_id + ' to ' + status + '.') signed_form = get_object_or_404(SignedAgreementForm, id=form_id) affected_user = signed_form.user user = request.user user_jwt = request.COOKIES.get("DBMI_JWT", None) project = signed_form.project sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, user.email) is_manager = sciauthz.user_has_manage_permission(project.project_key) if not is_manager: logger.debug( '[HYPATIO][DEBUG][change_signed_form_status] User {email} does not have MANAGE permissions for item {project_key}.' .format(email=user.email, project_key=project.project_key)) return HttpResponse("Error: permissions.", status=403) # First change the team's status if status == "approved": signed_form.status = 'A' signed_form.save() elif status == "rejected": signed_form.status = 'R' signed_form.save() logger.debug( '[HYPATIO][change_signed_form_status] Emailing a rejection notification to the affected participant' ) # Send an email notification to the affected person context = { 'signed_form': signed_form, 'administrator_message': administrator_message, 'site_url': settings.SITE_URL } email_success = email_send( subject='DBMI Portal - Signed Form Rejected', recipients=[affected_user.email], email_template='email_signed_form_rejection_notification', extra=context) # If the user is a participant on a team, then the team status may need to be changed try: participant = Participant.objects.get(user=affected_user, project=signed_form.project) team = participant.team except ObjectDoesNotExist: participant = None team = None # If the team is in an Active status, move the team status down to Ready and remove everyone's VIEW permissions if team is not None and team.status == "Active": team.status = "Ready" team.save() for member in team.participant_set.all(): sciauthz = SciAuthZ(settings.AUTHZ_BASE, request.COOKIES.get("DBMI_JWT", None), request.user.email) sciauthz.remove_view_permission( signed_form.project.project_key, member.user.email) # Remove their VIEW permission member.permission = None member.save() logger.debug( '[HYPATIO][change_signed_form_status] Emailing the whole team that their status has been moved to Ready because someone has a pending form' ) # Send an email notification to the team context = { 'status': "ready", 'reason': 'Your team has been temporarily disabled because of an issue with a team members\' forms. Challenge administrators will resolve this shortly.', 'project': signed_form.project, 'site_url': settings.SITE_URL } # Email list emails = [ member.user.email for member in team.participant_set.all() ] email_success = email_send( subject='DBMI Portal - Team Status Changed', recipients=emails, email_template='email_new_team_status_notification', extra=context) else: logger.debug('[HYPATIO][change_signed_form_status] Given status "' + status + '" not one of allowed statuses.') return HttpResponse(500) return HttpResponse(200)
def grant_view_permission(request, project_key, user_email): """ Grants a user VIEW permissions to a project in DBMI-AuthZ. """ user = get_object_or_404(User, email=user_email) project = get_object_or_404(DataProject, project_key=project_key) # Check Permissions in SciAuthZ user_jwt = request.COOKIES.get("DBMI_JWT", None) sciauthz = SciAuthZ(settings.AUTHZ_BASE, user_jwt, request.user.email) is_manager = sciauthz.user_has_manage_permission(project_key) logger.debug( '[HYPATIO][DEBUG][grant_view_permission] User {user} is attempting to grant VIEW permissions to participant {participant} for project {project_key}.' .format(user=request.user.email, participant=user_email, project_key=project_key)) if not is_manager: logger.error( '[HYPATIO][DEBUG][grant_view_permission] User {user} does not have manage permissions to project {project_key}.' .format(user=request.user.email, project_key=project_key)) return HttpResponse("Access denied.", status=403) sciauthz.create_view_permission(project_key, user_email) # Add permission to their Participant row try: participant = project.participant_set.get(user__email=user_email) participant.permission = 'VIEW' participant.save() except Exception as e: logger.exception( '[HYPATIO][DEBUG][grant_view_permission] User {user} could not have permission added to project {project_key}: {e}' .format( user=request.user.email, project_key=project_key, e=e, ), exc_info=True, extra={ 'manager': request.user.email, 'participant': user_email, 'project': project_key }) subject = "DBMI Data Portal - Access granted to dataset" email_context = { 'subject': subject, 'project': project, 'site_url': settings.SITE_URL } try: email_success = email_send( subject=subject, recipients=[user_email], email_template='email_access_granted_notification', extra=email_context) except Exception as e: logger.exception(e) return HttpResponse("Access granted")