diff --git a/scipost_django/journals/models/publication.py b/scipost_django/journals/models/publication.py index 478577f960887da700d9919a7bd31bd2ba1f37de..64adb5bad1933de7c49f8baf83bc08d6b7e4c095 100644 --- a/scipost_django/journals/models/publication.py +++ b/scipost_django/journals/models/publication.py @@ -63,20 +63,24 @@ class PublicationAuthorsTable(models.Model): self.order = self.publication.authors.count() + 1 return super().save(*args, **kwargs) + def is_empty(self) -> bool: + """Check if object is a temporary placeholder.""" + return self.profile is None + @property - def is_registered(self): + def is_registered(self) -> bool: """Check if author is registered at SciPost.""" - return self.profile.contributor is not None + return not self.is_empty() and self.profile.contributor is not None @property - def first_name(self): - """Return first name of author.""" - return self.profile.first_name + def first_name(self) -> str: + """Return first name of author. If not available, return None.""" + return self.profile.first_name if self.profile else None @property - def last_name(self): - """Return last name of author.""" - return self.profile.last_name + def last_name(self) -> str: + """Return last name of author. If not available, return None.""" + return self.profile.last_name if self.profile else None class Publication(models.Model): diff --git a/scipost_django/journals/templates/journals/add_author.html b/scipost_django/journals/templates/journals/add_author.html index e6154e98fb9b298b2e459701634e0cb269def7d7..67c30c3644d2795a96c21f13320240b090de1d40 100644 --- a/scipost_django/journals/templates/journals/add_author.html +++ b/scipost_django/journals/templates/journals/add_author.html @@ -20,48 +20,36 @@ {% block content %} - <h1 class="highlight">Add author to publication</h1> + <h1 class="highlight">Associate author profiles to the publication</h1> {% include 'journals/_publication_li_content.html' with publication=publication %} <br> + <div class="row position-relative"> + <div class="col-md-12 col-lg-6"> + <h3>List of authors </h3> + {% include "journals/add_author_list.html" %} + </div> - <div class="row"> - <div class="col-12"> - <h3>Current list of authors</h3> - <ul> - {% for author in publication.authors.all %} - <li> - {% if author.is_registered %} - <a href="{{ author.profile.contributor.get_absolute_url }}">{{ author.profile.contributor }}</a> - {% else %} - <a href="{{ author.profile }}">{{ author.profile }}</a> - {% endif %} - </li> - {% empty %} - <li>No authors known.</li> - {% endfor %} - </ul> - - <br> - <h2 class="highlight">Add another author</h2> - - <div class="row"> - <div class="col-md-6 col-lg-6"> - <form action="{% url 'journals:add_author' doi_label=publication.doi_label %}" method="post"> - {% csrf_token %} - {{ form|bootstrap }} - <input class="btn btn-primary" type="submit" value="Add this author"> - </form> - </div> - <div class="col-md-6 col-lg-6"> - <h3>Not found?</h3> - <p>Then you can <a href="{% url 'profiles:profile_create' %}" target="_blank">add the required Profile</a> and come back to this page.</p> - </div> + <div class="col-md-12 col-lg-6"> + <div class="position-sticky top-0 pt-5"> + <h3>Author actions:</h3> + + <form hx-post="{% url 'journals:add_author' doi_label=publication.doi_label%}" + hx-target="#add_author_list" id = "add_author_form" hx-swap="outerHTML"> + {% csrf_token %} + {{ form|bootstrap }} + </form> + <div class="d-flex flex-row gap-2"> + <input class="btn btn-primary" type="submit" value="Add Author" form="add_author_form"/> + <a class="btn btn-secondary" + title="Fetch author list data from the git." + href="{% url 'journals:reset_authors' doi_label=publication.doi_label %}"> Reset + </a> + </div> </div> </div> </div> - - <p>Return to the <a href="{{publication.get_absolute_url}}">publication's page</a> or to the <a href="{% url 'journals:manage_metadata' %}">general metadata management page</a>.</p> +</div> {% endblock content %} diff --git a/scipost_django/journals/templates/journals/add_author_list.html b/scipost_django/journals/templates/journals/add_author_list.html new file mode 100644 index 0000000000000000000000000000000000000000..075284380e764e60acea380b03d7b9844626a363 --- /dev/null +++ b/scipost_django/journals/templates/journals/add_author_list.html @@ -0,0 +1,51 @@ +<table class = "ms-4" id="add_author_list"> + <tr> + <th> </th> + <th class="ps-3"> # </th> + <th class="ps-2"> Author </th> + <th class="ps-4"> Profile </th> + </tr> + {% for author in author_list %} + <tr> + <td> <input type="radio" name="selected_author_id" value="{{ forloop.counter }}" form = "add_author_form" id="{{ forloop.counter }}" {% if forloop.counter == selected_author_id %}checked{% endif %}> </td> + <td class="ps-3"><label for="{{ forloop.counter }}">{{ forloop.counter }}.</label></td> + <td class="ps-2"> <label for="{{ forloop.counter }}"> + {{ author.tex_name }} + <sup> {{author.affiliations|join:", "}} </sup> </label> + </td> + <td class="ps-4"> + {% if not author.profile %} + {% if author.other_with_same_name > 1 %} + <span class="badge bg-danger text-white" + title="There are potentially {{author.other_with_same_name}} profiles matching this author." + data-bs-html="true" + data-bs-toggle="tooltip" + >{% include "bi/people-fill.html" %}</span> + {% else %} + <a class="badge bg-danger text-white" + href="{% url 'profiles:profile_create' %}?first_name={{author.first_name_guess}}&last_name={{author.last_name_guess}}" + target="_blank" + hx-prompt="Enter a string" + hx-confirm="Are you sure?" + > Create a profile </a> + {% endif %} + + {% else %} + {% if author.profile.contributor %} + <a href="{{ author.profile.contributor.get_absolute_url }}">{{ author.profile.full_name }}</a> + {% else %} + <a href="{{ author.profile.get_absolute_url }}">{{ author.profile.full_name }}</a> + {% endif %} + + {% if author.has_name_warning %} + <span class="badge bg-warning text-dark" + title="The name in the tex file is different from the name in the profile." + data-bs-html="true" + data-bs-toggle="tooltip" + >!</span> + {% endif %} + {% endif %} + </td> + </tr> + {% endfor %} +</table> \ No newline at end of file diff --git a/scipost_django/journals/templatetags/publication_administration.py b/scipost_django/journals/templatetags/publication_administration.py index 140a9f1443934175bb7822b3036d38050f9d2f97..366678d0ccff3c238e20696fad6c014f86f7ab5f 100644 --- a/scipost_django/journals/templatetags/publication_administration.py +++ b/scipost_django/journals/templatetags/publication_administration.py @@ -24,7 +24,7 @@ def authors_in_right_order(publication): return False list_of_authors = publication.author_list.split(",") for author in publication.authors.all(): - if author.last_name not in list_of_authors[author.order - 1]: + if author.last_name and author.last_name not in list_of_authors[author.order - 1]: return False return True diff --git a/scipost_django/journals/urls/general.py b/scipost_django/journals/urls/general.py index 603659cf16f01104c026f357d8a0a0a727e1a665..d77134e943d6ab94d8e5805ee1de5b8af556a97e 100644 --- a/scipost_django/journals/urls/general.py +++ b/scipost_django/journals/urls/general.py @@ -100,6 +100,11 @@ urlpatterns = [ journals_views.add_author, name="add_author", ), + path( + "admin/<publication_doi_label:doi_label>/authors/reset_authors", + journals_views.reset_authors, + name="reset_authors", + ), path( "admin/<publication_doi_label:doi_label>/manage_metadata/", journals_views.manage_metadata, diff --git a/scipost_django/journals/views.py b/scipost_django/journals/views.py index ac7501113f7a9b2db3759654d288790219602080..a8132282bb21b29a18f97574e5fb2d71670fe9df 100644 --- a/scipost_django/journals/views.py +++ b/scipost_django/journals/views.py @@ -6,6 +6,7 @@ from decimal import Decimal, getcontext import hashlib import json import os +from profiles.models import Profile import random import string import shutil @@ -38,7 +39,7 @@ from django.db.models import ( Subquery, prefetch_related_objects, ) -from django.http import Http404, HttpResponse +from django.http import Http404, HttpRequest, HttpResponse from django.template.response import TemplateResponse from django.utils import timezone from django.utils.decorators import method_decorator @@ -926,7 +927,35 @@ def manage_metadata(request, doi_label=None): @permission_required("scipost.can_draft_publication", return_403=True) @transaction.atomic -def add_author(request, doi_label): +def reset_authors(request, doi_label): + publication = get_object_or_404(Publication, doi_label=doi_label) + if not publication.is_draft and not request.user.has_perm( + "can_publish_accepted_submission" + ): + raise Http404("You do not have permission to edit this non-draft Publication") + + reset_author_associations(publication) + + return redirect(reverse("journals:add_author", kwargs={"doi_label": doi_label})) + + +def reset_author_associations(publication_object: Publication) -> None: + """Deletes all PublicationAuthorsTable entries for the given publication_object and repopulates it based on the author_list.""" + PublicationAuthorsTable.objects.filter(publication=publication_object).delete() + + author_query_list = search_for_authors(publication_object) + author_dict_list = build_author_dict_list(publication_object, author_query_list) + + # We add the succesful authors from the author_dict_list to the PublicationAuthorsTable. + for author_dict in author_dict_list: + PublicationAuthorsTable.objects.create( + publication=publication_object, profile=author_dict["profile"] + ) + + +@permission_required("scipost.can_draft_publication", return_403=True) +@transaction.atomic +def add_author(request, doi_label: str) -> HttpResponse: """ Link authors (via their Profile) to a Publication. @@ -941,10 +970,21 @@ def add_author(request, doi_label): ): raise Http404("You do not have permission to edit this non-draft Publication") + authors_metadata_table = PublicationAuthorsTable.objects.filter( + publication=publication + ) + if request.method == "GET" and not authors_metadata_table.exists(): + reset_author_associations(publication) + form = ProfileSelectForm(request.POST or None) if request.POST and form.is_valid(): + author_order = request.POST["selected_author_id"] + # We remove previous request.POST["selected_author_id"] and add a new one there. + authors_metadata_table.filter(order=author_order).delete() table, created = PublicationAuthorsTable.objects.get_or_create( - publication=publication, profile=form.cleaned_data["profile"] + publication=publication, + profile=form.cleaned_data["profile"], + order=author_order, ) if created: messages.success(request, "Added {} as an author.".format(table.profile)) @@ -956,14 +996,97 @@ def add_author(request, doi_label): "Publication.".format(table.profile) ), ) - return redirect(publication.get_absolute_url()) + + author_query_list = search_for_authors(publication) + author_dict_list = build_author_dict_list(publication, author_query_list) + + return render( + request, + "journals/add_author_list.html", + { + "author_list": author_dict_list, + "selected_author_id": int(request.POST["selected_author_id"]), + }, + ) + + author_query_list = search_for_authors(publication) + author_dict_list = build_author_dict_list(publication, author_query_list) + context = { "publication": publication, "form": form, + "author_list": author_dict_list, } return render(request, "journals/add_author.html", context) +def search_for_authors(publication_object: Publication) -> list: + """Creates a list of (Profile) queryset objects based on the publication_object's.author_list [string].""" + author_profile_query_list = [] + + authors_metadata_table = PublicationAuthorsTable.objects.filter( + publication=publication_object + ) + + for i, tex_author in enumerate( + publication_object.author_list.split(", ") + ): # Author1, Author2, ... + # If an author is already on the index of authors_metadata_table, do not search. + if ( + not authors_metadata_table.exists() + or i + 1 > len(authors_metadata_table) + or authors_metadata_table[i].is_empty() + ): + query_results = Profile.objects.search(tex_author) + else: + print("Skipping search for author: ", tex_author) + query_results = Profile.objects.filter( + id=authors_metadata_table[i].profile.id + ) + + author_profile_query_list.append(query_results) + + return author_profile_query_list + + +def build_author_dict_list( + publication_object: Publication, author_profile_query_list: list +) -> list: + """Creates a list of dictionaries for each author in the publication_object's.author_list [string]. This list is used to render the author list in the add_author view.""" + author_dict_lsit = [] + + for author_profile_query, tex_author in zip( + author_profile_query_list, publication_object.author_list.split(", ") + ): # Author1, Author2, ... + author_profile = ( + author_profile_query.first() if len(author_profile_query) < 2 else None + ) + has_name_warning = ( + author_profile.full_name != tex_author if author_profile else False + ) + other_with_same_name = len( + author_profile_query + ) # To handle duplicate warnings. + + # We assume the first and last names in order to automate 2-word profile creation. + first_name_guess = tex_author.split(" ")[0] + last_name_guess = " ".join(tex_author.split(" ")[1:]) + + author_dict_lsit.append( + { + "tex_name": tex_author, + "profile": author_profile, + "affiliations": [1, 2], + "first_name_guess": first_name_guess, + "last_name_guess": last_name_guess, + "has_name_warning": has_name_warning, + "other_with_same_name": other_with_same_name, + } + ) + + return author_dict_lsit + + class AuthorAffiliationView(PublicationMixin, PermissionsMixin, DetailView): """ Handle the author affiliations for a Publication. @@ -1207,13 +1330,21 @@ class AbstractJATSUpdateView( form_class = AbstractJATSForm template_name = "journals/create_abstract_jats.html" - + def get_success_url(self): return reverse( "journals:abstract_jats", kwargs={"doi_label": self.object.doi_label}, ) + def post(self, request: HttpRequest, *args: str, **kwargs: Any) -> HttpResponse: + + if request.POST.get("convert_button"): + # Change the abstract_jats field value to "LMAO" + self.get_form().data["abstract_jats"] = "LMAO" + + return super().post(request, *args, **kwargs) + class FundingInfoView( PublicationMixin, ProdSupervisorPublicationPermissionMixin, UpdateView diff --git a/scipost_django/templates/bi/people-fill.html b/scipost_django/templates/bi/people-fill.html new file mode 100644 index 0000000000000000000000000000000000000000..6361ec1e49ebf6823008f3a2fec3547ce423435a --- /dev/null +++ b/scipost_django/templates/bi/people-fill.html @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-people-fill" viewBox="0 0 16 16"> + <path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6m-5.784 6A2.24 2.24 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.3 6.3 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1zM4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5"/> + </svg> \ No newline at end of file