From 674df525d0e2cb5b9465345f6106822c95f40490 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:32:59 -0400 Subject: [PATCH 01/27] Add upload agreement path. --- physionet-django/project/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/physionet-django/project/urls.py b/physionet-django/project/urls.py index 4e4a6b41fc..731947370a 100644 --- a/physionet-django/project/urls.py +++ b/physionet-django/project/urls.py @@ -72,6 +72,7 @@ path('/submission/', views.project_submission, name='project_submission'), path('/ethics/', views.project_ethics, name='project_ethics'), path('/ethics/edit-document/', views.edit_ethics, name='edit_ethics'), + path('/upload-agreement/', views.project_upload_agreement, name='project_upload_agreement'), path('ethics//', views.serve_document, name='serve_document'), path( '/view-required-trainings/', From b9768a5cbf938c24b52742a85d12f3835ee02406 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:33:51 -0400 Subject: [PATCH 02/27] Update preparation checklist with missing steps. --- physionet-django/templates/about/preparation_checklist.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/physionet-django/templates/about/preparation_checklist.html b/physionet-django/templates/about/preparation_checklist.html index 70df79298a..db5c415544 100644 --- a/physionet-django/templates/about/preparation_checklist.html +++ b/physionet-django/templates/about/preparation_checklist.html @@ -3,6 +3,8 @@
  • Fill in the descriptive metadata.
  • Set the access policy and license.
  • Add discovery information.
  • +
  • Complete the ethics statement.
  • +
  • Accept the upload agreement.
  • Upload the files.
  • Proofread the project and make sure it is ready for submission.
  • From 5b28713fd6bf52966dc23c32966a55bf4e025ea2 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:34:39 -0400 Subject: [PATCH 03/27] import uploadagreement module. --- physionet-django/project/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/physionet-django/project/models.py b/physionet-django/project/models.py index e5d5a1eae1..d6d71d3a69 100644 --- a/physionet-django/project/models.py +++ b/physionet-django/project/models.py @@ -10,3 +10,4 @@ from project.modelcomponents.storage import * from project.modelcomponents.submission import * from project.modelcomponents.log import * +from project.modelcomponents.uploadagreement import * From d3d3ce6640ac6fb636d238d56e773bfea3879de3 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:35:43 -0400 Subject: [PATCH 04/27] Add uploadagreement model. --- .../modelcomponents/uploadagreement.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 physionet-django/project/modelcomponents/uploadagreement.py diff --git a/physionet-django/project/modelcomponents/uploadagreement.py b/physionet-django/project/modelcomponents/uploadagreement.py new file mode 100644 index 0000000000..e387016eb4 --- /dev/null +++ b/physionet-django/project/modelcomponents/uploadagreement.py @@ -0,0 +1,75 @@ +from django.db import models +from django.utils import timezone + + +class UploadAgreement(models.Model): + """ + Model to track upload agreements for projects. + Each project can have one active upload agreement. + """ + project = models.ForeignKey( + 'project.ActiveProject', + on_delete=models.CASCADE, + related_name='upload_agreements' + ) + + # Agreement acceptance + accepted = models.BooleanField( + default=False, + help_text='Whether the upload agreement has been accepted' + ) + + accepted_datetime = models.DateTimeField( + null=True, + blank=True, + help_text='When the upload agreement was accepted' + ) + + # Data type options (at least one must be selected) + 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' + ) + + # Metadata + created_datetime = models.DateTimeField(auto_now_add=True) + updated_datetime = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'project_uploadagreement' + verbose_name = 'Upload Agreement' + verbose_name_plural = 'Upload Agreements' + constraints = [ + models.CheckConstraint( + check=models.Q(no_human_subjects=True) | + models.Q(derived_data=True) | + models.Q(human_subjects_deidentified=True), + name='at_least_one_data_type_selected' + ) + ] + + def __str__(self): + return f"Upload Agreement for {self.project.title} - {'Accepted' if self.accepted else 'Pending'}" + + def save(self, *args, **kwargs): + # If this is being marked as accepted, set the timestamp + if self.accepted and not self.accepted_datetime: + self.accepted_datetime = timezone.now() + super().save(*args, **kwargs) + + @classmethod + def get_active_agreement(cls, project): + """ + Get the active (accepted) upload agreement for a project, if it exists. + """ + return cls.objects.filter(project=project, accepted=True).first() From be065653a5067d00c758b3a06648fe2ffb717437 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:36:11 -0400 Subject: [PATCH 05/27] Add uploadagreement model to admin tool. --- physionet-django/project/admin.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/physionet-django/project/admin.py b/physionet-django/project/admin.py index 81831cb206..0b212ec7de 100644 --- a/physionet-django/project/admin.py +++ b/physionet-django/project/admin.py @@ -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) @@ -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) From 474c2736aa0f178657792d831d9771d9abe96e24 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:36:41 -0400 Subject: [PATCH 06/27] Add uploadagreement form. --- physionet-django/project/forms.py | 62 ++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/physionet-django/project/forms.py b/physionet-django/project/forms.py index dc67c36976..32e2e0587c 100644 --- a/physionet-django/project/forms.py +++ b/physionet-django/project/forms.py @@ -39,6 +39,7 @@ Topic, exists_project_slug, UploadedDocument, + UploadAgreement, ) from user.models import User, TrainingType from user.validators import validate_affiliation @@ -1163,7 +1164,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' @@ -1196,6 +1196,66 @@ 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 published on PhysioNet 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, *args, **kwargs): + super().__init__(*args, **kwargs) + self.project = project + + 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, commit=True): + # Check if we already have an instance (for updates) + if self.instance and self.instance.pk: + # Update existing agreement + agreement = self.instance + agreement.no_human_subjects = self.cleaned_data['no_human_subjects'] + agreement.derived_data = self.cleaned_data['derived_data'] + agreement.human_subjects_deidentified = self.cleaned_data['human_subjects_deidentified'] + agreement.accepted = True + else: + # Create new agreement + agreement = super().save(commit=False) + agreement.project = self.project + agreement.accepted = True + + if commit: + agreement.save() + + return agreement + + class InviteDataAccessReviewerForm(forms.ModelForm): reviewer = forms.CharField(widget=forms.TextInput( attrs={'class': 'form-control'}), From f80a895535c66f607fe9e5e8d674d53b0d39d1de Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:38:13 -0400 Subject: [PATCH 07/27] Add upload agreement page in submission workflow. --- .../project/project_upload_agreement.html | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 physionet-django/project/templates/project/project_upload_agreement.html diff --git a/physionet-django/project/templates/project/project_upload_agreement.html b/physionet-django/project/templates/project/project_upload_agreement.html new file mode 100644 index 0000000000..189f9e8c4a --- /dev/null +++ b/physionet-django/project/templates/project/project_upload_agreement.html @@ -0,0 +1,94 @@ +{% extends "project/project.html" %} + +{% block title %}Upload Agreement - {{ project }}{% endblock %} + +{% load static %} + +{% block main_content %} + +
    + +
    +

    Upload Agreement

    +

    By clicking "I Agree" and uploading files to PhysioNet, you agree that:

    +
      +
    • You are the original creator, or copyright holder, or you have permission from the creators and copyright holders to share these files.
    • +
    • These files do not contain information that could identify individual human subjects.
    • +
    +
    + +{% if not project.author_editable %} +
    + The project cannot be edited right now. +
    +{% elif not is_submitting %} +
    + Only the submitting author may accept the upload agreement. +
    +{% endif %} + +{% if project.upload_agreements.filter.accepted.0 %} +
    + Upload agreement accepted on {{ project.upload_agreements.filter.accepted.0.accepted_datetime|date:"F j, Y" }} at {{ project.upload_agreements.filter.accepted.0.accepted_datetime|time:"g:i A" }}. +

    You can now proceed to upload files to your project.

    + Go to Project Files +
    +{% else %} +
    +
    +
    + {% csrf_token %} + +

    Please check all that apply:

    + +
    +
    + {{ upload_agreement_form.no_human_subjects }} + +
    +
    + +
    +
    + {{ upload_agreement_form.derived_data }} + + {% if upload_agreement_form.derived_data.help_text %} + {{ upload_agreement_form.derived_data.help_text }} + {% endif %} +
    +
    + +
    +
    + {{ upload_agreement_form.human_subjects_deidentified }} + +
    +
    + + {% if upload_agreement_form.non_field_errors %} +
    + {{ upload_agreement_form.non_field_errors }} +
    + {% endif %} + +
    + +
    + +
    +
    +
    +
    +{% endif %} + + + +{% endblock %} From 41b10d5546ba39d55b6a097799c25080889e7df6 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:40:25 -0400 Subject: [PATCH 08/27] Explain that the upload agreement must be signed before uploading files. --- .../project/templates/project/project_files.html | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/physionet-django/project/templates/project/project_files.html b/physionet-django/project/templates/project/project_files.html index 61b8565a18..5dd123501f 100644 --- a/physionet-django/project/templates/project/project_files.html +++ b/physionet-django/project/templates/project/project_files.html @@ -17,7 +17,7 @@ {% endblock %} {% block main_content %} - +
    {% include "about/files.html" %}
    @@ -35,6 +35,17 @@ {{ maintenance_message }} {% endif %} + +{% if not has_accepted_agreement %} +
    + Upload Agreement Required! +

    You must accept the upload agreement before you can upload files to this project.

    + + Accept Upload Agreement + +
    +{% endif %} +
    {% include "project/project_storage_allowance.html" %} From 6b58215c8299398baed6fda5af893a844584413a Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:41:55 -0400 Subject: [PATCH 09/27] Bump proofread page from 7 to 8 in submission sequence. --- .../project/templates/project/project_proofread.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/physionet-django/project/templates/project/project_proofread.html b/physionet-django/project/templates/project/project_proofread.html index 281566245c..4f47574f0f 100644 --- a/physionet-django/project/templates/project/project_proofread.html +++ b/physionet-django/project/templates/project/project_proofread.html @@ -3,7 +3,7 @@ {% block title %}Project Proofread - {{ project }}{% endblock %} {% block main_content %} - +
    {% include "about/proofread.html" %} From 5aa1e71a6458bfc54b0412608181d93276e23d89 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 22:43:54 -0400 Subject: [PATCH 10/27] Disable file upload until upload agreement is signed. --- .../project/templates/project/project_files_form.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/physionet-django/project/templates/project/project_files_form.html b/physionet-django/project/templates/project/project_files_form.html index 2ab850d03a..39627e1652 100644 --- a/physionet-django/project/templates/project/project_files_form.html +++ b/physionet-django/project/templates/project/project_files_form.html @@ -4,7 +4,8 @@
    -
    From 7ce5ebd7dd1469c0f1386ca0370a5daed5dd5c04 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 23:20:36 -0400 Subject: [PATCH 13/27] Add upload agreement view. --- physionet-django/project/views.py | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/physionet-django/project/views.py b/physionet-django/project/views.py index cc42d377a9..390a6a8098 100644 --- a/physionet-django/project/views.py +++ b/physionet-django/project/views.py @@ -50,6 +50,7 @@ Topic, UploadedDocument, AWS, + UploadAgreement, ) from project.authorization.access import can_view_project_files, can_access_project from project.projectfiles import ProjectFiles @@ -890,6 +891,57 @@ def project_discovery(request, project_slug, **kwargs): 'remove_item_url':edit_url, 'is_submitting':is_submitting}) +@project_auth(auth_mode=0, post_auth_mode=2) +def project_upload_agreement(request, project_slug, **kwargs): + """ + Page to accept the upload agreement + """ + project, is_submitting = (kwargs[k] for k in ('project', 'is_submitting')) + + if is_submitting and project.author_editable(): + editable = True + else: + editable = False + + existing_agreement = UploadAgreement.get_active_agreement(project) + + if request.method == 'POST': + # Pass the existing instance if it exists + if existing_agreement: + upload_agreement_form = forms.UploadAgreementForm(project=project, + data=request.POST, + instance=existing_agreement) + else: + upload_agreement_form = forms.UploadAgreementForm(project=project, + data=request.POST) + + if upload_agreement_form.is_valid(): + upload_agreement_form.save() + messages.success(request, 'Upload agreement has been accepted.') + return redirect('project_files', project_slug=project.slug) + else: + messages.error(request, 'Invalid submission. See errors below.') + else: + # Get existing agreement or create new form + if existing_agreement: + upload_agreement_form = forms.UploadAgreementForm(project=project, + instance=existing_agreement) + else: + upload_agreement_form = forms.UploadAgreementForm(project=project) + + # Disable form fields if not editable + if not editable: + for field_name in upload_agreement_form.fields: + upload_agreement_form.fields[field_name].widget.attrs['disabled'] = 'disabled' + + return render(request, 'project/project_upload_agreement.html', { + 'project': project, + 'upload_agreement_form': upload_agreement_form, + 'is_submitting': is_submitting, + 'editable': editable, + }) + + class ProjectAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): qs = PublishedProject.objects.all() @@ -1071,6 +1123,12 @@ def process_files_post(request, project): raise ServiceUnavailable() if 'upload_files' in request.POST: + # Check if upload agreement has been accepted + active_agreement = UploadAgreement.get_active_agreement(project) + if not active_agreement or not active_agreement.accepted: + messages.error(request, 'You must accept the upload agreement before uploading files.') + return '' + form = forms.UploadFilesForm(project=project, data=request.POST, files=request.FILES) subdir = process_items(request, form) @@ -1150,6 +1208,10 @@ def project_files(request, project_slug, subdir='', **kwargs): move_items_form, delete_items_form) = get_file_forms( project=project, subdir=subdir, display_dirs=display_dirs) + # Check if upload agreement has been accepted + from project.models import UploadAgreement + has_accepted_agreement = UploadAgreement.objects.filter(project=project, accepted=True).exists() + return render( request, 'project/project_files.html', @@ -1177,6 +1239,7 @@ def project_files(request, project_slug, subdir='', **kwargs): 'maintenance_message': maintenance_message, 'is_lightwave_supported': project.files.is_lightwave_supported(), 'storage_type': settings.STORAGE_TYPE, + 'has_accepted_agreement': has_accepted_agreement, }, ) From 9c84ce882e40fce56e7c9a1e6b91df0288357269 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 23:21:13 -0400 Subject: [PATCH 14/27] Fix circular uploads. --- physionet-django/notification/models.py | 2 +- physionet-django/physionet/models.py | 2 +- physionet-django/project/modelcomponents/publishedproject.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/physionet-django/notification/models.py b/physionet-django/notification/models.py index 5137f3abc1..4c066e5222 100644 --- a/physionet-django/notification/models.py +++ b/physionet-django/notification/models.py @@ -2,7 +2,7 @@ from django.db import models -from project.models import SafeHTMLField +from project.modelcomponents.fields import SafeHTMLField class News(models.Model): diff --git a/physionet-django/physionet/models.py b/physionet-django/physionet/models.py index ba3e25a474..d6ed358446 100644 --- a/physionet-django/physionet/models.py +++ b/physionet-django/physionet/models.py @@ -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 diff --git a/physionet-django/project/modelcomponents/publishedproject.py b/physionet-django/project/modelcomponents/publishedproject.py index 26380d8aac..62abf50c22 100644 --- a/physionet-django/project/modelcomponents/publishedproject.py +++ b/physionet-django/project/modelcomponents/publishedproject.py @@ -15,7 +15,7 @@ from project.modelcomponents.fields import SafeHTMLField from project.modelcomponents.metadata import Metadata, PublishedTopic from project.modelcomponents.submission import SubmissionInfo -from project.models import AccessPolicy +from project.modelcomponents.access import AccessPolicy from project.utility import StorageInfo, clear_directory, get_tree_size from project.validators import MAX_PROJECT_SLUG_LENGTH, validate_slug, validate_subdir from user.models import Training From 67f106def3ff2abdf9f50373723614715bbd00f7 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 23:21:37 -0400 Subject: [PATCH 15/27] Add upload agreement. --- physionet-django/project/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/physionet-django/project/test_views.py b/physionet-django/project/test_views.py index e95cdf18cd..3ef4f6d45e 100644 --- a/physionet-django/project/test_views.py +++ b/physionet-django/project/test_views.py @@ -32,7 +32,7 @@ PROJECT_VIEWS = [ 'project_overview', 'project_authors', 'project_content', - 'project_access', 'project_discovery', 'project_files', + 'project_access', 'project_discovery', 'project_ethics', 'project_upload_agreement', 'project_files', 'project_proofread', 'project_preview', 'project_submission' ] From 7bf3c69c2e95f44bbabff004c409cc2378931b08 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 23:38:43 -0400 Subject: [PATCH 16/27] Fix style issues. --- physionet-django/project/forms.py | 22 ++++++++-- .../modelcomponents/uploadagreement.py | 40 +++++++++++-------- physionet-django/project/test_views.py | 19 ++++++++- 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/physionet-django/project/forms.py b/physionet-django/project/forms.py index 32e2e0587c..1ff5411ef4 100644 --- a/physionet-django/project/forms.py +++ b/physionet-django/project/forms.py @@ -1208,12 +1208,26 @@ class Meta: '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 published on PhysioNet or elsewhere.', - 'human_subjects_deidentified': 'This project contains data obtained from human subjects, and all personally identifiable information has been removed.', + '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 ' + 'published on PhysioNet 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.', + '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, *args, **kwargs): diff --git a/physionet-django/project/modelcomponents/uploadagreement.py b/physionet-django/project/modelcomponents/uploadagreement.py index e387016eb4..19f813d5d9 100644 --- a/physionet-django/project/modelcomponents/uploadagreement.py +++ b/physionet-django/project/modelcomponents/uploadagreement.py @@ -12,61 +12,69 @@ class UploadAgreement(models.Model): on_delete=models.CASCADE, related_name='upload_agreements' ) - + # Agreement acceptance accepted = models.BooleanField( default=False, help_text='Whether the upload agreement has been accepted' ) - + accepted_datetime = models.DateTimeField( - null=True, + null=True, blank=True, help_text='When the upload agreement was accepted' ) - + # Data type options (at least one must be selected) 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' + help_text=( + 'This project contains data obtained from human subjects, and all ' + 'personally identifiable information has been removed' + ) ) - + # Metadata created_datetime = models.DateTimeField(auto_now_add=True) updated_datetime = models.DateTimeField(auto_now=True) - + class Meta: db_table = 'project_uploadagreement' verbose_name = 'Upload Agreement' verbose_name_plural = 'Upload Agreements' constraints = [ models.CheckConstraint( - check=models.Q(no_human_subjects=True) | - models.Q(derived_data=True) | - models.Q(human_subjects_deidentified=True), + check=( + models.Q(no_human_subjects=True) + | models.Q(derived_data=True) + | models.Q(human_subjects_deidentified=True) + ), name='at_least_one_data_type_selected' ) ] - + def __str__(self): - return f"Upload Agreement for {self.project.title} - {'Accepted' if self.accepted else 'Pending'}" - + return ( + f"Upload Agreement for {self.project.title} - " + f"{'Accepted' if self.accepted else 'Pending'}" + ) + def save(self, *args, **kwargs): # If this is being marked as accepted, set the timestamp if self.accepted and not self.accepted_datetime: self.accepted_datetime = timezone.now() super().save(*args, **kwargs) - + @classmethod def get_active_agreement(cls, project): """ diff --git a/physionet-django/project/test_views.py b/physionet-django/project/test_views.py index 3ef4f6d45e..9f7c6dc518 100644 --- a/physionet-django/project/test_views.py +++ b/physionet-django/project/test_views.py @@ -32,7 +32,8 @@ PROJECT_VIEWS = [ 'project_overview', 'project_authors', 'project_content', - 'project_access', 'project_discovery', 'project_ethics', 'project_upload_agreement', 'project_files', + 'project_access', 'project_discovery', 'project_ethics', + 'project_upload_agreement', 'project_files', 'project_proofread', 'project_preview', 'project_submission' ] @@ -422,6 +423,14 @@ def test_new_project(self): reverse('project_overview', args=(project.slug,))) self.assertEqual(response.status_code, 200) + # Create an upload agreement for the new project so file uploads work + from project.models import UploadAgreement + UploadAgreement.objects.create( + project=project, + accepted=True, + no_human_subjects=True + ) + # Upload a file response = self.client.post( reverse('project_files', args=(project.slug,)), @@ -648,6 +657,14 @@ def setUp(self): self.submitting_author = self.project.authors.filter(is_submitting=True).first() self.coauthor = self.project.authors.filter(is_submitting=False).first() + # Create an upload agreement for the test project so file uploads work + from project.models import UploadAgreement + UploadAgreement.objects.create( + project=self.project, + accepted=True, + no_human_subjects=True + ) + def test_transfer_author(self): """ Test that an activate project can be transferred to a co-author. From ad30545b35ed0588bb8566f8d0efd628b304f7fa Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 23:39:03 -0400 Subject: [PATCH 17/27] Add migration. Adds UploadAgreement. --- .../0086_uploadagreement_and_more.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 physionet-django/project/migrations/0086_uploadagreement_and_more.py diff --git a/physionet-django/project/migrations/0086_uploadagreement_and_more.py b/physionet-django/project/migrations/0086_uploadagreement_and_more.py new file mode 100644 index 0000000000..e021d4279b --- /dev/null +++ b/physionet-django/project/migrations/0086_uploadagreement_and_more.py @@ -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' + ), + ), + ] From 51db8dfd63542975c54460e1d5fdc6f99e2f7c84 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 23:42:27 -0400 Subject: [PATCH 18/27] Remove hardcoded site name. --- physionet-django/project/forms.py | 2 +- .../project/templates/project/project_upload_agreement.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/physionet-django/project/forms.py b/physionet-django/project/forms.py index 1ff5411ef4..c527b040c3 100644 --- a/physionet-django/project/forms.py +++ b/physionet-django/project/forms.py @@ -1213,7 +1213,7 @@ class Meta: ), 'derived_data': ( 'This project contains data derived from other de-identified datasets ' - 'published on PhysioNet or elsewhere.' + f'published on {settings.SITE_NAME} or elsewhere.' ), 'human_subjects_deidentified': ( 'This project contains data obtained from human subjects, and all ' diff --git a/physionet-django/project/templates/project/project_upload_agreement.html b/physionet-django/project/templates/project/project_upload_agreement.html index 39e63b3b81..8327e2ab10 100644 --- a/physionet-django/project/templates/project/project_upload_agreement.html +++ b/physionet-django/project/templates/project/project_upload_agreement.html @@ -10,7 +10,7 @@

    Upload Agreement

    -

    By clicking "I Agree" and uploading files to PhysioNet, you agree that:

    +

    By clicking "I Agree" and uploading files to {{ SITE_NAME }}, you agree that:

    • You are the original creator, or copyright holder, or you have permission from the creators and copyright holders to share these files.
    • These files do not contain information that could identify individual human subjects.
    • From 58739bce36c1f5ef47d0a15afc8582960aba88bf Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Thu, 28 Aug 2025 23:53:02 -0400 Subject: [PATCH 19/27] Fix tests --- physionet-django/project/test_views.py | 30 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/physionet-django/project/test_views.py b/physionet-django/project/test_views.py index 9f7c6dc518..c4ab5f0a20 100644 --- a/physionet-django/project/test_views.py +++ b/physionet-django/project/test_views.py @@ -75,6 +75,8 @@ class TestAccessPresubmission(TestMixin): """ + + @prevent_request_warnings def test_visit_get(self): """ @@ -249,6 +251,16 @@ def test_project_files(self): """ project = ActiveProject.objects.get(title='MIMIC-III Clinical Database') + + # Ensure upload agreement exists for this test + from project.models import UploadAgreement + if not UploadAgreement.objects.filter(project=project, accepted=True).exists(): + UploadAgreement.objects.create( + project=project, + accepted=True, + no_human_subjects=True + ) + # Submitting author self.client.login(username='rgmark@mit.edu', password='Tester11!') @@ -355,6 +367,16 @@ def test_project_file_upload(self): Additional test cases for project_files. """ project = ActiveProject.objects.get(title='MIMIC-III Clinical Database') + + # Ensure upload agreement exists for this test + from project.models import UploadAgreement + if not UploadAgreement.objects.filter(project=project, accepted=True).exists(): + UploadAgreement.objects.create( + project=project, + accepted=True, + no_human_subjects=True + ) + self.client.login(username='rgmark@mit.edu', password='Tester11!') # Set a small storage allowance @@ -657,14 +679,6 @@ def setUp(self): self.submitting_author = self.project.authors.filter(is_submitting=True).first() self.coauthor = self.project.authors.filter(is_submitting=False).first() - # Create an upload agreement for the test project so file uploads work - from project.models import UploadAgreement - UploadAgreement.objects.create( - project=self.project, - accepted=True, - no_human_subjects=True - ) - def test_transfer_author(self): """ Test that an activate project can be transferred to a co-author. From 65a74035872cd4f52b8a07b403dedeb2f9670735 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Tue, 2 Sep 2025 13:54:32 -0400 Subject: [PATCH 20/27] Remove duplicate import. --- physionet-django/project/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/physionet-django/project/views.py b/physionet-django/project/views.py index 390a6a8098..77c0237b2c 100644 --- a/physionet-django/project/views.py +++ b/physionet-django/project/views.py @@ -1208,8 +1208,6 @@ def project_files(request, project_slug, subdir='', **kwargs): move_items_form, delete_items_form) = get_file_forms( project=project, subdir=subdir, display_dirs=display_dirs) - # Check if upload agreement has been accepted - from project.models import UploadAgreement has_accepted_agreement = UploadAgreement.objects.filter(project=project, accepted=True).exists() return render( From 53085cdd3ae459b3706428aaf3337e3ff4607b01 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Tue, 2 Sep 2025 14:00:01 -0400 Subject: [PATCH 21/27] Require a completed upload agreement in order to submit. --- physionet-django/project/modelcomponents/activeproject.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/physionet-django/project/modelcomponents/activeproject.py b/physionet-django/project/modelcomponents/activeproject.py index 4f0d23f74b..b055465b92 100644 --- a/physionet-django/project/modelcomponents/activeproject.py +++ b/physionet-django/project/modelcomponents/activeproject.py @@ -27,6 +27,7 @@ PublishedReference, UploadedDocument, ) +from project.modelcomponents.uploadagreement import UploadAgreement from project.modelcomponents.publishedproject import PublishedProject from project.modelcomponents.submission import CopyeditLog, EditLog, SubmissionInfo from project.modelcomponents.unpublishedproject import UnpublishedProject @@ -341,6 +342,11 @@ def check_integrity(self): AccessPolicy.CONTRIBUTOR_REVIEW} and self.required_trainings is None: self.integrity_errors.append('You have to choose a required training.') + # Upload agreement + active_agreement = UploadAgreement.get_active_agreement(self) + if not active_agreement or not active_agreement.accepted: + self.integrity_errors.append('You must accept the upload agreement before submitting.') + if self.integrity_errors: return False else: From f48cf2d21754324f26604d6b7a5ecc7d43964e0c Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Tue, 2 Sep 2025 14:12:51 -0400 Subject: [PATCH 22/27] If a project is reassigned to a different submitting author, the upload form must be resigned. --- physionet-django/project/forms.py | 11 +++++- .../modelcomponents/uploadagreement.py | 34 ++++++++++++++++++- physionet-django/project/views.py | 6 +++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/physionet-django/project/forms.py b/physionet-django/project/forms.py index c527b040c3..baad2a3d15 100644 --- a/physionet-django/project/forms.py +++ b/physionet-django/project/forms.py @@ -99,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): """ @@ -1230,9 +1234,10 @@ class Meta: ), } - def __init__(self, project, *args, **kwargs): + 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() @@ -1258,11 +1263,15 @@ def save(self, commit=True): agreement.derived_data = self.cleaned_data['derived_data'] agreement.human_subjects_deidentified = self.cleaned_data['human_subjects_deidentified'] agreement.accepted = True + if self.user: + agreement.accepted_by = self.user else: # Create new agreement agreement = super().save(commit=False) agreement.project = self.project agreement.accepted = True + if self.user: + agreement.accepted_by = self.user if commit: agreement.save() diff --git a/physionet-django/project/modelcomponents/uploadagreement.py b/physionet-django/project/modelcomponents/uploadagreement.py index 19f813d5d9..6bf2144c01 100644 --- a/physionet-django/project/modelcomponents/uploadagreement.py +++ b/physionet-django/project/modelcomponents/uploadagreement.py @@ -13,6 +13,15 @@ class UploadAgreement(models.Model): related_name='upload_agreements' ) + # User who accepted the agreement + accepted_by = models.ForeignKey( + 'user.User', + on_delete=models.CASCADE, + null=True, + blank=True, + help_text='The user who accepted this upload agreement' + ) + # Agreement acceptance accepted = models.BooleanField( default=False, @@ -79,5 +88,28 @@ def save(self, *args, **kwargs): def get_active_agreement(cls, project): """ Get the active (accepted) upload agreement for a project, if it exists. + The agreement is only considered active if it was signed by the current submitting author. """ - return cls.objects.filter(project=project, accepted=True).first() + try: + submitting_author = project.authors.get(is_submitting=True) + except project.authors.model.DoesNotExist: + return None + except project.authors.model.MultipleObjectsReturned: + raise project.authors.model.MultipleObjectsReturned( + f"Multiple submitting authors found for project {project.id}. " + "This indicates a data integrity issue." + ) + + try: + return cls.objects.get( + project=project, + accepted=True, + accepted_by=submitting_author.user + ) + except cls.DoesNotExist: + return None + except cls.MultipleObjectsReturned: + raise cls.MultipleObjectsReturned( + f"Multiple active upload agreements found for project {project.id} " + f"and user {submitting_author.user.id}. This indicates a data integrity issue." + ) diff --git a/physionet-django/project/views.py b/physionet-django/project/views.py index 77c0237b2c..2d171568b1 100644 --- a/physionet-django/project/views.py +++ b/physionet-django/project/views.py @@ -909,10 +909,12 @@ def project_upload_agreement(request, project_slug, **kwargs): # Pass the existing instance if it exists if existing_agreement: upload_agreement_form = forms.UploadAgreementForm(project=project, + user=request.user, data=request.POST, instance=existing_agreement) else: upload_agreement_form = forms.UploadAgreementForm(project=project, + user=request.user, data=request.POST) if upload_agreement_form.is_valid(): @@ -925,9 +927,11 @@ def project_upload_agreement(request, project_slug, **kwargs): # Get existing agreement or create new form if existing_agreement: upload_agreement_form = forms.UploadAgreementForm(project=project, + user=request.user, instance=existing_agreement) else: - upload_agreement_form = forms.UploadAgreementForm(project=project) + upload_agreement_form = forms.UploadAgreementForm(project=project, + user=request.user) # Disable form fields if not editable if not editable: From fe80d29be03eb8f3ab95c08774f8233fcf537cd2 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Tue, 2 Sep 2025 14:16:15 -0400 Subject: [PATCH 23/27] Migration: Record user who accepted the agreement. --- ...0087_add_accepted_by_to_uploadagreement.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 physionet-django/project/migrations/0087_add_accepted_by_to_uploadagreement.py diff --git a/physionet-django/project/migrations/0087_add_accepted_by_to_uploadagreement.py b/physionet-django/project/migrations/0087_add_accepted_by_to_uploadagreement.py new file mode 100644 index 0000000000..87614384c2 --- /dev/null +++ b/physionet-django/project/migrations/0087_add_accepted_by_to_uploadagreement.py @@ -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), + ), + ] From 1ae0fe36774a378a91c3967f72d1795b24af10db Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Wed, 3 Sep 2025 20:58:59 -0400 Subject: [PATCH 24/27] Add upload agreement to fixtures. --- .../project/fixtures/demo-project.json | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/physionet-django/project/fixtures/demo-project.json b/physionet-django/project/fixtures/demo-project.json index f57758af7b..5c25846f35 100644 --- a/physionet-django/project/fixtures/demo-project.json +++ b/physionet-django/project/fixtures/demo-project.json @@ -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" + } } ] From acb9342d9d9f95abebab4e6ef7ac1a05824896f6 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Wed, 3 Sep 2025 22:32:19 -0400 Subject: [PATCH 25/27] Add upload agreement for new project versions. --- physionet-django/console/test_views.py | 13 ++++++++++++- physionet-django/project/test_views.py | 22 +++------------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/physionet-django/console/test_views.py b/physionet-django/console/test_views.py index a12ae0f624..e746a68e5a 100644 --- a/physionet-django/console/test_views.py +++ b/physionet-django/console/test_views.py @@ -19,6 +19,7 @@ PublishedProject, StorageRequest, SubmissionStatus, + UploadAgreement ) from user.models import User from physionet.models import FrontPageButton, StaticPage @@ -390,7 +391,6 @@ def get_project(): project.refresh_from_db() self.assertTrue(project.is_publishable()) - def test_publish(self): """ Test publishing project @@ -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 diff --git a/physionet-django/project/test_views.py b/physionet-django/project/test_views.py index c4ab5f0a20..d572ce7125 100644 --- a/physionet-django/project/test_views.py +++ b/physionet-django/project/test_views.py @@ -25,6 +25,7 @@ PublishedProject, StorageRequest, SubmissionStatus, + UploadAgreement, AWS ) from user.models import User @@ -252,15 +253,6 @@ def test_project_files(self): """ project = ActiveProject.objects.get(title='MIMIC-III Clinical Database') - # Ensure upload agreement exists for this test - from project.models import UploadAgreement - if not UploadAgreement.objects.filter(project=project, accepted=True).exists(): - UploadAgreement.objects.create( - project=project, - accepted=True, - no_human_subjects=True - ) - # Submitting author self.client.login(username='rgmark@mit.edu', password='Tester11!') @@ -368,15 +360,6 @@ def test_project_file_upload(self): """ project = ActiveProject.objects.get(title='MIMIC-III Clinical Database') - # Ensure upload agreement exists for this test - from project.models import UploadAgreement - if not UploadAgreement.objects.filter(project=project, accepted=True).exists(): - UploadAgreement.objects.create( - project=project, - accepted=True, - no_human_subjects=True - ) - self.client.login(username='rgmark@mit.edu', password='Tester11!') # Set a small storage allowance @@ -446,9 +429,10 @@ def test_new_project(self): self.assertEqual(response.status_code, 200) # Create an upload agreement for the new project so file uploads work - from project.models import UploadAgreement + submitting_author = project.authors.get(is_submitting=True) UploadAgreement.objects.create( project=project, + accepted_by=submitting_author.user, accepted=True, no_human_subjects=True ) From 8d3236b04824de539388c704270bfd15cf894a42 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Tue, 9 Sep 2025 10:51:28 -0400 Subject: [PATCH 26/27] Simplify save method. --- physionet-django/project/forms.py | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/physionet-django/project/forms.py b/physionet-django/project/forms.py index baad2a3d15..6d11e67621 100644 --- a/physionet-django/project/forms.py +++ b/physionet-django/project/forms.py @@ -1254,28 +1254,12 @@ def clean(self): return cleaned_data - def save(self, commit=True): - # Check if we already have an instance (for updates) - if self.instance and self.instance.pk: - # Update existing agreement - agreement = self.instance - agreement.no_human_subjects = self.cleaned_data['no_human_subjects'] - agreement.derived_data = self.cleaned_data['derived_data'] - agreement.human_subjects_deidentified = self.cleaned_data['human_subjects_deidentified'] - agreement.accepted = True - if self.user: - agreement.accepted_by = self.user - else: - # Create new agreement - agreement = super().save(commit=False) - agreement.project = self.project - agreement.accepted = True - if self.user: - agreement.accepted_by = self.user - - if commit: - agreement.save() - + def save(self): + agreement = super().save(commit=False) + agreement.project = self.project + agreement.accepted = True + agreement.accepted_by = self.user + agreement.save() return agreement From e4ff3cc8482e765470ad376adc2f95944f09291a Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Tue, 9 Sep 2025 10:54:40 -0400 Subject: [PATCH 27/27] Remove redundant code. --- physionet-django/project/views.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/physionet-django/project/views.py b/physionet-django/project/views.py index 2d171568b1..240409afc3 100644 --- a/physionet-django/project/views.py +++ b/physionet-django/project/views.py @@ -906,16 +906,10 @@ def project_upload_agreement(request, project_slug, **kwargs): existing_agreement = UploadAgreement.get_active_agreement(project) if request.method == 'POST': - # Pass the existing instance if it exists - if existing_agreement: - upload_agreement_form = forms.UploadAgreementForm(project=project, - user=request.user, - data=request.POST, - instance=existing_agreement) - else: - upload_agreement_form = forms.UploadAgreementForm(project=project, - user=request.user, - data=request.POST) + upload_agreement_form = forms.UploadAgreementForm(project=project, + user=request.user, + data=request.POST, + instance=existing_agreement) if upload_agreement_form.is_valid(): upload_agreement_form.save() @@ -925,13 +919,9 @@ def project_upload_agreement(request, project_slug, **kwargs): messages.error(request, 'Invalid submission. See errors below.') else: # Get existing agreement or create new form - if existing_agreement: - upload_agreement_form = forms.UploadAgreementForm(project=project, - user=request.user, - instance=existing_agreement) - else: - upload_agreement_form = forms.UploadAgreementForm(project=project, - user=request.user) + upload_agreement_form = forms.UploadAgreementForm(project=project, + user=request.user, + instance=existing_agreement) # Disable form fields if not editable if not editable: