Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds data upload and sharing agreement template to the project files page. #1698

Open
wants to merge 32 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c273168
adds dua template to project files page.
lamawmouk Nov 9, 2022
af827a8
adds model to store responses from dua before project submission
lamawmouk Nov 9, 2022
d5f9db6
adds migration to apply changes to the model into the database schema.
lamawmouk Nov 9, 2022
b08f105
fixes styling
lamawmouk Nov 10, 2022
956827e
initial data upload agreement
lamawmouk Nov 15, 2022
3d4ef1a
add the data upload agreement to projects
lamawmouk Nov 29, 2022
5200a8a
adding demo for fixtures to datauploadagreement
lamawmouk Nov 29, 2022
a5d99df
fixed the data upload button
lamawmouk Dec 2, 2022
d224ae9
adds the data upload agreement for a new active project
lamawmouk Dec 6, 2022
b3e60eb
restructures project file with data uploadagreement at top
lamawmouk Dec 6, 2022
0daf6d3
fixes styling issues for changed files
Dec 14, 2022
de95983
removes the except in dua
Dec 14, 2022
451cbc2
fix style checks
Dec 14, 2022
c703060
adds except error explicitly
Dec 14, 2022
e705677
fixes exception error and response variable status
Dec 14, 2022
b1d6155
adds status assertion for another response variable
Dec 14, 2022
cba93f9
adjusts status code to redirect 302
Dec 14, 2022
bbad8ca
fix style errors
danamouk Dec 14, 2022
50b7916
fixes styling
danamouk Dec 14, 2022
6411c2a
fixes styling
lamawmouk Dec 14, 2022
b5af640
add the dua agreement icon for submission
lamawmouk Dec 14, 2022
5e3e8eb
updates the data upload agreement text in project files
lamawmouk Dec 14, 2022
8133f65
update the migration file
lamawmouk Mar 16, 2023
ddecae0
adds a boostrap to the agreement form
lamawmouk Mar 23, 2023
8ff4211
populates the form after submission
lamawmouk Mar 23, 2023
852148d
updates the html to include site name
lamawmouk Mar 23, 2023
932f071
updated views to disable project file upload
lamawmouk Mar 23, 2023
8871f94
adds an error upon submitting the project if PHI is present
lamawmouk Apr 13, 2023
509977a
updates the warning message after form is submitted with has PHI as yes
lamawmouk Apr 13, 2023
624eb3b
prevents file upload if the project has PHI
lamawmouk Apr 13, 2023
64ad824
fixes styling and wording
lamawmouk Apr 13, 2023
d9405bf
updates the fixtures to fail test if has_phi is yes
lamawmouk Apr 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions physionet-django/console/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,25 @@ def test_assign_editor(self):
response = self.client.post(reverse(
'submitted_projects'), data={'project':project.id,
'editor':editor.id})
self.assertEqual(response.status_code, 200)
project = ActiveProject.objects.get(title='MIT-BIH Arrhythmia Database')
self.assertTrue(project.editor, editor)
self.assertEqual(project.submission_status, 20)


def test_reassign_editor(self):
"""
Assign an editor, then reassign it
"""
# Submit project
project = ActiveProject.objects.get(title='MIT-BIH Arrhythmia Database')
project.submit(author_comments='')
# Assign editor
# Assign editorgi
self.client.login(username='admin', password='Tester11!')
editor = User.objects.get(username='cindyehlert')
response = self.client.post(reverse('submitted_projects'), data={
'project': project.id, 'editor': editor.id})
self.assertEqual(response.status_code, 200)
project = ActiveProject.objects.get(title='MIT-BIH Arrhythmia Database')
self.assertTrue(project.editor, editor)
self.assertEqual(project.submission_status, 20)
Expand All @@ -77,9 +80,11 @@ def test_reassign_editor(self):
editor = User.objects.get(username='amitupreti')
response = self.client.post(reverse('submission_info',
args=(project.slug,)), data={'editor': editor.id})
self.assertEqual(response.status_code, 200)
project = ActiveProject.objects.get(title='MIT-BIH Arrhythmia Database')
self.assertTrue(project.editor, editor)


def test_edit_reject(self):
"""
Edit a project, rejecting it.
Expand Down Expand Up @@ -212,8 +217,8 @@ def test_copyedit(self):
# Recomplete copyedit
response = self.client.post(reverse(
'copyedit_submission', args=(project.slug,)),
data={'complete_copyedit':'', 'made_changes':1,
'changelog_summary':'Removed your things'})
data={'complete_copyedit': '', 'made_changes': 1,
'changelog_summary': 'Removed your things'})
project = ActiveProject.objects.get(id=project.id)
self.assertFalse(project.copyeditable())

Expand Down Expand Up @@ -261,15 +266,15 @@ def get_project():
# Complete copyedit
response = self.client.post(reverse(
'copyedit_submission', args=(project.slug,)),
data={'complete_copyedit':'', 'made_changes':0})
data={'complete_copyedit': '', 'made_changes': 0})
self.assertEqual(get_project().modified_datetime, timestamp)

# Approve publication
self.assertFalse(ActiveProject.objects.get(id=project.id).is_publishable())
self.client.login(username='rgmark', password='Tester11!')
response = self.client.post(reverse(
'project_submission', args=(project.slug,)),
data={'approve_publication':''})
data={'approve_publication': ''})
self.assertEqual(get_project().modified_datetime, timestamp)

self.assertTrue(ActiveProject.objects.get(id=project.id).is_publishable())
Expand Down Expand Up @@ -300,16 +305,14 @@ def test_publish(self):
# publish_submission ignores the slug parameter)
if not project.is_new_version:
taken_slug = PublishedProject.objects.all().first().slug
response = self.client.post(reverse(
'publish_submission', args=(project.slug,)),
data={'slug':taken_slug, 'doi': False, 'make_zip':1})
self.assertTrue(bool(ActiveProject.objects.filter(
slug=project_slug)))
response = self.client.post(reverse('publish_submission', args=(project.slug,)),
data={'slug': taken_slug, 'doi': False, 'make_zip': 1})
self.assertTrue(bool(ActiveProject.objects.filter(slug=project_slug)))

# Publish with a valid custom slug
response = self.client.post(reverse(
'publish_submission', args=(project.slug,)),
data={'slug':custom_slug, 'doi': False, 'make_zip':1})
response = self.client.post(reverse('publish_submission',
args=(project.slug,)), data={
'slug': custom_slug, 'doi': False, 'make_zip': 1})

# Run background tasks
self.assertTrue(bool(tasks.run_next_task()))
Expand All @@ -321,8 +324,7 @@ def test_publish(self):
project = PublishedProject.objects.get(slug=custom_slug,
version=project.version)
# Access the published project's page and its (open) files
response = self.client.get(reverse('published_project',
args=(project.slug, project.version)))
response = self.client.get(reverse('published_project', args=(project.slug, project.version)))
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('serve_published_project_file', args=(
project.slug, project.version, 'subject-100/100.atr')))
Expand All @@ -332,8 +334,7 @@ def test_publish(self):
self.assertEqual(response.status_code, 200)
# Access the submission log as the author
self.client.login(username='rgmark', password='Tester11!')
response = self.client.get(reverse('published_submission_history',
args=(project.slug, project.version,)))
response = self.client.get(reverse('published_submission_history', args=(project.slug, project.version,)))
self.assertEqual(response.status_code, 200)

# The internal links should now point to published files
Expand Down Expand Up @@ -382,11 +383,16 @@ def test_publish_with_versions(self):
for version in versions[1:]:
self.client.login(username=self.AUTHOR,
password=self.AUTHOR_PASSWORD)
response = self.client.post(
reverse('new_project_version', args=(self.PROJECT_SLUG,)),
data={'version': version})
response = self.client.post(reverse('new_project_version', args=(self.PROJECT_SLUG,)),
data={'version': version})
self.assertEqual(response.status_code, 302)
project = ActiveProject.objects.get(title='MIT-BIH Arrhythmia Database')
self.client.post(reverse('project_files', args=(project.slug,)), data={
'has_copy_right_permission': '0', 'has_human_subject_data': '0',
'has_phi': '0', 'submit_upload_agreement': 'submitbutton'})
self.test_publish()


# Sort the list of version numbers
sorted_versions = []
for version in versions:
Expand Down
11 changes: 11 additions & 0 deletions physionet-django/project/fixtures/demo-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -2249,5 +2249,16 @@
"responder": null,
"responder_comments": ""
}
},
{
"model": "project.datauploadagreement",
"pk": 1,
"fields": {
"project": 1,
"has_copy_right_permission":0,
"has_human_subject_data":0,
"has_phi":0
}
}
]

20 changes: 20 additions & 0 deletions physionet-django/project/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
Topic,
exists_project_slug,
UploadedDocument,
DataUploadAgreement
)
from project.projectfiles import ProjectFiles
from user.models import User, TrainingType
Expand Down Expand Up @@ -1175,3 +1176,22 @@ def __init__(self, *args, **kwargs):
"Statements on ethics approval should appear here. "
"Your statement will be included in the public project description."
)


class UploadedAgreementDataForm(forms.ModelForm):
class Meta:
model = DataUploadAgreement
fields = (
'has_copy_right_permission',
'has_human_subject_data',
'has_phi')
labels = {
'has_copy_right_permission': 'Are you the original creator, or copyright holder, or have\
permission from the creators and copyright holders to share these files?',
'has_human_subject_data': 'Does this project contain data collected from human subjects?',
'has_phi': 'Do these files contain any personally identifiable information (information\
that could identify individual human subjects)?'}

def __init__(self, project, *args, **kwargs):
super().__init__(*args, **kwargs)
self.project = project
25 changes: 25 additions & 0 deletions physionet-django/project/migrations/0068_datauploadagreement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 3.1.14 on 2022-11-29 18:54

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('project', '0067_alter_activeproject_core_project_and_more'),
]

operations = [
migrations.CreateModel(
name='DataUploadAgreement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('has_copy_right_permission', models.PositiveSmallIntegerField(choices=[(0, 'Yes'), (1, 'No')])),
('has_human_subject_data', models.PositiveSmallIntegerField(choices=[(0, 'Yes'), (1, 'No')])),
('has_phi', models.PositiveSmallIntegerField(choices=[(0, 'Yes'), (1, 'No'), (2, 'NA')])),
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE,
to='project.activeproject')),
],
),
]
10 changes: 9 additions & 1 deletion physionet-django/project/modelcomponents/activeproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
UploadedDocument,
)
from project.modelcomponents.publishedproject import PublishedProject
from project.modelcomponents.submission import CopyeditLog, EditLog, SubmissionInfo
from project.modelcomponents.submission import CopyeditLog, EditLog, SubmissionInfo, DataUploadAgreement
from project.modelcomponents.unpublishedproject import UnpublishedProject
from project.projectfiles import ProjectFiles
from project.validators import validate_subdir
Expand Down Expand Up @@ -329,6 +329,14 @@ def check_integrity(self):
f'Corresponding author {author.user.username} '
'has not set a corresponding email')

# Data Upload Agreement
try:
data_upload = DataUploadAgreement.objects.get(project_id=self.id)
if data_upload.has_phi == 0:
self.integrity_errors.append('Please remove any Personal Health Information from your data')
except DataUploadAgreement.DoesNotExist:
self.integrity_errors.append('Missing required field: Data Upload Agreement')

# Metadata
for attr in ActiveProject.REQUIRED_FIELDS[self.resource_type.id]:
value = getattr(self, attr)
Expand Down
18 changes: 18 additions & 0 deletions physionet-django/project/modelcomponents/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,21 @@ def quota_manager(self):
creation_time=self.creation_datetime)
quota_manager.set_limits(bytes_hard=limit, bytes_soft=limit)
return quota_manager


class DataUploadAgreement(models.Model):
"""This model is used to store the responses from the data use agreement for the project."""
RESPONSE_CHOICES_1_and_2 = (
(0, 'Yes'),
(1, 'No'),
)
RESPONSE_CHOICES_3 = (
(0, 'Yes'),
(1, 'No'),
(2, 'NA'),
)

project = models.OneToOneField('project.ActiveProject', on_delete=models.CASCADE)
has_copy_right_permission = models.PositiveSmallIntegerField(choices=RESPONSE_CHOICES_1_and_2)
has_human_subject_data = models.PositiveSmallIntegerField(choices=RESPONSE_CHOICES_1_and_2)
has_phi = models.PositiveSmallIntegerField(choices=RESPONSE_CHOICES_3)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
{% block main_content %}
<h2 class="form-signin-heading">6. Project Files</h2>
<hr>
{% include "project/upload_data_agreement.html" %}
{% include "about/files.html" %}
<br>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<h4> Data Upload and Sharing Agreement</h4>
{% if not project.datauploadagreement %}
<div class="alert alert-warning">
Please complete the project data upload agreement form below before uploading the project files.
</div>
{% elif project.datauploadagreement.has_phi == 0 %}
<div class="alert alert-warning">
You are unable to submit your project for review, because you have not removed Personal Health Information from your project files.
</div>
{% else %}
<div class="alert alert-info">
Please don't include any Personal Health Information within your uploaded project files.
</div>
{% endif %}
<button id="upload-agreement-button" type="button" class="btn btn-primary" data-toggle="modal" data-target="#upload-agreement-modal">
<i class="fa fa-handshake"></i> Data Upload Agreement
</button>
<p></p>
<div class="modal fade" id="upload-agreement-modal" tabindex="-1" role="dialog" aria-labelledby="upload-agreement-modal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Data Upload Agreement</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form action="" method="post" enctype="multipart/form-data">
<div class="modal-body">
{% csrf_token %}
<p>Your responses to the following questions will be shared with the editor, but will not be published.</p>
{{upload_agreement_form}}
<p>By clicking “I agree” and uploading files to {{SITE_NAME}}, you agree that the above information is accurate.</p>
</div>
<div class="modal-footer">
<button class="btn btn-primary" name="submit_upload_agreement" type="submit"><i class="fa fa-thumbs-up"></i> I agree</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
26 changes: 24 additions & 2 deletions physionet-django/project/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
StorageRequest,
Topic,
UploadedDocument,
DataUploadAgreement
)
from project.projectfiles import ProjectFiles
from project.validators import validate_filename, validate_gcs_bucket_object
Expand Down Expand Up @@ -1036,6 +1037,19 @@ def project_files(request, project_slug, subdir='', **kwargs):
if storage_request:
storage_request.get().delete()
messages.success(request, 'Your storage request has been cancelled.')
elif 'submit_upload_agreement' in request.POST:
try:
agreement = DataUploadAgreement.objects.get(project=project)
except DataUploadAgreement.DoesNotExist:
agreement = None
upload_agreement_form = forms.UploadedAgreementDataForm(project=project,
data=request.POST, instance=agreement)
if upload_agreement_form.is_valid():
upload_agreement_form.instance.project = project
upload_agreement_form.save()
messages.success(request, 'Your upload agreement has been received.')
else:
messages.error(request, utility.get_form_errors(upload_agreement_form))
else:
# process the file manipulation post
subdir = process_files_post(request, project)
Expand All @@ -1048,8 +1062,7 @@ def project_files(request, project_slug, subdir='', **kwargs):
if settings.SYSTEM_MAINTENANCE_NO_UPLOAD:
maintenance_message = settings.SYSTEM_MAINTENANCE_MESSAGE or (
"The site is currently undergoing maintenance, and project "
"files cannot be edited. Please try again later."
)
"files cannot be edited. Please try again later.")
files_editable = False
else:
maintenance_message = None
Expand All @@ -1061,6 +1074,14 @@ def project_files(request, project_slug, subdir='', **kwargs):
storage_request_form = (
forms.StorageRequestForm(project=project) if (not storage_request and is_submitting) else None
)
try:
agreement = DataUploadAgreement.objects.get(project=project)
if agreement.has_phi == 0:
files_editable = False
except DataUploadAgreement.DoesNotExist:
agreement = None
files_editable = False
upload_agreement_form = forms.UploadedAgreementDataForm(project=project, instance=agreement)

(display_files, display_dirs, dir_breadcrumbs, parent_dir,
file_error) = get_project_file_info(project=project, subdir=subdir)
Expand Down Expand Up @@ -1096,6 +1117,7 @@ def project_files(request, project_slug, subdir='', **kwargs):
'maintenance_message': maintenance_message,
'is_lightwave_supported': ProjectFiles().is_lightwave_supported(),
'storage_type': settings.STORAGE_TYPE,
'upload_agreement_form': upload_agreement_form,
},
)

Expand Down