From d4e9e58f17973595ecc98eeb1846254b4ead1968 Mon Sep 17 00:00:00 2001 From: George Katsikas <giorgakis.katsikas@gmail.com> Date: Fri, 14 Feb 2025 17:21:41 +0100 Subject: [PATCH] =?UTF-8?q?refactor(metadata):=20=E2=99=BB=EF=B8=8F=20simp?= =?UTF-8?q?lify=20and=20beautify=20specifying=20author=20affiliations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More reliable method of extracting affiliations. Improve table presentation. Sticky form submit buttons. Use clientside validation for the form. Allow non-numerical identifiers for affiliations. --- scipost_django/journals/models/publication.py | 40 ++---- .../journals/author_affiliations.html | 115 +++++++++++------- .../journals/author_affiliations_orgcell.html | 34 ++++-- scipost_django/journals/views.py | 64 ++++------ 4 files changed, 136 insertions(+), 117 deletions(-) diff --git a/scipost_django/journals/models/publication.py b/scipost_django/journals/models/publication.py index 5016779bb..d355dc29a 100644 --- a/scipost_django/journals/models/publication.py +++ b/scipost_django/journals/models/publication.py @@ -590,32 +590,18 @@ class Publication(models.Model): | models.Q(doi_label__startswith=f"{doi_anchor}-") ).order_by("doi_label") - @staticmethod - def extract_affiliations_from_tex(tex_contents: str) -> list[str]: - """ - Extracts the affiliations from the TeX contents, constructing a list with each affiliation as a string. - This list is used to render the affiliation list in the add_author view. - """ - affiliation_field = re.findall("%%%%%%%%%% TODO: AFFILIATIONS(.*?)%%%%%%%%%% END TODO: AFFILIATIONS", tex_contents, re.DOTALL)[0] - affiliation_texts = affiliation_field.strip().split("\n\\\\") - - affiliation_list = [] - for affiliation_text in affiliation_texts: - affiliation = re.findall("{\\\\bf\s*\d+} (.*)", affiliation_text, re.DOTALL) or [affiliation_text] - affiliation = affiliation[0].strip() - affiliation_list.append(affiliation) - - return affiliation_list - - def construct_affiliation_list(self) -> list[str]: + @cached_property + def tex_affiliations(self) -> list[tuple[str, str]]: """ - Constructs a list of affiliations from the TeX file of the publication. - This list is used to render the affiliation list in the add_author view. + Extracts the affiliations from the TeX contents, constructing a list + with each affiliation as an (identifier, text) pair + Matches the pattern: `{\\bf #} Affiliation Text ...\\\\` + Returns a list of tuples with the affiliation identifier (number) and the affiliation text. """ - if self.tex_contents: - return Publication.extract_affiliations_from_tex(self.tex_contents) - return [] + return re.findall( + r"{\\bf (.+?)\}\s?(.+?)(?:\\\\\n|%{5,})", self.tex_contents, re.DOTALL + ) @cached_property def tex_contents(self) -> str | None: @@ -624,7 +610,7 @@ class Publication(models.Model): return self.proofs_repository.fetch_tex() - def construct_tex_author_info(self) -> tuple[list[str], list[list[int]]]: + def construct_tex_author_info(self) -> tuple[list[str], list[list[str]]]: """Puts together information for each author from the TeX file of the publication.""" tex_contents = self.tex_contents or "" @@ -661,8 +647,8 @@ class Publication(models.Model): # If no affiliation is present, we add them all. if not has_affiliation: - total_affiliations = len(Publication.extract_affiliations_from_tex(tex_contents)) - default_affiliations = list(range(1, total_affiliations +1)) + total_affiliations = len(self.tex_affiliations) + default_affiliations = list(range(1, total_affiliations + 1)) affiliation_list.append(default_affiliations) continue @@ -670,7 +656,7 @@ class Publication(models.Model): # We remove anything following the first "$" character which separates affiliations from emails. affiliations = superscript.split("$")[0].strip().split(",") - affiliations = [int(aff.strip()) for aff in affiliations] + affiliations = [aff.strip() for aff in affiliations] affiliation_list.append(affiliations) return author_list, affiliation_list diff --git a/scipost_django/journals/templates/journals/author_affiliations.html b/scipost_django/journals/templates/journals/author_affiliations.html index f277f632a..a752216b9 100644 --- a/scipost_django/journals/templates/journals/author_affiliations.html +++ b/scipost_django/journals/templates/journals/author_affiliations.html @@ -1,6 +1,8 @@ {% extends 'scipost/base.html' %} -{% block pagetitle %}: Author Affiliations{% endblock pagetitle %} +{% block pagetitle %} + : Author Affiliations +{% endblock pagetitle %} {% load bootstrap %} {% load common_extras %} @@ -19,51 +21,76 @@ {% block content %} - <div class="row"> - <div class="col-12"> - <h1 class="highlight">Author affiliations for <a href="{{ publication.get_absolute_url }}">{{ publication.doi_label }}</a></h1> - <br> - <div> - <p>Can't find it in the selector? <a href="{% url 'organizations:organization_create' %}" target="_blank">Add a new organization to our database</a> (opens in new window)</p> - </div> - - <div class="d-flex flex-row gap-2"> - <form hx-post="{% url 'journals:author_affiliations' doi_label=publication.doi_label%}" id="add_affiliation_form" hx-swap="outerHTML"> - {% csrf_token %} - {{ form|bootstrap_inline }} - <input name="add" class="btn btn-primary" type="submit" value="Add Organization" form="add_affiliation_form"/> - </form> - <form action="{% url 'journals:author_affiliations' doi_label=publication.doi_label%}" id="submit_affiliation_form" method="POST"> - {% csrf_token %} - <input name="submit" class="btn btn-secondary" type="submit" value="Submit" form="submit_affiliation_form"/> - <input type="hidden" name="total_affiliations" value="{{ affiliation_texts|length }}"/> - </form> - </div> - - <br> - <h3>List of affiliations:</h3> - <table class = "ms-4" id="author_affiliations_list"> + <h1 class="highlight"> + Author affiliations for <a href="{{ publication.get_absolute_url }}">{{ publication.doi_label }}</a> + </h1> + <br /> + <div> + <p> + Can't find it in the selector? <a href="{% url 'organizations:organization_create' %}" target="_blank">Add a new organization to our database</a> (opens in new window) + </p> + </div> + + <div class="position-relative"> + + <form class="d-flex flex-row gap-2 p-2 position-sticky top-0 bg-white" + hx-post="{% url 'journals:author_affiliations' doi_label=publication.doi_label %}" + id="add_affiliation_form" + hx-swap="outerHTML"> + <input name="add" + class="btn btn-primary" + type="submit" + value="Add Organization" + form="add_affiliation_form" /> + {{ form|bootstrap_inline }} + </form> + + <h3>List of affiliations:</h3> + <form action="{% url 'journals:author_affiliations' doi_label=publication.doi_label %}" + id="submit_affiliation_form" + method="POST"> + {% csrf_token %} + <table class="table mb-4" id="author_affiliations_list"> <tr> - <th> </th> - <th class="ps-3"> # </th> - <th class="ps-2"> Affiliation Text </th> - <th class="ps-4"> Organization </th> + <th></th> + <th>#</th> + <th>Affiliation Text</th> + <th>Organization</th> </tr> - {% for affiliation_tex, default_affiliation in affiliation_texts|zip_dj:default_affiliations %} - <tr> - <td> <input type="radio" name="checked_row_id" value="{{ forloop.counter }}" form = "add_affiliation_form" id="{{ forloop.counter }}" {% if forloop.counter == checked_row_id %}checked{% endif %}> </td> - <td class="ps-3"><label for="{{ forloop.counter }}">{{ forloop.counter }}.</label></td> - <td class="ps-2"><label for="{{ forloop.counter }}">{{affiliation_tex}}</label></td> - {% include "journals/author_affiliations_orgcell.html" with checked_row_id=forloop.counter organization=default_affiliation %} - </tr> - {% endfor %} - </table> + + {% for identifier, text, organization in affiliations %} + <tr class="align-middle"> + <td> + <input class="me-2" type="radio" name="checked_row_id" value="{{ identifier }}" form = "add_affiliation_form" id="{{ identifier }}" + {% if identifier == checked_row_id %}checked{% endif %} + /> + </td> + <td> + <label for="{{ identifier }}">{{ identifier }}.</label> + </td> + <td> + <label class="w-100" for="{{ identifier }}">{{ text }}</label> + </td> + {% include "journals/author_affiliations_orgcell.html" with checked_row_id=identifier organization=organization %} + </tr> + {% endfor %} + + + </table> + <input name="submit" + class="btn btn-primary" + type="submit" + value="Submit" + form="submit_affiliation_form" /> + </form> + </div> - </div> -{% endblock %} + {% endblock %} + + -{% block footer_script %} - {{ block.super }} - {{ form.media }} -{% endblock footer_script %} + {% block footer_script %} + {{ block.super }} + {{ form.media }} + {% endblock footer_script %} diff --git a/scipost_django/journals/templates/journals/author_affiliations_orgcell.html b/scipost_django/journals/templates/journals/author_affiliations_orgcell.html index f6752ba99..235b2b35f 100644 --- a/scipost_django/journals/templates/journals/author_affiliations_orgcell.html +++ b/scipost_django/journals/templates/journals/author_affiliations_orgcell.html @@ -1,9 +1,25 @@ -<td class="ps-4" id="organization_column_{{checked_row_id}}"> - <input type="hidden" value="{{organization.id}}" form="submit_affiliation_form" name="affiliation_id_{{checked_row_id}}"> - {% if organization is not None %} - <img src="{{ organization.country.flag }}" style="width:15px;" alt="{{ organization.country }} flag" title="{{organization.get_country_display}}" data-bs-toggle="tooltip"/> - <a href="{{ organization.get_absolute_url }}">{{ organization.name }} </a> - {% else %} - -- No affiliation -- - {% endif %} -</td> \ No newline at end of file +<td id="organization_column_{{ checked_row_id }}"> + + {% if organization is not None %} + <input type="hidden" + value="{{ organization.id }}" + form="submit_affiliation_form" + name="affiliation_id_{{ checked_row_id }}" + required /> + <img src="{{ organization.country.flag }}" + style="width:15px" + alt="{{ organization.country }} flag" + title="{{ organization.get_country_display }}" + data-bs-toggle="tooltip" /> + <a href="{{ organization.get_absolute_url }}">{{ organization.name }}</a> + {% else %} + <div class="text-nowrap"> + <input type="text" + form="submit_affiliation_form" + name="affiliation_id_{{ checked_row_id }}" + placeholder="-- No affiliation --" + required /> + </div> + {% endif %} + +</td> diff --git a/scipost_django/journals/views.py b/scipost_django/journals/views.py index 013267520..79cfd1bf7 100644 --- a/scipost_django/journals/views.py +++ b/scipost_django/journals/views.py @@ -1043,28 +1043,32 @@ def build_author_render_list( @permission_required("scipost.can_draft_publication", return_403=True) @transaction.atomic def author_affiliations(request, doi_label: str) -> HttpResponse: - def _affiliations_from_post(request: HttpRequest) -> list[Organization | None]: - """Iterate over the POST request keys 'affiliation_id_k' to build a list of affiliation Organization objects.""" - total_affiliations_from_tex = int(request.POST.get("total_affiliations")) - - affiliations = [] - for i in range(total_affiliations_from_tex): - affiliation_id = request.POST.get(f"affiliation_id_{i +1}") - affiliation_id = int(affiliation_id) if affiliation_id else None - affiliation = Organization.objects.filter(pk=affiliation_id).first() - affiliations.append(affiliation) - - return affiliations - - # ------------- 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") + raise PermissionDenied( + "You do not have permission to edit this non-draft Publication" + ) form = AuthorsTableOrganizationSelectForm(request.POST or None) + organizations = { + identifier: Organization.objects.filter(pk=value).first() + if request.POST and (value := request.POST.get(f"affiliation_id_{identifier}")) + else None + for identifier, _ in publication.tex_affiliations + } + affiliations = [ + (i, t, o) + for (i, t), o in zip(publication.tex_affiliations, organizations.values()) + ] + context = { + "publication": publication, + "form": form, + "affiliations": affiliations, + } + if request.POST: if not request.POST.get("submit"): context = { @@ -1084,11 +1088,13 @@ def author_affiliations(request, doi_label: str) -> HttpResponse: return response else: # First we need to check if all of the affiliations exist. - affiliations = _affiliations_from_post(request) - total_affiliations_from_tex = int(request.POST.get("total_affiliations")) + total_affiliations_from_tex = len( + list(filter(lambda x: x.startswith("affiliation_id_"), request.POST)) + ) total_non_empty_affiliations = len( - list(filter(lambda x: x is not None, affiliations)) + list(filter(lambda x: x is not None, organizations.values())) ) + print(total_affiliations_from_tex, total_non_empty_affiliations) if total_non_empty_affiliations != total_affiliations_from_tex: messages.warning( @@ -1097,24 +1103,15 @@ def author_affiliations(request, doi_label: str) -> HttpResponse: % publication.doi_label, ) - tex_affiliations = publication.construct_affiliation_list() - context = { - "publication": publication, - "form": form, - "affiliation_texts": tex_affiliations, - "default_affiliations": affiliations, # To reconstruct the form. - } return render(request, "journals/author_affiliations.html", context) else: authors = PublicationAuthorsTable.objects.filter( publication=publication ) - authors_list, affiliations_of_authors = ( - publication.construct_tex_author_info() - ) - for author, their_affiliations in zip(authors, affiliations_of_authors): + _, author_aff_ids = publication.construct_tex_author_info() + for author, aff_ids in zip(authors, author_aff_ids): author.affiliations.set( - [affiliations[index - 1] for index in their_affiliations] + [organizations.get(i) for i in aff_ids] or None ) publication.cf_author_affiliation_indices_list = [] @@ -1122,13 +1119,6 @@ def author_affiliations(request, doi_label: str) -> HttpResponse: return publication_detail(request, doi_label) - tex_affiliations = publication.construct_affiliation_list() - context = { - "publication": publication, - "form": form, - "affiliation_texts": tex_affiliations, - "default_affiliations": [None] * len(tex_affiliations), - } return render(request, "journals/author_affiliations.html", context) -- GitLab