Skip to content

Commit

Permalink
Allow editorial team to record internal notes for active projects (#2255
Browse files Browse the repository at this point in the history
)

We are struggling to keep on top of reviewing and processing new
project submissions to PhysioNet. One small feature that would be
helpful is to allow people with editorial permissions to record short
notes about a project.

e.g. "Contact the authors to arrange an embargo date"; "The references
are incorrectly ordered. This should be fixed before publication";
"Takeshi, please could you review some of the images?"

This pull request adds functionality to create and delete notes about
an active project.
  • Loading branch information
Benjamin Moody committed Jul 11, 2024
2 parents c49c57b + f2a408b commit 2ce3a8a
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 9 deletions.
10 changes: 10 additions & 0 deletions physionet-django/console/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
PublishedPublication,
SubmissionStatus,
exists_project_slug,
InternalNote,
)
from project.validators import MAX_PROJECT_SLUG_LENGTH, validate_doi, validate_slug
from user.models import CodeOfConduct, CredentialApplication, CredentialReview, User, TrainingQuestion
Expand Down Expand Up @@ -73,6 +74,15 @@
)


class InternalNoteForm(forms.ModelForm):
class Meta:
widgets = {
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
}
model = InternalNote
fields = ['content']


class AssignEditorForm(forms.Form):
"""
Assign an editor to a project under submission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<a class="nav-link" id="timeline-tab" data-toggle="tab" href="#timeline" role="tab" aria-controls="timeline" aria-selected="false">Submission Timeline</a>
</li>
<li class="nav-item">
<a class="nav-link" id="ethics-tab" data-toggle="tab" href="#ethics" role="tab" aria-controls="timeline" aria-selected="false">Ethics</a>
<a class="nav-link" id="ethics-tab" data-toggle="tab" href="#ethics" role="tab" aria-controls="ethics" aria-selected="false">Ethics</a>
</li>
<li class="nav-item">
<a class="nav-link {% if passphrase %} active {% endif %}" id="anonymous-tab" data-toggle="tab" href="#anonymous" role="tab" aria-controls="anonymous" aria-selected="false">Anonymous Access</a>
Expand All @@ -24,9 +24,12 @@
{% endif %}
{% if project.submission_status >= SubmissionStatus.NEEDS_COPYEDIT %}
<li class="nav-item">
<a class="nav-link" id="doi-tab" data-toggle="tab" href="#doi" role="tab" aria-controls="timeline" aria-selected="false">DOIs</a>
<a class="nav-link" id="doi-tab" data-toggle="tab" href="#doi" role="tab" aria-controls="doi" aria-selected="false">DOIs</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" id="notes-tab" data-toggle="tab" href="#notes" role="tab" aria-controls="notes" aria-selected="false">Internal notes</a>
</li>
</ul>
</div>
<div class="card-body">
Expand Down Expand Up @@ -69,7 +72,7 @@ <h4 class="card-title">{{ project.title }}</h4>
{% include "project/active_submission_timeline.html" %}
</div>
{# Ethics #}
<div class="tab-pane fade" id="ethics" role="tabpanel" aria-labelledby="timeline-tab">
<div class="tab-pane fade" id="ethics" role="tabpanel" aria-labelledby="ethics-tab">
<h5 class="card-title">Ethics statement</h5>
<p class="card-text">{{ project.ethics_statement|safe }}</p>
<h5>Uploaded Documents</h5>
Expand Down Expand Up @@ -118,6 +121,40 @@ <h5>Uploaded Documents</h5>
</form>
</div>

{# Notes #}
<div class="tab-pane fade" id="notes" role="tabpanel" aria-labelledby="notes-tab">
<p>Notes posted here are visible only to the editorial team. They are not visible to authors.</p>
<ul class="list-group">
{% for note in notes %}
<li class="list-group-item">
<p>{{ note.content|linebreaks }}</p>
<p class="small text-muted">Created by {{ note.created_by }} on {{ note.created_at }}</p>
{% if note.created_by == user %}
<form action="{% url 'submission_info' project.slug %}" method="POST" id="internal_note_form">
{% csrf_token %}
<input type="hidden" name="note_id" value="{{ note.id }}">
<button class="btn btn-danger btn-rsp" type="submit" name="delete_internal_note" onclick="return confirm('Are you sure you want to delete this note?');">
Delete
</button>
</form>
{% endif %}
</li>
{% endfor %}
</ul>

<br />

<form action="{% url 'submission_info' project.slug %}" method="POST" id="internal_note_form">
{% csrf_token %}
<div class="form-group">
{{ internal_note_form.content }}
</div>
<button class="btn btn-primary" type="submit" name="add_internal_note">
Add Note
</button>
</form>
</div>

{# Permanent Reassign #}
{% if project.editor == user %}
<div class="tab-pane fade" id="reassign" role="tabpanel" aria-labelledby="reassign-tab">
Expand Down
31 changes: 25 additions & 6 deletions physionet-django/console/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
SubmissionStatus,
Topic,
exists_project_slug,
InternalNote
)
from project.authorization.access import can_view_project_files
from project.utility import readable_size
Expand Down Expand Up @@ -275,16 +276,15 @@ def submission_info(request, project_slug):
"""
View information about a project under submission
"""
try:
project = ActiveProject.objects.get(slug=project_slug)
except ActiveProject.DoesNotExist:
raise Http404()
project = get_object_or_404(ActiveProject, slug=project_slug)
notes = project.internal_notes.all().order_by('-created_at')

user = request.user
authors, author_emails, storage_info, edit_logs, copyedit_logs, latest_version = project.info_card()

data = request.POST or None
reassign_editor_form = forms.ReassignEditorForm(user, data=data)
internal_note_form = forms.InternalNoteForm(data)
embargo_form = forms.EmbargoFilesDaysForm()
passphrase = ''
anonymous_url = project.get_anonymous_url()
Expand Down Expand Up @@ -313,6 +313,24 @@ def submission_info(request, project_slug):
project.embargo_files_days = days
project.save()
messages.success(request, f"An embargo was set for {project.embargo_files_days} day(s)")
if 'add_internal_note' in request.POST:
if internal_note_form.is_valid():
note = internal_note_form.save(commit=False)
note.project = project
note.created_by = request.user
note.save()
messages.success(request, "Note added.")
internal_note_form = forms.InternalNoteForm()
return redirect(f'{request.path}?tab=notes')
if 'delete_internal_note' in request.POST:
note_id = request.POST['note_id']
note = get_object_or_404(InternalNote, pk=note_id, project=project)
if note.created_by == request.user:
note.delete()
messages.success(request, "Note deleted.")
else:
messages.error(request, "You are not authorized to delete this note.")
return redirect(f'{request.path}?tab=notes')

url_prefix = notification.get_url_prefix(request)
bulk_url_prefix = notification.get_url_prefix(request, bulk_download=True)
Expand All @@ -324,8 +342,9 @@ def submission_info(request, project_slug):
'anonymous_url': anonymous_url, 'url_prefix': url_prefix,
'bulk_url_prefix': bulk_url_prefix,
'reassign_editor_form': reassign_editor_form,
'embargo_form': embargo_form})

'embargo_form': embargo_form,
'notes': notes,
'internal_note_form': internal_note_form})

@handling_editor
def edit_submission(request, project_slug, *args, **kwargs):
Expand Down
46 changes: 46 additions & 0 deletions physionet-django/project/migrations/0076_internalnote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated by Django 4.1.13 on 2024-07-11 16:35

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", "0075_publishedreference_url"),
]

operations = [
migrations.CreateModel(
name="InternalNote",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("content", models.TextField()),
(
"created_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
(
"project",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="internal_notes",
to="project.activeproject",
),
),
],
),
]
13 changes: 13 additions & 0 deletions physionet-django/project/modelcomponents/activeproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,3 +641,16 @@ def is_editable_by(self, user):
editor_copyediting = self.copyeditable() and user == self.editor

return author_submitting or editor_copyediting


class InternalNote(models.Model):
"""
Allow notes to be created for active projects. These notes should only be viewable by people with editor status.
"""
project = models.ForeignKey('project.ActiveProject', on_delete=models.CASCADE, related_name='internal_notes')
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
content = models.TextField()

def __str__(self):
return f"Note by {self.created_by} on {self.created_at}"

0 comments on commit 2ce3a8a

Please sign in to comment.