diff --git a/physionet-django/console/test_views.py b/physionet-django/console/test_views.py index cdbe89679c..c674656366 100644 --- a/physionet-django/console/test_views.py +++ b/physionet-django/console/test_views.py @@ -53,10 +53,12 @@ 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 @@ -64,11 +66,12 @@ def test_reassign_editor(self): # 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) @@ -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. @@ -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()) @@ -261,7 +266,7 @@ 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 @@ -269,7 +274,7 @@ def get_project(): 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()) @@ -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())) @@ -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'))) @@ -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 @@ -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: diff --git a/physionet-django/project/fixtures/demo-project.json b/physionet-django/project/fixtures/demo-project.json index dd9eee62bd..0220330ca1 100644 --- a/physionet-django/project/fixtures/demo-project.json +++ b/physionet-django/project/fixtures/demo-project.json @@ -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 + } } ] + diff --git a/physionet-django/project/forms.py b/physionet-django/project/forms.py index 95261eff23..3243bb48d4 100644 --- a/physionet-django/project/forms.py +++ b/physionet-django/project/forms.py @@ -39,6 +39,7 @@ Topic, exists_project_slug, UploadedDocument, + DataUploadAgreement ) from project.projectfiles import ProjectFiles from user.models import User, TrainingType @@ -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 diff --git a/physionet-django/project/migrations/0068_datauploadagreement.py b/physionet-django/project/migrations/0068_datauploadagreement.py new file mode 100644 index 0000000000..a4e23f1752 --- /dev/null +++ b/physionet-django/project/migrations/0068_datauploadagreement.py @@ -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')), + ], + ), + ] diff --git a/physionet-django/project/modelcomponents/activeproject.py b/physionet-django/project/modelcomponents/activeproject.py index 2ece76d93c..e3a287e65a 100644 --- a/physionet-django/project/modelcomponents/activeproject.py +++ b/physionet-django/project/modelcomponents/activeproject.py @@ -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 @@ -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) diff --git a/physionet-django/project/modelcomponents/submission.py b/physionet-django/project/modelcomponents/submission.py index f18cf1387e..b27ef68f2c 100644 --- a/physionet-django/project/modelcomponents/submission.py +++ b/physionet-django/project/modelcomponents/submission.py @@ -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) diff --git a/physionet-django/project/templates/project/project_files.html b/physionet-django/project/templates/project/project_files.html index 61b8565a18..98dbb4884f 100644 --- a/physionet-django/project/templates/project/project_files.html +++ b/physionet-django/project/templates/project/project_files.html @@ -19,6 +19,7 @@ {% block main_content %}