Skip to content

Commit

Permalink
Merge pull request #2131 into dev
Browse files Browse the repository at this point in the history
As discussed in #2128, currently the submitting author of a project
cannot be changed. There are times when it would be helpful to allow
submitting authors to transfer this status to co-authors. e.g. when:

* A co-author would like to take responsibility for editing content or
  uploading files.

* A project requires updating and the original submitting author is no
  longer available.

This change adds a 'transfer project' button to the author page of the
project submission system.
  • Loading branch information
Benjamin Moody committed Nov 15, 2023
2 parents b713147 + 84141fa commit 80e589a
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -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 %}
19 changes: 19 additions & 0 deletions physionet-django/notification/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -1039,3 +1039,22 @@ def notify_primary_email(associated_email):
}
body = loader.render_to_string('user/email/notify_primary_email.html', context)
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [associated_email.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)
25 changes: 25 additions & 0 deletions physionet-django/project/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions physionet-django/project/static/project/js/transfer-author.js
Original file line number Diff line number Diff line change
@@ -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();
});
});

46 changes: 46 additions & 0 deletions physionet-django/project/templates/project/project_authors.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,61 @@ <h3>Your Affiliations</h3>
<button class="btn btn-primary btn-rsp" name="edit_affiliations" type="submit">Set Affiliations</button>
</form>
<hr>
<br>

{# Transfer project to a new submitting author #}
{% if is_submitting %}
<h3>Submitting Author</h3>
<p>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!</p>

<form id="authorTransferForm" action="{% url 'project_authors' project.slug %}" method="post" class="no-pd">
{% csrf_token %}
{% include "inline_form_snippet.html" with form=transfer_author_form %}
<button class="btn btn-primary btn-rsp" name="transfer_author" type="submit">Transfer</button>
</form>
<hr>
{% endif %}

<!-- Modal for confirming author transfer -->
<div class="modal fade" id="transfer_author_modal" tabindex="-1" role="dialog" aria-labelledby="transferAuthorshipModalTitle" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="transferAuthorshipModalTitle">Transfer Authorship</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p id="confirmationText">Please confirm that you would like to assign '<span id="project_author"></span>' as the new submitting author.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="confirmAuthorTransfer">Transfer</button>
</div>
</div>
</div>
</div>
<!-- End Modal -->

{% endblock %}

{% block local_js_bottom %}
<script src="{% static 'custom/js/enable-popover.js' %}"></script>
<script>
disableAddButtons();
</script>

{# Disable submission if not currently editable #}
{% if not project.author_editable %}
<script src="{% static 'custom/js/disable-input.js' %}"></script>
{% endif %}

{% if is_submitting %}
<script src="{% static 'project/js/transfer-author.js' %}"></script>
{% endif %}

{% endblock %}
39 changes: 39 additions & 0 deletions physionet-django/project/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '[email protected]'
COAUTHOR_EMAIL = '[email protected]'
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
Expand Down
39 changes: 30 additions & 9 deletions physionet-django/project/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -591,18 +593,37 @@ 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)
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,
'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):
Expand Down

0 comments on commit 80e589a

Please sign in to comment.