diff --git a/scipost_django/common/constants.py b/scipost_django/common/constants.py index 31499ef62d1adb514750e1ad2a0f8bf90395838f..e925a1d65bf953ad34bd28285d6b687dcb995e9f 100644 --- a/scipost_django/common/constants.py +++ b/scipost_django/common/constants.py @@ -17,72 +17,31 @@ CHARACTER_ALTERNATIVES = { "Ü": "Ue", } -CHARACTER_UNACCENTED = { - "à ": "a", - "À": "A", - "á": "a", - "Ã": "A", - "â": "a", - "Â": "A", - "ä": "ae", - "Ä": "Ae", - "ã": "a", - "Ã": "A", - "Ã¥": "a", - "Ã…": "A", - "ç": "c", - "Ç": "C", - "ć": "c", - "Ć": "c", - "é": "e", - "É": "E", - "è": "e", - "È": "E", - "ê": "e", - "Ê": "E", - "ë": "e", - "Ë": "E", - "Ä™": "e", - "Ę": "E", - "Ã": "i", - "Ã": "I", - "ì": "i", - "ÃŒ": "I", - "î": "i", - "ÃŽ": "I", - "ï": "i", - "Ã": "I", - "Å‚": "l", - "Å": "L", - "ñ": "n", - "Ñ": "N", - "Å„": "n", - "Ń": "N", - "ó": "o", - "Ó": "O", - "ò": "o", - "Ã’": "O", - "ô": "o", - "Ô": "O", - "ö": "oe", - "Ö": "Oe", - "õ": "o", - "Õ": "O", +# Character latinisations are used to convert foreign letters +# to their latinised equivalents / lookalikes. +CHARACTER_LATINISATIONS = { + "æ": "ae", + "Æ": "Ae", + "Å“": "oe", + "Å’": "Oe", + "ß": "ss", + "ð": "d", + "Ã": "D", "ø": "o", "Ø": "O", - "Å›": "s", - "Åš": "S", - "ß": "ss", - "ú": "u", - "Ú": "U", - "ù": "u", - "Ù": "U", - "û": "u", - "Û": "U", - "ü": "ue", - "Ü": "Ue", - "ź": "z", - "Ź": "Z", - "ż": "z", - "Å»": "Z", + "Å‚": "l", + "Å": "L", + "ij": "ij", + "IJ": "IJ", + "Å‹": "ng", + "ÅŠ": "Ng", + "ȶ": "t", + "È·": "j", + "ɉ": "j", + "É‹": "q", + "µ": "u", + "√": "v", + "aÌ": "a", + "ą": "a", + "ı": "i", } diff --git a/scipost_django/common/utils.py b/scipost_django/common/utils.py index 5d3e35bd2c167aae79f70c2bad8ad110f62dfa7b..9e5b7fd34c56694cbd0d6bff988f7e0296134f81 100644 --- a/scipost_django/common/utils.py +++ b/scipost_django/common/utils.py @@ -9,17 +9,62 @@ from django.core.mail import EmailMultiAlternatives from django.db.models import Q from django.template import loader -from .constants import CHARACTER_ALTERNATIVES, CHARACTER_UNACCENTED +from .constants import CHARACTER_ALTERNATIVES, CHARACTER_LATINISATIONS +import unicodedata -def unaccent(text): + +def unaccent(text: str) -> str: + """ + Remove accented characters in the given string (e.g. é -> e), + with the exception of the German umlauts (e.g. ö -> oe). + """ + UMLAUT = "\u0308" + + unaccented_text = "" + for char in unicodedata.normalize("NFD", text): + char_category = unicodedata.category(char) + + if char_category != "Mn": + unaccented_text += char + elif char == UMLAUT: + unaccented_text += "e" + + return unaccented_text + + +def latinise(text: str) -> str: """ - Replace accented characters by unaccented ones. + Convert accented characters in the given string to their + latinised equivalents / lookalikes (e.g. ö -> o). """ - unaccented = text - for key, val in CHARACTER_UNACCENTED.items(): - unaccented = unaccented.replace(key, val) - return unaccented + latinised_text = "" + for char in unicodedata.normalize("NFD", text): + char_category = unicodedata.category(char) + + translated_char = char + is_latin = ord(char) < 128 + + # Keep spaces and dashes + if char in [" ", "-", "–"]: + pass + # Remove apostrophes and parentheses + elif char in ["'", "’", "(", ")"]: + translated_char = "" + # Translate only letters, symbols and punctuation + # skipping numbers and other characters (e.g. diacritics) + elif char_category[0] in ["L", "S", "P"] and not is_latin: + translated_char = CHARACTER_LATINISATIONS.get(char, "") + + # Remove everything not in the ASCII range + translated_char = translated_char.encode("ascii", "ignore").decode("utf-8") + + latinised_text += translated_char + + # Remove multiple spaces + latinised_text = " ".join(latinised_text.split()) + + return latinised_text def alternative_spellings(text): diff --git a/scipost_django/proceedings/forms.py b/scipost_django/proceedings/forms.py index 17c0d8fcdceb6ebfe2aef528b05f666696d49532..0ab15c35344ab4656590d595338c97ea30d85021 100644 --- a/scipost_django/proceedings/forms.py +++ b/scipost_django/proceedings/forms.py @@ -23,8 +23,3 @@ class ProceedingsForm(forms.ModelForm): "submissions_close", "template_latex_tgz", ) - - -class ProceedingsMultipleChoiceField(forms.ModelMultipleChoiceField): - def label_from_instance(self, obj): - return obj.event_suffix or obj.event_name diff --git a/scipost_django/production/admin.py b/scipost_django/production/admin.py index 645fc77ac952b5335078de76f3639077e25a2bcb..191991474634f31e30458f52907baebabc549d61 100644 --- a/scipost_django/production/admin.py +++ b/scipost_django/production/admin.py @@ -15,6 +15,8 @@ from .models import ( ProofsRepository, ) +from django.utils.html import format_html + def event_count(obj): return obj.events.count() @@ -102,11 +104,17 @@ class ProofsRepositoryAdmin(GuardedModelAdmin): search_fields = [ "stream__submission__author_list", "stream__submission__title", - "stream__submission__preprint__identifier_w_vn_nr", + "name", ] + list_filter = ["status"] - list_display = ["stream", "status", "git_path"] - readonly_fields = ["template_path", "git_path"] + list_display = ["name", "status", "gitlab_link"] + readonly_fields = ["stream", "template_path", "gitlab_link"] + + def gitlab_link(self, obj): + return format_html( + '<a href="{1}" target="_blank">{0}</a>', obj.git_path, obj.git_url + ) admin.site.register(ProofsRepository, ProofsRepositoryAdmin) diff --git a/scipost_django/production/forms.py b/scipost_django/production/forms.py index 44af1bec6e71cdcafa5fb6ef831d8e76ab60ed64..c944cc1f8cc1a837159c57021910cb0c7bcdacb4 100644 --- a/scipost_django/production/forms.py +++ b/scipost_django/production/forms.py @@ -3,11 +3,12 @@ __license__ = "AGPL v3" import datetime +from typing import Dict from django import forms from django.contrib.auth import get_user_model -from django.db.models import Max -from django.db.models.functions import Greatest +from django.db.models import Max, Value, Q +from django.db.models.functions import Greatest, Coalesce, NullIf from django.contrib.sessions.backends.db import SessionStore from crispy_forms.helper import FormHelper @@ -18,7 +19,6 @@ from django.urls import reverse from journals.models import Journal from markup.widgets import TextareaWithPreview from proceedings.models import Proceedings -from proceedings.forms import ProceedingsMultipleChoiceField from scipost.fields import UserModelChoiceField from . import constants @@ -281,36 +281,59 @@ class ProofsDecisionForm(forms.ModelForm): class ProductionStreamSearchForm(forms.Form): - accepted_in = forms.ModelMultipleChoiceField( - queryset=Journal.objects.active(), + author = forms.CharField(max_length=100, required=False, label="Author(s)") + title = forms.CharField(max_length=512, required=False) + identifier = forms.CharField(max_length=128, required=False) + + all_streams = ProductionStream.objects.ongoing() + journal = forms.MultipleChoiceField( + choices=Journal.objects.active() + .filter( + id__in=all_streams.values_list( + "submission__editorialdecision__for_journal", flat=True + ) + ) + .order_by("name") + .values_list("id", "name"), required=False, ) - proceedings = ProceedingsMultipleChoiceField( - queryset=Proceedings.objects.order_by("-submissions_close"), + proceedings = forms.MultipleChoiceField( + choices=Proceedings.objects.all() + .filter(id__in=all_streams.values_list("submission__proceedings", flat=True)) + .order_by("-submissions_close") + # Short name is `event_suffix` if set, otherwise `event_name` + .annotate(short_name=Coalesce(NullIf("event_suffix", Value("")), "event_name")) + .values_list("id", "short_name") + .distinct(), required=False, ) - author = forms.CharField(max_length=100, required=False, label="Author(s)") - title = forms.CharField(max_length=512, required=False) - identifier = forms.CharField(max_length=128, required=False) - officer = forms.ModelChoiceField( - queryset=ProductionUser.objects.active().filter( - user__groups__name="Production Officers" - ), + officer = forms.MultipleChoiceField( + choices=[(0, "Unassigned")] + + [ + (prod_user.id, str(prod_user)) + for prod_user in ProductionUser.objects.active() + .filter(id__in=all_streams.values_list("officer", flat=True)) + .order_by("-user__id") + .distinct() + ], required=False, - empty_label="Any", ) - supervisor = forms.ModelChoiceField( - queryset=ProductionUser.objects.active().filter( - user__groups__name="Production Supervisor" - ), + supervisor = forms.MultipleChoiceField( + choices=[(0, "Unassigned")] + + [ + (prod_user.id, str(prod_user)) + for prod_user in ProductionUser.objects.active() + .filter(id__in=all_streams.values_list("supervisor", flat=True)) + .order_by("-user__id") + .distinct() + ], required=False, - empty_label="Any", ) status = forms.MultipleChoiceField( # Use short status names from their internal (code) name choices=[ (status_code_name, status_code_name.replace("_", " ").title()) - for status_code_name, _ in constants.PRODUCTION_STREAM_STATUS + for status_code_name, _ in constants.PRODUCTION_STREAM_STATUS[:-2] ], required=False, ) @@ -352,39 +375,50 @@ class ProductionStreamSearchForm(forms.Form): self.helper = FormHelper() self.helper.layout = Layout( - Div( - Div(FloatingField("identifier"), css_class="col-md-3 col-4"), - Div(FloatingField("author"), css_class="col-md-3 col-8"), - Div(FloatingField("title"), css_class="col-md-6"), - css_class="row mb-0 mt-2", - ), Div( Div( Div( - Div(Field("accepted_in", size=5), css_class="col-sm-8"), - Div(Field("proceedings", size=5), css_class="col-sm-4"), - css_class="row mb-0", - ), - Div( - Div(Field("supervisor"), css_class="col-6"), - Div(Field("officer"), css_class="col-6"), + Div(FloatingField("identifier"), css_class="col-4"), + Div(FloatingField("author"), css_class="col-8"), + Div(FloatingField("title"), css_class="col-12"), css_class="row mb-0", ), + css_class="col-12 col-md-8", + ), + Div( Div( - Div(Field("orderby"), css_class="col-6"), - Div(Field("ordering"), css_class="col-6"), + Div(FloatingField("orderby"), css_class="col-6 col-md-12"), + Div(FloatingField("ordering"), css_class="col-6 col-md-12"), css_class="row mb-0", ), - css_class="col-sm-9", + css_class="col-12 col-md-4", ), + css_class="row mb-0 mt-2", + ), + Div( + Div(Field("journal", size=10), css_class="col-6 col-md-4 col-lg"), Div( - Field("status", size=len(constants.PRODUCTION_STREAM_STATUS)), - css_class="col-sm-3", + Field("proceedings", size=10), + css_class="col-6 col-md-8 col-lg d-none d-md-block", ), + Div(Field("status", size=10), css_class="col-6 col-md-4 col-lg"), + Div(Field("supervisor", size=10), css_class="col-6 col-md-4 col-lg"), + Div(Field("officer", size=10), css_class="col-6 col-md-4 col-lg"), css_class="row mb-0", ), ) + def apply_filter_set(self, filters: Dict, none_on_empty: bool = False): + # Apply the filter set to the form + for key in self.fields: + if key in filters: + self.fields[key].initial = filters[key] + elif none_on_empty: + if isinstance(self.fields[key], forms.MultipleChoiceField): + self.fields[key].initial = [] + else: + self.fields[key].initial = None + def search_results(self): # Save the form data to the session if self.session_key is not None: @@ -393,23 +427,6 @@ class ProductionStreamSearchForm(forms.Form): for key in self.cleaned_data: session[key] = self.cleaned_data.get(key) - session["accepted_in"] = ( - [journal.id for journal in session.get("accepted_in")] - if (session.get("accepted_in")) - else [] - ) - session["proceedings"] = ( - [proceedings.id for proceedings in session.get("proceedings")] - if (session.get("proceedings")) - else [] - ) - session["officer"] = ( - officer.id if (officer := session.get("officer")) else None - ) - session["supervisor"] = ( - supervisor.id if (supervisor := session.get("supervisor")) else None - ) - session.save() streams = ProductionStream.objects.ongoing() @@ -418,13 +435,6 @@ class ProductionStreamSearchForm(forms.Form): latest_activity_annot=Greatest(Max("events__noted_on"), "opened", "closed") ) - if accepted_in := self.cleaned_data.get("accepted_in"): - streams = streams.filter( - submission__editorialdecision__for_journal__in=accepted_in, - ) - if proceedings := self.cleaned_data.get("proceedings"): - streams = streams.filter(submission__proceedings__in=proceedings) - if identifier := self.cleaned_data.get("identifier"): streams = streams.filter( submission__preprint__identifier_w_vn_nr__icontains=identifier, @@ -434,12 +444,31 @@ class ProductionStreamSearchForm(forms.Form): if title := self.cleaned_data.get("title"): streams = streams.filter(submission__title__icontains=title) - if officer := self.cleaned_data.get("officer"): - streams = streams.filter(officer=officer) - if supervisor := self.cleaned_data.get("supervisor"): - streams = streams.filter(supervisor=supervisor) - if status := self.cleaned_data.get("status"): - streams = streams.filter(status__in=status) + def is_in_or_null(queryset, key, value, implicit_all=True): + """ + Filter a queryset by a list of values. If the list contains a 0, then + also include objects where the key is null. If the list is empty, then + include all objects if implicit_all is True. + """ + value = self.cleaned_data.get(value) + has_unassigned = "0" in value + is_unassigned = Q(**{key + "__isnull": True}) + is_in_values = Q(**{key + "__in": list(filter(lambda x: x != 0, value))}) + + if has_unassigned: + return queryset.filter(is_unassigned | is_in_values) + elif implicit_all and not value: + return queryset + else: + return queryset.filter(is_in_values) + + streams = is_in_or_null( + streams, "submission__editorialdecision__for_journal", "journal" + ) + streams = is_in_or_null(streams, "submission__proceedings", "proceedings") + streams = is_in_or_null(streams, "officer", "officer") + streams = is_in_or_null(streams, "supervisor", "supervisor") + streams = is_in_or_null(streams, "status", "status") if not self.user.has_perm("scipost.can_view_all_production_streams"): # Restrict stream queryset if user is not supervisor diff --git a/scipost_django/production/management/commands/advance_git_repos.py b/scipost_django/production/management/commands/advance_git_repos.py index c9640dfa9a3068a82b8b5c5e3ad888ea8ddc2e5c..4c93d9a3d62dd09ab9f43bcb2fb1a1b6473c5117 100644 --- a/scipost_django/production/management/commands/advance_git_repos.py +++ b/scipost_django/production/management/commands/advance_git_repos.py @@ -13,6 +13,7 @@ from common.utils import get_current_domain from gitlab import Gitlab from gitlab.v4.objects import Group, Project from gitlab.exceptions import GitlabGetError +from gitlab.const import AccessLevel import arxiv import requests @@ -42,7 +43,7 @@ class Command(BaseCommand): def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "--id", - type=int, + type=str, required=False, help="The submission preprint identifier to handle a specific submission, leave blank to handle all", ) @@ -197,6 +198,19 @@ class Command(BaseCommand): } ) + # Allow Developers to push to the protected "main" branch + # Protected branches lay on top of the branches. Deleting and recreating them is + # the only way to change their settings and does not affect the branches themselves + project.protectedbranches.delete("main") + project.protectedbranches.create( + { + "name": "main", + "merge_access_level": AccessLevel.MAINTAINER, + "push_access_level": AccessLevel.DEVELOPER, + "allow_force_push": False, + } + ) + self.stdout.write( self.style.SUCCESS(f"Copied pure templates to {repo.git_path}") ) @@ -241,12 +255,18 @@ class Command(BaseCommand): # Define the formatting functions def format_authors(authors: List[str]) -> str: + # Append a superscript to each author + authors = [ + author + "\\textsuperscript{" + str(i) + "}" + for i, author in enumerate(authors, start=1) + ] + *other_authors, last_author = authors if len(other_authors) == 0: return last_author else: - return ", ".join(other_authors) + " and " + last_author + return ",\n".join(other_authors) + "\nand " + last_author def format_title(title: str) -> str: return title + NEWLINE diff --git a/scipost_django/production/migrations/0007_auto_20230706_1502.py b/scipost_django/production/migrations/0007_auto_20230706_1502.py new file mode 100644 index 0000000000000000000000000000000000000000..347c2862fdd26f8de996c832f7350d2978878a78 --- /dev/null +++ b/scipost_django/production/migrations/0007_auto_20230706_1502.py @@ -0,0 +1,67 @@ +# Generated by Django 3.2.18 on 2023-07-06 13:02 + +from django.db import migrations, models + + +def add_name_to_repos(apps, schema_editor): + from common.utils import latinise + from django.db.models.functions import Concat + from django.db.models import Value + + ProofsRepository = apps.get_model("production", "ProofsRepository") + Profile = apps.get_model("profiles", "Profile") + + def _clean_author_list(authors_str: str): + comma_separated = authors_str.replace(", and", ", ") + comma_separated = comma_separated.replace(" and ", ", ") + comma_separated = comma_separated.replace(", & ", ", ") + comma_separated = comma_separated.replace(" & ", ", ") + comma_separated = comma_separated.replace(";", ", ") + return [e.lstrip().rstrip() for e in comma_separated.split(",")] + + def _get_repo_name(stream) -> str: + """ + Return the name of the repository in the form of "id_lastname". + """ + # Get the last name of the first author by getting the first author string from the submission + first_author_str = _clean_author_list(stream.submission.author_list)[0] + first_author_profile = ( + Profile.objects.annotate( + full_name=Concat("first_name", Value(" "), "last_name") + ) + .filter(full_name=first_author_str) + .first() + ) + if first_author_profile is None: + first_author_last_name = first_author_str.split(" ")[-1] + else: + first_author_last_name = first_author_profile.last_name + + # Remove accents from the last name to avoid encoding issues + # and join multiple last names into one + first_author_last_name = latinise(first_author_last_name).strip() + first_author_last_name = first_author_last_name.replace(" ", "-") + + return "{preprint_id}_{last_name}".format( + preprint_id=stream.submission.preprint.identifier_w_vn_nr, + last_name=first_author_last_name, + ) + + for repo in ProofsRepository.objects.all(): + repo.name = _get_repo_name(repo.stream) + repo.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("production", "0006_proofsrepository"), + ] + + operations = [ + migrations.AddField( + model_name="proofsrepository", + name="name", + field=models.CharField(default="", max_length=128), + ), + migrations.RunPython(add_name_to_repos, reverse_code=migrations.RunPython.noop), + ] diff --git a/scipost_django/production/migrations/0008_alter_productionstream_status.py b/scipost_django/production/migrations/0008_alter_productionstream_status.py new file mode 100644 index 0000000000000000000000000000000000000000..0a396621394e742116c79b25ee5dd2100b2d8c5b --- /dev/null +++ b/scipost_django/production/migrations/0008_alter_productionstream_status.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.18 on 2023-07-06 14:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("production", "0007_auto_20230706_1502"), + ] + + operations = [ + migrations.AlterField( + model_name="productionstream", + name="status", + field=models.CharField( + choices=[ + ("initiated", "New Stream started"), + ("source_requested", "Source files requested"), + ("tasked", "Supervisor tasked officer with proofs production"), + ("produced", "Proofs have been produced"), + ("checked", "Proofs have been checked by Supervisor"), + ("sent", "Proofs sent to Authors"), + ("returned", "Proofs returned by Authors"), + ("corrected", "Corrections implemented"), + ("accepted", "Authors have accepted proofs"), + ("published", "Paper has been published"), + ("cited", "Cited people have been notified/invited to SciPost"), + ("completed", "Completed"), + ], + default="initiated", + max_length=32, + ), + ), + ] diff --git a/scipost_django/production/models.py b/scipost_django/production/models.py index 55023d88c41ae8ba26ab1e94985dcd628fdd3e3e..1e52ef4ebe02072bcd3e8daf6ec7db92983b098f 100644 --- a/scipost_django/production/models.py +++ b/scipost_django/production/models.py @@ -15,6 +15,8 @@ from django.db.models import Value from django.db.models.functions import Concat from django.conf import settings +from common.utils import latinise + from .constants import ( PRODUCTION_STREAM_STATUS, PRODUCTION_STREAM_INITIATED, @@ -293,14 +295,18 @@ class ProofsRepository(models.Model): choices=PROOFS_REPO_STATUSES, default=PROOFS_REPO_UNINITIALIZED, ) + name = models.CharField(max_length=128, default="") - @property - def name(self) -> str: + def __str__(self): + return self.name + + @staticmethod + def _get_repo_name(stream) -> str: """ Return the name of the repository in the form of "id_lastname". """ # Get the last name of the first author by getting the first author string from the submission - first_author_str = self.stream.submission.authors_as_list[0] + first_author_str = stream.submission.authors_as_list[0] first_author_profile = ( Profile.objects.annotate( full_name=Concat("first_name", Value(" "), "last_name") @@ -312,11 +318,14 @@ class ProofsRepository(models.Model): first_author_last_name = first_author_str.split(" ")[-1] else: first_author_last_name = first_author_profile.last_name - # Keep only the last of the last names - first_author_last_name = first_author_last_name.split(" ")[-1] + + # Remove accents from the last name to avoid encoding issues + # and join multiple last names into one + first_author_last_name = latinise(first_author_last_name).strip() + first_author_last_name = first_author_last_name.replace(" ", "-") return "{preprint_id}_{last_name}".format( - preprint_id=self.stream.submission.preprint.identifier_w_vn_nr, + preprint_id=stream.submission.preprint.identifier_w_vn_nr, last_name=first_author_last_name, ) @@ -409,6 +418,7 @@ def production_stream_create_proofs_repo(sender, instance, created, **kwargs): ProofsRepository.objects.create( stream=instance, status=ProofsRepository.PROOFS_REPO_UNINITIALIZED, + name=ProofsRepository._get_repo_name(instance), ) diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_invitations_officer.html b/scipost_django/production/templates/production/_hx_productionstream_change_invitations_officer.html index ac9e3efc8ba0ad5ddb719dbb080aa0fda5cb936f..8152c57481fd30234e3a95cd96c3e064a26bdd28 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_change_invitations_officer.html +++ b/scipost_django/production/templates/production/_hx_productionstream_change_invitations_officer.html @@ -2,7 +2,7 @@ {% if form.fields.invitations_officer.choices|length > 0 %} <div id="productionstream-{{ stream.id }}-update-invitations_officer"> - <form hx-post="{% url 'production:update_invitations_officer' stream.id %}" + <form hx-post="{% url 'production:_hx_update_invitations_officer' stream.id %}" hx-target="#productionstream-{{ stream.id }}-update-invitations_officer" hx-swap="outerHTML" class="row mb-0"> diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_officer.html b/scipost_django/production/templates/production/_hx_productionstream_change_officer.html index 2efbd7f2ebb9f75e1f2e64f85e11ed8f0248086d..014d8347a20bd1184865ce74b261861019455a66 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_change_officer.html +++ b/scipost_django/production/templates/production/_hx_productionstream_change_officer.html @@ -2,7 +2,7 @@ {% if form.fields.officer.choices|length > 0 %} <div id="productionstream-{{ stream.id }}-update-officer"> - <form hx-post="{% url 'production:update_officer' stream.id %}" + <form hx-post="{% url 'production:_hx_update_officer' stream.id %}" hx-target="#productionstream-{{ stream.id }}-update-officer" hx-swap="outerHTML" class="row"> diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_status.html b/scipost_django/production/templates/production/_hx_productionstream_change_status.html index 874111480ad50645cf1518a02f55608d585e0069..e2358f8ed2ac967612f05ed38c67dab6a3783296 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_change_status.html +++ b/scipost_django/production/templates/production/_hx_productionstream_change_status.html @@ -2,7 +2,7 @@ {% if form.fields.status.choices|length > 0 %} <form id="productionstream-{{ stream.id }}-update-status" - hx-post="{% url 'production:update_status' stream.id %}" + hx-post="{% url 'production:_hx_update_status' stream.id %}" hx-target="this" hx-swap="outerHTML" hx-trigger="submit" diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html b/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html index 4ce3afd863abb8ac1a9ece6f79340083938592c1..2a0aba9c9afb93535c0230dc98aa0366be4ecf3e 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html +++ b/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html @@ -2,7 +2,7 @@ {% if form.fields.supervisor.choices|length > 0 %} <div id="productionstream-{{ stream.id }}-update-supervisor"> - <form hx-post="{% url 'production:update_supervisor' stream.id %}" + <form hx-post="{% url 'production:_hx_update_supervisor' stream.id %}" hx-target="#productionstream-{{ stream.id }}-update-supervisor" hx-swap="outerHTML" class="row"> diff --git a/scipost_django/production/templates/production/_hx_productionstream_details.html b/scipost_django/production/templates/production/_hx_productionstream_details.html index 2e51ab633cda7337e6d7b40ba0bcd0f0b1c63399..a4977aae49aae0ab3e6416421763c205a13b2c18 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_details.html +++ b/scipost_django/production/templates/production/_hx_productionstream_details.html @@ -1,12 +1,12 @@ <details id="productionstream-{{ productionstream.id }}-details" class="border border-2 mx-3 p-2 bg-primary bg-opacity-10"> - <summary class="summary-unstyled p-2"> + <summary class="list-none"> {% include "production/_productionstream_details_summary_contents.html" with productionstream=productionstream %} </summary> <div id="productionstream-{{ productionstream.id }}-details-contents" - class="m-2 p-2 bg-white" + class="p-2 mt-2 bg-white" hx-get="{% url 'production:_hx_productionstream_details_contents' productionstream_id=productionstream.id %}" hx-trigger="toggle once from:#productionstream-{{ productionstream.id }}-details" hx-indicator="#indicator-productionstream-{{ productionstream.id }}-details-contents"></div> diff --git a/scipost_django/production/templates/production/_hx_productionstream_details_contents.html b/scipost_django/production/templates/production/_hx_productionstream_details_contents.html index 2fdf85f4ffcb53b7ad0a3267ed5329cd068435e0..476f155e9bb58018655ec51545291e1433e249ed 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_details_contents.html +++ b/scipost_django/production/templates/production/_hx_productionstream_details_contents.html @@ -4,11 +4,11 @@ {% get_obj_perms request.user for productionstream as "sub_perms" %} -<div class="row"> +<div class="row mb-0"> {% if "can_work_for_stream" in sub_perms or perms.scipost.can_assign_production_supervisor %} <div class="col-12 col-md d-flex flex-column"> - <div class="accordion px-2 mb-2" + <div class="accordion" id="productionstream-{{ productionstream.id }}-actions-accordion"> <h3>Actions</h3> @@ -74,7 +74,7 @@ data-bs-parent="#productionstream-{{ productionstream.id }}-actions-accordion"> <div id="productionstream-{{ productionstream.id }}-upload-proofs-body" class="accordion-body" - hx-get="{% url 'production:upload_proofs' stream_id=productionstream.id %}" + hx-get="{% url 'production:_hx_upload_proofs' stream_id=productionstream.id %}" hx-trigger="intersect once"> {% comment %} Placeholder before HTMX content loads {% endcomment %} @@ -170,13 +170,13 @@ {% if perms.scipost.can_draft_publication or perms.scipost.can_publish_accepted_submission %} <div class="mb-2 mb-md-0 mt-md-auto px-2"> - <div class="row mb-0 g-2"> + <div class="row mb-0 mt-2 g-2"> {% if perms.scipost.can_publish_accepted_submission %} <div class="col-12 col-sm-auto col-md-12 col-lg-auto h-100 d-none-empty"> <div class="row m-0 d-none-empty"> <button class="btn btn-warning text-white" - hx-get="{% url 'production:mark_as_completed' stream_id=productionstream.id %}" - {% if stream.status != 'published' %}hx-confirm="Are you sure you want to mark this unpublished stream as completed?"{% endif %} + hx-get="{% url 'production:_hx_mark_as_completed' stream_id=productionstream.id %}" + {% if productionstream.status != 'published' %}hx-confirm="Are you sure you want to mark this unpublished stream as completed?"{% endif %} hx-target="#productionstream-{{ productionstream.id }}-details"> Mark this stream as completed </button> @@ -184,7 +184,7 @@ </div> {% endif %} - {% if perms.scipost.can_draft_publication and stream.status == 'accepted' %} + {% if perms.scipost.can_draft_publication and productionstream.status == 'accepted' %} <div class="col-12 col-sm-auto col-md-12 col-lg-auto h-100 d-none-empty"> <div class="row m-0 d-none-empty"> <a class="btn btn-primary text-white" @@ -202,7 +202,7 @@ </div> <div id="productionstream-{{ productionstream.id }}-event-container" - class="col-12 col-md d-flex flex-column px-3"> + class="col-12 col-md d-flex flex-column"> {% comment %} This might be better to refactor with an OOB response on each event addition {% endcomment %} <h3>Events</h3> <div id="productionstream-{{ productionstream.id }}-event-list" diff --git a/scipost_django/production/templates/production/_hx_productionstream_search_form.html b/scipost_django/production/templates/production/_hx_productionstream_search_form.html new file mode 100644 index 0000000000000000000000000000000000000000..177079afdf6d853d4465550126697f6b71977160 --- /dev/null +++ b/scipost_django/production/templates/production/_hx_productionstream_search_form.html @@ -0,0 +1,10 @@ +{% load crispy_forms_tags %} + +<form hx-post="{% url 'production:_hx_productionstream_list' %}" + hx-trigger="load, keyup delay:500ms, change delay:500ms, click from:#refresh-button" + hx-sync="#search-productionstreams-form:replace" + hx-target="#search-productionstreams-results" + hx-indicator="#indicator-search-productionstreams"> + + <div id="search-productionstreams-form">{% crispy form %}</div> +</form> diff --git a/scipost_django/production/templates/production/_hx_upload_proofs.html b/scipost_django/production/templates/production/_hx_upload_proofs.html new file mode 100644 index 0000000000000000000000000000000000000000..a60d1c2c6d8dc18ed091141455fd55e6b594d9a2 --- /dev/null +++ b/scipost_django/production/templates/production/_hx_upload_proofs.html @@ -0,0 +1,27 @@ +{% load bootstrap %} + + +<h3>Proofs</h3> +<div class="accordion" + id="productionstream-{{ stream.id }}-proofs-list-accordion"> + {% for proofs in stream.proofs.all %} + {% include 'production/_hx_productionstream_actions_proofs_item.html' with i_proof=forloop.counter0|add:1 active_id=total_proofs stream=stream proofs=proofs %} + {% empty %} + <div>No Proofs found.</div> + {% endfor %} +</div> + +<div class="row mt-3"> + <div class="col-12"> + <form enctype="multipart/form-data" + hx-post="{% url 'production:_hx_upload_proofs' stream_id=stream.id %}" + hx-target="#productionstream-{{ stream.id }}-upload-proofs-body"> + {% csrf_token %} + {{ form|bootstrap_purely_inline }} + <input type="submit" + class="btn btn-primary proof-action-button" + name="submit" + value="Upload"> + </form> + </div> +</div> diff --git a/scipost_django/production/templates/production/_production_stream_card.html b/scipost_django/production/templates/production/_production_stream_card.html index a7fbb4c1edbb1ae01a67d828c1bc3bc699999999..2b4430a24ae092adb1c7a641d336dffe57d07f0e 100644 --- a/scipost_django/production/templates/production/_production_stream_card.html +++ b/scipost_django/production/templates/production/_production_stream_card.html @@ -88,7 +88,7 @@ <li> <button type="button" class="btn btn-link" data-bs-toggle="toggle" data-bs-target="#upload_proofs">Upload Proofs</button> <div id="upload_proofs" style="display: none;"> - <form class="my-3" action="{% url 'production:upload_proofs' stream_id=stream.id %}" method="post" enctype="multipart/form-data"> + <form class="my-3" action="{% url 'production:_hx_upload_proofs' stream_id=stream.id %}" method="post" enctype="multipart/form-data"> {% csrf_token %} {{ upload_proofs_form|bootstrap_inline }} <input type="submit" class="btn btn-outline-primary" name="submit" value="Upload"> diff --git a/scipost_django/production/templates/production/_productionstream_details_summary_contents.html b/scipost_django/production/templates/production/_productionstream_details_summary_contents.html index 6fb15371a18c733794117ae31a8497750b3aab03..7239595fcf5f72d457fbb1b1137398b778c26e38 100644 --- a/scipost_django/production/templates/production/_productionstream_details_summary_contents.html +++ b/scipost_django/production/templates/production/_productionstream_details_summary_contents.html @@ -64,12 +64,11 @@ <small class="text-muted">Submitter</small> <br> {% if productionstream.submission.submitted_by.profile.email %} - <a href="mailto:{{ productionstream.submission.submitted_by.profile.email }}?body=Dear%20{{ productionstream.submission.submitted_by.formal_str }},%0A%0A"> - {{ productionstream.submission.submitted_by.formal_str }} - </a> - {% else %} - {{ productionstream.submission.submitted_by.formal_str }} + <a href="mailto:{{ productionstream.submission.submitted_by.profile.email }}?body=Dear%20{{ productionstream.submission.submitted_by.formal_str }},%0A%0A" + class="text-primary"><span style="pointer-events: none;">{% include 'bi/pencil-square.html' %}</span></a> + {% endif %} + <a href="{% url 'scipost:contributor_info' productionstream.submission.submitted_by.id %}">{{ productionstream.submission.submitted_by.formal_str }}</a> </div> <div class="col-auto"> @@ -77,6 +76,8 @@ <br> <div class="d-inline-flex"> <a href="{{ productionstream.submission.get_absolute_url }}">Submission</a> + • + <a href="{% url 'production:stream' stream_id=productionstream.id %}">Stream</a> {% if perms.scipost.can_oversee_refereeing %} • <a href="{% url 'submissions:editorial_page' productionstream.submission.preprint.identifier_w_vn_nr %}">Editorial</a> diff --git a/scipost_django/production/templates/production/_productionstream_events.html b/scipost_django/production/templates/production/_productionstream_events.html index 28b1ed7d2fb9dadef06ba3d8d6142cfe57d03147..03918572a39b555c95fa419d05da8dd739d24842 100644 --- a/scipost_django/production/templates/production/_productionstream_events.html +++ b/scipost_django/production/templates/production/_productionstream_events.html @@ -50,15 +50,15 @@ </tr> <tr> - <td colspan="4" class="ps-4 pb-4"> + <td colspan="4" class="py-1 px-3"> {% if event.comments %} - <p class="mt-2 mb-0"> + <div> {% if event.noted_to %} <strong>To: {{ event.noted_to.user.first_name }} {{ event.noted_to.user.last_name }}</strong> <br> {% endif %} {% automarkup event.comments %} - </p> + </div> {% endif %} diff --git a/scipost_django/production/templates/production/production_new.html b/scipost_django/production/templates/production/production_new.html index 388326ba4d72a5a47cc83c2762a180b4e5d23279..6f2c4b9bad5602a074e658aa8c0590a7bef9894b 100644 --- a/scipost_django/production/templates/production/production_new.html +++ b/scipost_django/production/templates/production/production_new.html @@ -25,11 +25,11 @@ </div> <details id="productionstreams-filter-details" class="card my-4"> - <summary class="card-header fs-6 d-inline-flex align-items-center"> - Search / Filter / Bulk Actions - <div class="d-none d-sm-inline-flex ms-auto align-items-center"> + <summary class="card-header fs-6 d-flex flex-row align-items-center justify-content-between list-triangle"> + <div>Search / Filter / Bulk Actions</div> + <div class="d-none d-md-flex align-items-center"> + <div id="indicator-search-productionstreams" class="htmx-indicator"> - <button class="btn btn-warning text-white d-none d-md-block me-2" type="button" disabled> @@ -38,10 +38,14 @@ <div class="spinner-grow spinner-grow-sm ms-2" role="status" aria-hidden="true"></div> - </button> </div> + <button class="btn btn-outline-secondary me-2" + type="button" + hx-get="{% url 'production:_hx_productionstream_search_form' filter_set="empty" %}" + hx-target="#productionstream-search-form-container">Clear Filters</button> + <a id="refresh-button" class="m-2 btn btn-primary"> {% include "bi/arrow-clockwise.html" %} Refresh</a> @@ -49,13 +53,9 @@ </summary> <div class="card-body"> - <form hx-post="{% url 'production:_hx_productionstream_list' %}" - hx-trigger="load, keyup delay:500ms, change, click from:#refresh-button" - hx-target="#search-productionstreams-results" - hx-indicator="#indicator-search-productionstreams"> - - <div id="search-productionstreams-form">{% crispy search_productionstreams_form %}</div> - </form> + <div id="productionstream-search-form-container"> + {% include 'production/_hx_productionstream_search_form.html' with form=search_productionstreams_form %} + </div> {% comment %} Bulk Action buttons {% endcomment %} diff --git a/scipost_django/production/templates/production/upload_proofs.html b/scipost_django/production/templates/production/upload_proofs.html index afa710d9b9a743eb84cffd2967076dac34d7b02a..16c6c905ebab596a3884f0665cd1cec92792efcf 100644 --- a/scipost_django/production/templates/production/upload_proofs.html +++ b/scipost_django/production/templates/production/upload_proofs.html @@ -1,27 +1,32 @@ -{% load bootstrap %} +{% extends 'production/base.html' %} +{% block breadcrumb_items %} + {{ block.super }} + <span class="breadcrumb-item">Upload Proofs</span> +{% endblock %} -<h3>Proofs</h3> -<div class="accordion" - id="productionstream-{{ stream.id }}-proofs-list-accordion"> - {% for proofs in stream.proofs.all %} - {% include 'production/_hx_productionstream_actions_proofs_item.html' with i_proof=forloop.counter0|add:1 active_id=total_proofs stream=stream proofs=proofs %} - {% empty %} - <div>No Proofs found.</div> - {% endfor %} -</div> +{% load bootstrap %} -<div class="row mt-3"> - <div class="col-12"> - <form enctype="multipart/form-data" - hx-post="{% url 'production:upload_proofs' stream_id=stream.id %}" - hx-target="#productionstream-{{ stream.id }}-upload-proofs-body"> - {% csrf_token %} - {{ form|bootstrap_purely_inline }} - <input type="submit" - class="btn btn-primary proof-action-button" - name="submit" - value="Upload"> - </form> +{% block content %} + + <div class="row"> + <div class="col-12"> + <h1 class="highlight">Upload Proofs</h1> + {% include 'submissions/_submission_card_content.html' with submission=stream.submission %} + </div> + </div> + <div class="row"> + <div class="col-12"> + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" + class="btn btn-outline-secondary" + name="submit" + value="Upload"> + </form> + </ul> </div> </div> + +{% endblock content %} diff --git a/scipost_django/production/tests/test_models.py b/scipost_django/production/tests/test_models.py index e35c6d72e48182c4ed96b86204e5deeeaf51c62d..78a12ca5f3a62aebb84143c3235e223f80e6d7cb 100644 --- a/scipost_django/production/tests/test_models.py +++ b/scipost_django/production/tests/test_models.py @@ -153,7 +153,7 @@ class TestProofRepository(TestCase): user_profile.last_name = "Usable User" user_profile.save() - self.assertEqual(proofs_repo.name, "scipost_202101_00001v1_User") + self.assertEqual(proofs_repo.name, "scipost_202101_00001v1_Usable-User") def test_repo_name_two_authors(self): proofs_repo = ProofsRepository.objects.get( @@ -166,6 +166,20 @@ class TestProofRepository(TestCase): self.assertEqual(proofs_repo.name, "scipost_202101_00001v1_Person") + def test_repo_name_accented_authors(self): + proofs_repo = ProofsRepository.objects.get( + stream__submission__preprint__identifier_w_vn_nr="scipost_202101_00001v1" + ) + + user_profile = Contributor.objects.get(user__username="testuser").profile + user_profile.first_name = "Some" + user_profile.last_name = "Pérsønüsær (陈)" + user_profile.save() + + proofs_repo.stream.submission.author_list = "Some Pérsønüsær (陈)" + + self.assertEqual(proofs_repo.name, "scipost_202101_00001v1_Personusaer") + def test_repo_paths_scipostphys(self): proofs_repo = ProofsRepository.objects.get( stream__submission__preprint__identifier_w_vn_nr="scipost_202101_00001v1" diff --git a/scipost_django/production/urls.py b/scipost_django/production/urls.py index 81e04bc2e833da0da4b1dbdd2c9a1ce49bba0bd0..54b32e500a26161a0ccefe456f6d2454c9d2f932 100644 --- a/scipost_django/production/urls.py +++ b/scipost_django/production/urls.py @@ -47,6 +47,11 @@ urlpatterns = [ production_views._hx_productionstream_list, name="_hx_productionstream_list", ), + path( + "_hx_productionstream_search_form/<str:filter_set>", + production_views._hx_productionstream_search_form, + name="_hx_productionstream_search_form", + ), path( "_hx_productionstream_actions_bulk_assign_officers", production_views._hx_productionstream_actions_bulk_assign_officers, @@ -134,6 +139,11 @@ urlpatterns = [ [ path("", production_views.stream, name="stream"), path("status", production_views.update_status, name="update_status"), + path( + "_hx_status", + production_views._hx_update_status, + name="_hx_update_status", + ), path( "proofs/", include( @@ -143,6 +153,11 @@ urlpatterns = [ production_views.upload_proofs, name="upload_proofs", ), + path( + "_hx_upload", + production_views._hx_upload_proofs, + name="_hx_upload_proofs", + ), path( "<int:version>/", include( @@ -209,8 +224,8 @@ urlpatterns = [ ), path( "update", - production_views.update_officer, - name="update_officer", + production_views._hx_update_officer, + name="_hx_update_officer", ), ] ), @@ -231,8 +246,8 @@ urlpatterns = [ ), path( "update", - production_views.update_invitations_officer, - name="update_invitations_officer", + production_views._hx_update_invitations_officer, + name="_hx_update_invitations_officer", ), ] ), @@ -253,12 +268,17 @@ urlpatterns = [ ), path( "update", - production_views.update_supervisor, - name="update_supervisor", + production_views._hx_update_supervisor, + name="_hx_update_supervisor", ), ] ), ), + path( + "_hx_mark_completed", + production_views._hx_mark_as_completed, + name="_hx_mark_as_completed", + ), path( "mark_completed", production_views.mark_as_completed, diff --git a/scipost_django/production/utils.py b/scipost_django/production/utils.py index 994670e10bac7974c5513cbcaeb846283b48fa2f..e92aab229641424438efebd1af4419b0e1488c70 100644 --- a/scipost_django/production/utils.py +++ b/scipost_django/production/utils.py @@ -67,3 +67,27 @@ class ProductionUtils(BaseMailUtil): [cls._context["stream"].supervisor.user.email], "SciPost: you have a new supervisory task", ) + + @classmethod + def email_assigned_production_officer_bulk(cls): + """ + Email officer about his/her new assigned stream. + """ + cls._send_mail( + cls, + "email_assigned_production_officer_bulk", + [cls._context["officer"].user.email], + "SciPost: you have new production tasks", + ) + + @classmethod + def email_assigned_supervisor_bulk(cls): + """ + Email supervisor about his/her new assigned stream. + """ + cls._send_mail( + cls, + "email_assigned_supervisor_bulk", + [cls._context["supervisor"].user.email], + "SciPost: you have new supervisory tasks", + ) diff --git a/scipost_django/production/views.py b/scipost_django/production/views.py index f95e42c53d774d8a4a245602a405609f7c38ef8b..ce5b01f43f44f553adce53f811edc87a6bed519b 100644 --- a/scipost_django/production/views.py +++ b/scipost_django/production/views.py @@ -455,6 +455,29 @@ def _hx_productionstream_actions_work_log(request, productionstream_id): ) +is_production_user() + + +@permission_required( + "scipost.can_take_decisions_related_to_proofs", raise_exception=True +) +def update_status(request, stream_id): + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm("can_perform_supervisory_actions", stream): + return redirect(reverse("production:production", args=(stream.id,))) + + p = request.user.production_user + form = StreamStatusForm(request.POST or None, instance=stream, production_user=p) + + if form.is_valid(): + stream = form.save() + messages.warning(request, "Production Stream succesfully changed status.") + else: + messages.warning(request, "The status change was invalid.") + return redirect(stream.get_absolute_url()) + + @is_production_user() @permission_required_htmx( ( @@ -464,7 +487,7 @@ def _hx_productionstream_actions_work_log(request, productionstream_id): message="You do not have permission to update the status of this stream.", css_class="row", ) -def update_status(request, stream_id): +def _hx_update_status(request, stream_id): productionstream = get_object_or_404( ProductionStream.objects.ongoing(), pk=stream_id ) @@ -572,7 +595,7 @@ def remove_officer(request, stream_id, officer_id): css_class="row", ) @transaction.atomic -def update_officer(request, stream_id): +def _hx_update_officer(request, stream_id): productionstream = get_object_or_404( ProductionStream.objects.ongoing(), pk=stream_id ) @@ -742,7 +765,7 @@ def remove_invitations_officer(request, stream_id, officer_id): css_class="row", ) @transaction.atomic -def update_invitations_officer(request, stream_id): +def _hx_update_invitations_officer(request, stream_id): productionstream = get_object_or_404( ProductionStream.objects.ongoing(), pk=stream_id ) @@ -886,7 +909,7 @@ class UpdateEventView(UpdateView): css_class="row", ) @transaction.atomic -def update_supervisor(request, stream_id): +def _hx_update_supervisor(request, stream_id): productionstream = get_object_or_404( ProductionStream.objects.ongoing(), pk=stream_id ) @@ -973,7 +996,7 @@ class DeleteEventView(DeleteView): @is_production_user() @permission_required("scipost.can_publish_accepted_submission", raise_exception=True) @transaction.atomic -def mark_as_completed(request, stream_id): +def _hx_mark_as_completed(request, stream_id): productionstream = get_object_or_404( ProductionStream.objects.ongoing(), pk=stream_id ) @@ -995,12 +1018,32 @@ def mark_as_completed(request, stream_id): ) return HttpResponse( - r"""<summary class="text-white bg-success summary-unstyled p-3"> + r"""<summary class="text-white bg-success p-3"> Production Stream has been marked as completed. </summary>""" ) +@is_production_user() +@permission_required("scipost.can_publish_accepted_submission", raise_exception=True) +@transaction.atomic +def mark_as_completed(request, stream_id): + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + stream.status = constants.PRODUCTION_STREAM_COMPLETED + stream.closed = timezone.now() + stream.save() + + prodevent = ProductionEvent( + stream=stream, + event="status", + comments=" marked the Production Stream as completed.", + noted_by=request.user.production_user, + ) + prodevent.save() + messages.success(request, "Stream marked as completed.") + return redirect(reverse("production:production")) + + @is_production_user() @permission_required_htmx( ( @@ -1010,7 +1053,7 @@ def mark_as_completed(request, stream_id): message="You cannot upload proofs for this stream.", ) @transaction.atomic -def upload_proofs(request, stream_id): +def _hx_upload_proofs(request, stream_id): """ Called by a member of the Production Team. Upload the production version .pdf of a submission. @@ -1048,6 +1091,51 @@ def upload_proofs(request, stream_id): prodevent.save() context = {"stream": stream, "form": form, "total_proofs": stream.proofs.count()} + return render(request, "production/_hx_upload_proofs.html", context) + + +@is_production_user() +@permission_required("scipost.can_upload_proofs", raise_exception=True) +@transaction.atomic +def upload_proofs(request, stream_id): + """ + Called by a member of the Production Team. + Upload the production version .pdf of a submission. + """ + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm("can_work_for_stream", stream): + return redirect(reverse("production:production")) + + form = ProofsUploadForm(request.POST or None, request.FILES or None) + if form.is_valid(): + proofs = form.save(commit=False) + proofs.stream = stream + proofs.uploaded_by = request.user.production_user + proofs.save() + Proofs.objects.filter(stream=stream).exclude(version=proofs.version).exclude( + status=constants.PROOFS_ACCEPTED + ).update(status=constants.PROOFS_RENEWED) + messages.success(request, "Proof uploaded.") + + # Update Stream status + if stream.status == constants.PROOFS_TASKED: + stream.status = constants.PROOFS_PRODUCED + stream.save() + elif stream.status == constants.PROOFS_RETURNED: + stream.status = constants.PROOFS_CORRECTED + stream.save() + + prodevent = ProductionEvent( + stream=stream, + event="status", + comments="New Proofs uploaded, version {v}".format(v=proofs.version), + noted_by=request.user.production_user, + ) + prodevent.save() + return redirect(stream.get_absolute_url()) + + context = {"stream": stream, "form": form} return render(request, "production/upload_proofs.html", context) @@ -1485,6 +1573,22 @@ def _hx_productionstream_summary_assignees_status(request, productionstream_id): ) +def _hx_productionstream_search_form(request, filter_set: str): + productionstream_search_form = ProductionStreamSearchForm( + user=request.user, + session_key=request.session.session_key, + ) + + if filter_set == "empty": + productionstream_search_form.apply_filter_set({}, none_on_empty=True) + # TODO: add more filter sets saved in the session of the user + + context = { + "form": productionstream_search_form, + } + return render(request, "production/_hx_productionstream_search_form.html", context) + + def _hx_event_list(request, productionstream_id): productionstream = get_object_or_404(ProductionStream, pk=productionstream_id) @@ -1562,10 +1666,16 @@ def _hx_productionstream_actions_bulk_assign_officers(request): "can_work_for_stream", old_officer.user, productionstream ) - # Temp fix. - # TODO: Implement proper email - ProductionUtils.load({"request": request, "stream": productionstream}) - ProductionUtils.email_assigned_production_officer() + # Temp fix. + # TODO: Implement proper email + ProductionUtils.load( + { + "request": request, + "officer": officer, + "streams": form.productionstreams, + } + ) + ProductionUtils.email_assigned_production_officer_bulk() messages.success( request, @@ -1614,10 +1724,16 @@ def _hx_productionstream_actions_bulk_assign_officers(request): productionstream, ) - # Temp fix. - # TODO: Implement proper email - ProductionUtils.load({"request": request, "stream": productionstream}) - ProductionUtils.email_assigned_supervisor() + # Temp fix. + # TODO: Implement proper email + ProductionUtils.load( + { + "request": request, + "supervisor": supervisor, + "streams": form.productionstreams, + } + ) + ProductionUtils.email_assigned_supervisor_bulk() messages.success( request, diff --git a/scipost_django/scipost/static/scipost/assets/config/preconfig.scss b/scipost_django/scipost/static/scipost/assets/config/preconfig.scss index 56124499ce0e1c769f7be5d798e68acf7c3e6052..75c460daa87e3cec1694b07b4980e818a7a02481 100644 --- a/scipost_django/scipost/static/scipost/assets/config/preconfig.scss +++ b/scipost_django/scipost/static/scipost/assets/config/preconfig.scss @@ -202,15 +202,6 @@ $theme-colors: ( $theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value"); -// Browser specific fixes -// Select summary in details with list-style: none and remove triangle for safari -.summary-unstyled { - list-style: none; - &::-webkit-details-marker { - display: none; - } -} - // Utilities for common display issues // Hide div .d-none-empty:empty { @@ -220,4 +211,36 @@ $theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value"); .checkbox-lg { width: 1.5em !important; height: 1.5em !important; +} + +summary { + // Remove triangle for webkit browsers causing problems with flexbox + &::-webkit-details-marker { + display: none; + } + + // Remove all list styles + &.list-none { + list-style: none; + } + + // List triangle for summary element (necessary with display: flex) + &.list-triangle { + position: relative; + padding-left: 2em !important; + + // Styling the equilateral triangle + &::before { + content: "â–¶"; + position: absolute; + left: 0.75em; + top: 50%; + transform: translateY(-50%); + } + } +} + +// Rotate the equilateral triangle when summary is open +details[open] summary.list-triangle::before { + content: "â–¼"; } \ No newline at end of file diff --git a/scipost_django/templates/email/email_assigned_production_officer_bulk.html b/scipost_django/templates/email/email_assigned_production_officer_bulk.html new file mode 100644 index 0000000000000000000000000000000000000000..c4b31feb1a983102270209edb34f8924d4d8e308 --- /dev/null +++ b/scipost_django/templates/email/email_assigned_production_officer_bulk.html @@ -0,0 +1,15 @@ +<p>Dear {{ officer.user.last_name }},</p> +<p>You are now assigned as Production Officer to the streams:</p> + +<ul> + {% for stream in streams %}<li>{{ stream }}</li>{% endfor %} +</ul> + +<p> + The streams will now be open for you on the <a href="https://{{ domain }}{% url 'production:production_new' %}">Production page</a>. +</p> + + +<p> + <em>This mail is automatically generated and therefore not signed.</em> +</p> diff --git a/scipost_django/templates/email/email_assigned_production_officer_bulk.txt b/scipost_django/templates/email/email_assigned_production_officer_bulk.txt new file mode 100644 index 0000000000000000000000000000000000000000..4fff3519388a6a12a5033bde96b2358e413e0f95 --- /dev/null +++ b/scipost_django/templates/email/email_assigned_production_officer_bulk.txt @@ -0,0 +1,10 @@ +Dear {{ officer.user.last_name }}, + +You are now assigned as Production Officer to the streams: + +{% for stream in streams %}* {{ stream }} +{% endfor %} + +The stream will now be open for you on the Production page (https://{{ domain }}{% url 'production:production_new' %}). + +This mail is automatically generated and therefore not signed. diff --git a/scipost_django/templates/email/email_assigned_supervisor_bulk.html b/scipost_django/templates/email/email_assigned_supervisor_bulk.html new file mode 100644 index 0000000000000000000000000000000000000000..9fa51f280a5276cea9c823c33108dc971096fa99 --- /dev/null +++ b/scipost_django/templates/email/email_assigned_supervisor_bulk.html @@ -0,0 +1,15 @@ +<p>Dear {{ supervisor.user.last_name }},</p> +<p>You are now assigned as Production Supervisor to the streams:</p> + +<ul> + {% for stream in streams %}<li>{{ stream }}</li>{% endfor %} +</ul> + +<p> + The streams will now be open for you on the <a href="https://{{ domain }}{% url 'production:production_new' %}">Production page</a>. +</p> + + +<p> + <em>This mail is automatically generated and therefore not signed.</em> +</p> diff --git a/scipost_django/templates/email/email_assigned_supervisor_bulk.txt b/scipost_django/templates/email/email_assigned_supervisor_bulk.txt new file mode 100644 index 0000000000000000000000000000000000000000..dfa9a2a901b8773d9250475994336c024f895931 --- /dev/null +++ b/scipost_django/templates/email/email_assigned_supervisor_bulk.txt @@ -0,0 +1,10 @@ +Dear {{ supervisor.user.last_name }}, + +You are now assigned as Production Supervisor to the streams: + +{% for stream in streams %}* {{ stream }} +{% endfor %} + +The stream will now be open for you on the Production page (https://{{ domain }}{% url 'production:production_new' %}). + +This mail is automatically generated and therefore not signed.