From 749a6c1bfa972dd29d01492ffdef1426857921d0 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Mon, 13 Nov 2023 12:36:57 +0100 Subject: [PATCH 1/3] Allow submitting author status to be transferred to a co-author. Ref #2128. We occasionally ask for submtting author status of a project to be transferred. This change adds a 'transfer project' button to the author page of the project submission system. --- .../email/notify_submitting_author.html | 11 +++++ physionet-django/notification/utility.py | 19 ++++++++ physionet-django/project/forms.py | 25 ++++++++++ .../static/project/js/transfer-author.js | 22 +++++++++ .../templates/project/project_authors.html | 46 +++++++++++++++++++ physionet-django/project/views.py | 15 +++++- 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 physionet-django/notification/templates/notification/email/notify_submitting_author.html create mode 100644 physionet-django/project/static/project/js/transfer-author.js diff --git a/physionet-django/notification/templates/notification/email/notify_submitting_author.html b/physionet-django/notification/templates/notification/email/notify_submitting_author.html new file mode 100644 index 0000000000..25af6d4a77 --- /dev/null +++ b/physionet-django/notification/templates/notification/email/notify_submitting_author.html @@ -0,0 +1,11 @@ +{% load i18n %}{% autoescape off %}{% filter wordwrap:70 %} +Dear {{ name }}, + +You have been made submitting author of the project entitled "{{ project.title }}" on {{ SITE_NAME }}. + +You can view and edit the project on your project homepage: {{ url_prefix }}{% url "project_home" %}. + +{{ signature }} + +{{ footer }} +{% endfilter %}{% endautoescape %} diff --git a/physionet-django/notification/utility.py b/physionet-django/notification/utility.py index 4960b58e22..b720443704 100644 --- a/physionet-django/notification/utility.py +++ b/physionet-django/notification/utility.py @@ -1023,3 +1023,22 @@ def notify_event_participant_application(request, user, registered_user, event): body = loader.render_to_string('events/email/event_registration.html', context) # Not resend the email if there was an integrity error send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [user.email], fail_silently=False) + + +def notify_submitting_author(request, project): + """ + Notify a user that they have been made submitting author for a project. + """ + author = project.authors.get(is_submitting=True) + subject = f"{settings.SITE_NAME}: You are now a submitting author" + context = { + 'name': author.get_full_name(), + 'project': project, + 'url_prefix': get_url_prefix(request), + 'SITE_NAME': settings.SITE_NAME, + 'signature': settings.EMAIL_SIGNATURE, + 'footer': email_footer() + } + body = loader.render_to_string('notification/email/notify_submitting_author.html', context) + # Not resend the email if there was an integrity error + send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [author.user.email], fail_silently=False) diff --git a/physionet-django/project/forms.py b/physionet-django/project/forms.py index 7ee1985572..22ee0839a1 100644 --- a/physionet-django/project/forms.py +++ b/physionet-django/project/forms.py @@ -73,6 +73,31 @@ def update_corresponder(self): new_c.save() +class TransferAuthorForm(forms.Form): + """ + Transfer submitting author. + """ + transfer_author = forms.ModelChoiceField(queryset=None, required=True, + widget=forms.Select(attrs={'onchange': 'set_transfer_author()', + 'id': 'transfer_author_id'}), + empty_label="Select an author") + + def __init__(self, project, *args, **kwargs): + super().__init__(*args, **kwargs) + self.project = project + # Exclude the current submitting author from the queryset + authors = project.authors.exclude(is_submitting=True).order_by('display_order') + self.fields['transfer_author'].queryset = authors + + def transfer(self): + new_author = self.cleaned_data['transfer_author'] + + # Assign the new submitting author + self.project.authors.update(is_submitting=False) + new_author.is_submitting = True + new_author.save() + + class ActiveProjectFilesForm(forms.Form): """ Inherited form for manipulating project files/directories. Upload diff --git a/physionet-django/project/static/project/js/transfer-author.js b/physionet-django/project/static/project/js/transfer-author.js new file mode 100644 index 0000000000..5f72c8df18 --- /dev/null +++ b/physionet-django/project/static/project/js/transfer-author.js @@ -0,0 +1,22 @@ +$(document).ready(function() { + // Function to update the displayed author name when a new author is selected + function set_transfer_author() { + var selectedAuthorName = $("#transfer_author_id option:selected").text(); + $('#project_author').text(selectedAuthorName); + } + + // Attach the change event to the author select dropdown to update the name on change + $("#transfer_author_id").change(set_transfer_author); + + // Prevent the default form submission and show the confirmation modal + $('#authorTransferForm').on('submit', function(e) { + e.preventDefault(); + $('#transfer_author_modal').modal('show'); + }); + + // When the confirmation button is clicked, submit the form + $('#confirmAuthorTransfer').on('click', function() { + $('#authorTransferForm').off('submit').submit(); + }); +}); + diff --git a/physionet-django/project/templates/project/project_authors.html b/physionet-django/project/templates/project/project_authors.html index 08f368ba47..60ebdf8a80 100644 --- a/physionet-django/project/templates/project/project_authors.html +++ b/physionet-django/project/templates/project/project_authors.html @@ -170,6 +170,46 @@

Your Affiliations


+
+ +{# Transfer project to a new submitting author #} +{% if is_submitting %} +

Submitting Author

+

Only the submitting author of a project is able to edit content. + You may transfer the role of submitting author to a co-author. + Choose one of the co-authors below to make them the submitting author for this project. + Transferring authorship will remove your ability to edit content!

+ +
+ {% csrf_token %} + {% include "inline_form_snippet.html" with form=transfer_author_form %} + +
+
+{% endif %} + + + + + {% endblock %} {% block local_js_bottom %} @@ -177,8 +217,14 @@

Your Affiliations

+ {# Disable submission if not currently editable #} {% if not project.author_editable %} {% endif %} + +{% if is_submitting %} + +{% endif %} + {% endblock %} diff --git a/physionet-django/project/views.py b/physionet-django/project/views.py index a680ab43f5..74e47bb772 100644 --- a/physionet-django/project/views.py +++ b/physionet-django/project/views.py @@ -536,8 +536,10 @@ def project_authors(request, project_slug, **kwargs): inviter=user) corresponding_author_form = forms.CorrespondingAuthorForm( project=project) + transfer_author_form = forms.TransferAuthorForm( + project=project) else: - invite_author_form, corresponding_author_form = None, None + invite_author_form, corresponding_author_form, transfer_author_form = None, None, None if author.is_corresponding: corresponding_email_form = AssociatedEmailChoiceForm( @@ -591,6 +593,16 @@ def project_authors(request, project_slug, **kwargs): messages.success(request, 'Your corresponding email has been updated.') else: messages.error(request, 'Submission unsuccessful. See form for errors.') + elif 'transfer_author' in request.POST and is_submitting: + transfer_author_form = forms.TransferAuthorForm( + project=project, data=request.POST) + if transfer_author_form.is_valid(): + transfer_author_form.transfer() + notification.notify_submitting_author(request, project) + messages.success(request, 'The submitting author has been updated.') + return redirect('project_authors', project_slug=project.slug) + else: + messages.error(request, 'Submission unsuccessful. See form for errors.') authors = project.get_author_info() invitations = project.authorinvitations.filter(is_active=True) @@ -601,6 +613,7 @@ def project_authors(request, project_slug, **kwargs): 'invite_author_form':invite_author_form, 'corresponding_author_form':corresponding_author_form, 'corresponding_email_form':corresponding_email_form, + 'transfer_author_form': transfer_author_form, 'add_item_url':edit_affiliations_url, 'remove_item_url':edit_affiliations_url, 'is_submitting':is_submitting}) From 34e25d514c3f8bb54c5d502c09911a0b3dcceb63 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Mon, 13 Nov 2023 16:39:10 -0500 Subject: [PATCH 2/3] reformat return statement --- physionet-django/project/views.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/physionet-django/project/views.py b/physionet-django/project/views.py index 74e47bb772..eb11acae14 100644 --- a/physionet-django/project/views.py +++ b/physionet-django/project/views.py @@ -607,15 +607,23 @@ def project_authors(request, project_slug, **kwargs): authors = project.get_author_info() invitations = project.authorinvitations.filter(is_active=True) edit_affiliations_url = reverse('edit_affiliation', args=[project.slug]) - return render(request, 'project/project_authors.html', {'project':project, - 'authors':authors, 'invitations':invitations, - 'affiliation_formset':affiliation_formset, - 'invite_author_form':invite_author_form, - 'corresponding_author_form':corresponding_author_form, - 'corresponding_email_form':corresponding_email_form, - 'transfer_author_form': transfer_author_form, - 'add_item_url':edit_affiliations_url, 'remove_item_url':edit_affiliations_url, - 'is_submitting':is_submitting}) + return render( + request, + "project/project_authors.html", + { + "project": project, + "authors": authors, + "invitations": invitations, + "affiliation_formset": affiliation_formset, + "invite_author_form": invite_author_form, + "corresponding_author_form": corresponding_author_form, + "corresponding_email_form": corresponding_email_form, + "transfer_author_form": transfer_author_form, + "add_item_url": edit_affiliations_url, + "remove_item_url": edit_affiliations_url, + "is_submitting": is_submitting, + }, + ) def edit_content_item(request, project_slug): From 84141fa376bdce8e67c4d1b593b077fd555c1fe2 Mon Sep 17 00:00:00 2001 From: Tom Pollard Date: Wed, 15 Nov 2023 13:59:19 -0500 Subject: [PATCH 3/3] Add test case for transferring submitting author. --- physionet-django/project/test_views.py | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/physionet-django/project/test_views.py b/physionet-django/project/test_views.py index 97393f01ca..83a9f8f0cc 100644 --- a/physionet-django/project/test_views.py +++ b/physionet-django/project/test_views.py @@ -553,6 +553,45 @@ def test_content(self): self.assertFalse(project.is_submittable()) +class TestProjectTransfer(TestCase): + """ + Tests that submitting author status can be transferred to a co-author + """ + AUTHOR_EMAIL = 'rgmark@mit.edu' + COAUTHOR_EMAIL = 'aewj@mit.edu' + PASSWORD = 'Tester11!' + PROJECT_SLUG = 'T108xFtYkRAxiRiuOLEJ' + + def setUp(self): + self.client.login(username=self.AUTHOR_EMAIL, password=self.PASSWORD) + self.project = ActiveProject.objects.get(slug=self.PROJECT_SLUG) + self.submitting_author = self.project.authors.filter(is_submitting=True).first() + self.coauthor = self.project.authors.filter(is_submitting=False).first() + + def test_transfer_author(self): + """ + Test that an activate project can be transferred to a co-author. + """ + self.assertEqual(self.submitting_author.user.email, self.AUTHOR_EMAIL) + self.assertEqual(self.coauthor.user.email, self.COAUTHOR_EMAIL) + + response = self.client.post( + reverse('project_authors', args=(self.project.slug,)), + data={ + 'transfer_author': self.coauthor.user.id, + }) + + # Check if redirect happens, implying successful transfer + self.assertEqual(response.status_code, 302) + + # Fetch the updated project data + updated_project = ActiveProject.objects.get(slug=self.PROJECT_SLUG) + + # Verify that the author has been transferred + self.assertFalse(updated_project.authors.get(user=self.submitting_author.user).is_submitting) + self.assertTrue(updated_project.authors.get(user=self.coauthor.user).is_submitting) + + class TestAccessPublished(TestMixin): """ Test that certain views or content in their various states can only