Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
674df52
Add upload agreement path.
tompollard Aug 29, 2025
b9768a5
Update preparation checklist with missing steps.
tompollard Aug 29, 2025
5b28713
import uploadagreement module.
tompollard Aug 29, 2025
d3d3ce6
Add uploadagreement model.
tompollard Aug 29, 2025
be06565
Add uploadagreement model to admin tool.
tompollard Aug 29, 2025
474c273
Add uploadagreement form.
tompollard Aug 29, 2025
f80a895
Add upload agreement page in submission workflow.
tompollard Aug 29, 2025
41b10d5
Explain that the upload agreement must be signed before uploading files.
tompollard Aug 29, 2025
6b58215
Bump proofread page from 7 to 8 in submission sequence.
tompollard Aug 29, 2025
5aa1e71
Disable file upload until upload agreement is signed.
tompollard Aug 29, 2025
71c2f3c
Add upload agreement to submission process.
tompollard Aug 29, 2025
00b7930
Use editable flag.
tompollard Aug 29, 2025
7ce5ebd
Add upload agreement view.
tompollard Aug 29, 2025
9c84ce8
Fix circular uploads.
tompollard Aug 29, 2025
67f106d
Add upload agreement.
tompollard Aug 29, 2025
7bf3c69
Fix style issues.
tompollard Aug 29, 2025
ad30545
Add migration. Adds UploadAgreement.
tompollard Aug 29, 2025
51db8df
Remove hardcoded site name.
tompollard Aug 29, 2025
58739bc
Fix tests
tompollard Aug 29, 2025
65a7403
Remove duplicate import.
tompollard Sep 2, 2025
53085cd
Require a completed upload agreement in order to submit.
tompollard Sep 2, 2025
f48cf2d
If a project is reassigned to a different submitting author, the uplo…
tompollard Sep 2, 2025
fe80d29
Migration: Record user who accepted the agreement.
tompollard Sep 2, 2025
1ae0fe3
Add upload agreement to fixtures.
tompollard Sep 4, 2025
acb9342
Add upload agreement for new project versions.
tompollard Sep 4, 2025
8d3236b
Simplify save method.
tompollard Sep 9, 2025
e4ff3cc
Remove redundant code.
tompollard Sep 9, 2025
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
13 changes: 12 additions & 1 deletion physionet-django/console/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
PublishedProject,
StorageRequest,
SubmissionStatus,
UploadAgreement
)
from user.models import User
from physionet.models import FrontPageButton, StaticPage
Expand Down Expand Up @@ -390,7 +391,6 @@ def get_project():
project.refresh_from_db()
self.assertTrue(project.is_publishable())


def test_publish(self):
"""
Test publishing project
Expand Down Expand Up @@ -502,6 +502,17 @@ def test_publish_with_versions(self):
response = self.client.post(
reverse('new_project_version', args=(self.PROJECT_SLUG,)),
data={'version': version})

# Create upload agreement for the new project version
new_project = ActiveProject.objects.get(title=self.PROJECT_TITLE, version=version)
submitting_author = new_project.authors.get(is_submitting=True)
UploadAgreement.objects.create(
project=new_project,
accepted_by=submitting_author.user,
accepted=True,
no_human_subjects=True
)

self.test_publish()

# Sort the list of version numbers
Expand Down
2 changes: 1 addition & 1 deletion physionet-django/notification/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.db import models

from project.models import SafeHTMLField
from project.modelcomponents.fields import SafeHTMLField


class News(models.Model):
Expand Down
2 changes: 1 addition & 1 deletion physionet-django/physionet/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.db import models
from django.db import transaction
from project.models import SafeHTMLField
from project.modelcomponents.fields import SafeHTMLField
from storages.backends.gcloud import GoogleCloudStorage


Expand Down
25 changes: 25 additions & 0 deletions physionet-django/project/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,30 @@ def get_queryset(self, request):
return super().get_queryset(request).select_related('user').prefetch_related('project')


class UploadAgreementAdmin(admin.ModelAdmin):
list_display = ('project', 'accepted', 'accepted_datetime', 'created_datetime')
list_filter = ('accepted', 'no_human_subjects', 'derived_data', 'human_subjects_deidentified')
search_fields = ('project__title', 'project__slug')
readonly_fields = ('created_datetime', 'updated_datetime')

fieldsets = (
('Project Information', {
'fields': ('project',)
}),
('Agreement Status', {
'fields': ('accepted', 'accepted_datetime')
}),
('Data Type Options', {
'fields': ('no_human_subjects', 'derived_data', 'human_subjects_deidentified'),
'description': 'At least one option must be selected.'
}),
('Timestamps', {
'fields': ('created_datetime', 'updated_datetime'),
'classes': ('collapse',)
}),
)


# Unregister the tasks to add the custom tasks to the amdin page
admin.site.unregister(Task)
admin.site.unregister(CompletedTask)
Expand Down Expand Up @@ -115,3 +139,4 @@ def get_queryset(self, request):
# Add the custom tasks to the admin page
admin.site.register(Task, TaskAdmin)
admin.site.register(CompletedTask, CompletedTaskAdmin)
admin.site.register(models.UploadAgreement, UploadAgreementAdmin)
105 changes: 105 additions & 0 deletions physionet-django/project/fixtures/demo-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -2293,5 +2293,110 @@
"object_id": 1,
"document": "ethics/Ethics_Approval_567b029d-9ea6-41b8-b738-bf45675b24ce.txt"
}
},
{
"model": "project.uploadagreement",
"pk": 1,
"fields": {
"project": 1,
"accepted_by": 2,
"accepted": true,
"accepted_datetime": "2018-09-04T17:26:24.820Z",
"no_human_subjects": true,
"derived_data": false,
"human_subjects_deidentified": false,
"created_datetime": "2018-09-04T17:26:24.820Z",
"updated_datetime": "2018-09-04T17:26:24.820Z"
}
},
{
"model": "project.uploadagreement",
"pk": 2,
"fields": {
"project": 2,
"accepted_by": 2,
"accepted": true,
"accepted_datetime": "2018-09-04T17:26:24.820Z",
"no_human_subjects": true,
"derived_data": false,
"human_subjects_deidentified": false,
"created_datetime": "2018-09-04T17:26:24.820Z",
"updated_datetime": "2018-09-04T17:26:24.820Z"
}
},
{
"model": "project.uploadagreement",
"pk": 3,
"fields": {
"project": 4,
"accepted_by": 4,
"accepted": true,
"accepted_datetime": "2018-09-04T17:26:24.820Z",
"no_human_subjects": true,
"derived_data": false,
"human_subjects_deidentified": false,
"created_datetime": "2018-09-04T17:26:24.820Z",
"updated_datetime": "2018-09-04T17:26:24.820Z"
}
},
{
"model": "project.uploadagreement",
"pk": 4,
"fields": {
"project": 5,
"accepted_by": 4,
"accepted": true,
"accepted_datetime": "2018-09-04T17:26:24.820Z",
"no_human_subjects": true,
"derived_data": false,
"human_subjects_deidentified": false,
"created_datetime": "2018-09-04T17:26:24.820Z",
"updated_datetime": "2018-09-04T17:26:24.820Z"
}
},
{
"model": "project.uploadagreement",
"pk": 5,
"fields": {
"project": 7,
"accepted_by": 4,
"accepted": true,
"accepted_datetime": "2018-09-04T17:26:24.820Z",
"no_human_subjects": true,
"derived_data": false,
"human_subjects_deidentified": false,
"created_datetime": "2018-09-04T17:26:24.820Z",
"updated_datetime": "2018-09-04T17:26:24.820Z"
}
},
{
"model": "project.uploadagreement",
"pk": 6,
"fields": {
"project": 8,
"accepted_by": 1,
"accepted": true,
"accepted_datetime": "2018-09-04T17:26:24.820Z",
"no_human_subjects": true,
"derived_data": false,
"human_subjects_deidentified": false,
"created_datetime": "2018-09-04T17:26:24.820Z",
"updated_datetime": "2018-09-04T17:26:24.820Z"
}
},
{
"model": "project.uploadagreement",
"pk": 7,
"fields": {
"project": 9,
"accepted_by": 1,
"accepted": true,
"accepted_datetime": "2018-09-04T17:26:24.820Z",
"no_human_subjects": true,
"derived_data": false,
"human_subjects_deidentified": false,
"created_datetime": "2018-09-04T17:26:24.820Z",
"updated_datetime": "2018-09-04T17:26:24.820Z"
}
}
]
69 changes: 68 additions & 1 deletion 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,
UploadAgreement,
)
from user.models import User, TrainingType
from user.validators import validate_affiliation
Expand Down Expand Up @@ -98,6 +99,10 @@ def transfer(self):
new_author.is_submitting = True
new_author.save()

# Invalidate any existing upload agreements since the submitting author changed
# The new submitting author will need to accept a new agreement
self.project.upload_agreements.filter(accepted=True).update(accepted=False)


class ActiveProjectFilesForm(forms.Form):
"""
Expand Down Expand Up @@ -1163,7 +1168,6 @@ class Meta:
'responder_comments': forms.Textarea(attrs={'rows': 3}),
'status': forms.Select(choices=DataAccessRequest.REJECT_ACCEPT)
}

labels = {
'status': 'Decision',
'responder_comments': 'Comment or Justification'
Expand Down Expand Up @@ -1196,6 +1200,69 @@ def __init__(self, responder, *args, **kwargs):
self.responder = responder


class UploadAgreementForm(forms.ModelForm):
"""
Form for accepting the upload agreement
"""
class Meta:
model = UploadAgreement
fields = (
'no_human_subjects',
'derived_data',
'human_subjects_deidentified',
)
labels = {
'no_human_subjects': (
'This project does not contain any data derived from human subjects.'
),
'derived_data': (
'This project contains data derived from other de-identified datasets '
f'published on {settings.SITE_NAME} or elsewhere.'
),
'human_subjects_deidentified': (
'This project contains data obtained from human subjects, and all '
'personally identifiable information has been removed.'
),
}
help_texts = {
'derived_data': (
'You will need to cite these datasets in your project description, '
'and explain how you created the derived data. Even if you are using '
'data previously published elsewhere, we expect you to take all '
'reasonable steps to ensure the files you are uploading are free of '
'personally identifiable information.'
),
}

def __init__(self, project, user=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.project = project
self.user = user

def clean(self):
cleaned_data = super().clean()

# At least one checkbox must be selected
if not any([
cleaned_data.get('no_human_subjects'),
cleaned_data.get('derived_data'),
cleaned_data.get('human_subjects_deidentified'),
]):
raise forms.ValidationError(
'Please select at least one option that applies to your project.'
)

return cleaned_data

def save(self):
agreement = super().save(commit=False)
agreement.project = self.project
agreement.accepted = True
agreement.accepted_by = self.user
agreement.save()
return agreement


class InviteDataAccessReviewerForm(forms.ModelForm):
reviewer = forms.CharField(widget=forms.TextInput(
attrs={'class': 'form-control'}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Generated by Django 4.2.22 on 2025-08-29 01:59

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


class Migration(migrations.Migration):

dependencies = [
('project', '0085_activeproject_georestricted_and_more'),
]

operations = [
migrations.CreateModel(
name='UploadAgreement',
fields=[
('id', models.AutoField(
auto_created=True, primary_key=True, serialize=False,
verbose_name='ID'
)),
('accepted', models.BooleanField(
default=False,
help_text='Whether the upload agreement has been accepted'
)),
('accepted_datetime', models.DateTimeField(
blank=True,
help_text='When the upload agreement was accepted',
null=True
)),
('no_human_subjects', models.BooleanField(
default=False,
help_text='This project does not contain any data derived from human subjects'
)),
('derived_data', models.BooleanField(
default=False,
help_text='This project contains data derived from other de-identified datasets'
)),
('human_subjects_deidentified', models.BooleanField(
default=False,
help_text=(
'This project contains data obtained from human subjects, and all '
'personally identifiable information has been removed'
)
)),
('created_datetime', models.DateTimeField(auto_now_add=True)),
('updated_datetime', models.DateTimeField(auto_now=True)),
('project', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='upload_agreements',
to='project.activeproject'
)),
],
options={
'verbose_name': 'Upload Agreement',
'verbose_name_plural': 'Upload Agreements',
'db_table': 'project_uploadagreement',
},
),
migrations.AddConstraint(
model_name='uploadagreement',
constraint=models.CheckConstraint(
check=models.Q(
('no_human_subjects', True),
('derived_data', True),
('human_subjects_deidentified', True),
_connector='OR'
),
name='at_least_one_data_type_selected'
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.22 on 2025-09-02 18:06

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('project', '0086_uploadagreement_and_more'),
]

operations = [
migrations.AddField(
model_name='uploadagreement',
name='accepted_by',
field=models.ForeignKey(blank=True,
help_text='The user who accepted this upload agreement',
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL),
),
]
Loading
Loading