From ef083567eed6fb05e2a339324bd49d40091114da Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Mon, 17 Aug 2026 09:54:28 -0500 Subject: [PATCH 1/6] Move Edit and Add Pages --- src/submission/additional_field/__init__.py | 0 src/submission/additional_field/views.py | 65 ++++++++++++++++ src/submission/urls.py | 18 ++++- src/submission/views.py | 74 +++++-------------- .../edit_additional_field.html | 52 +++++++++++++ .../{ => additional_fields}/fields.html | 20 +---- 6 files changed, 152 insertions(+), 77 deletions(-) create mode 100644 src/submission/additional_field/__init__.py create mode 100644 src/submission/additional_field/views.py create mode 100644 src/templates/admin/submission/manager/additional_fields/edit_additional_field.html rename src/templates/admin/submission/manager/{ => additional_fields}/fields.html (77%) diff --git a/src/submission/additional_field/__init__.py b/src/submission/additional_field/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/submission/additional_field/views.py b/src/submission/additional_field/views.py new file mode 100644 index 0000000000..074f407dad --- /dev/null +++ b/src/submission/additional_field/views.py @@ -0,0 +1,65 @@ +from django.db.models import QuerySet +from django.http import HttpResponse +from django.shortcuts import redirect, render +from django.urls import reverse + +from security.decorators import editor_user_required +from submission import forms, logic +from submission.forms import FieldForm +from submission.models import Field + + +@editor_user_required +def additional_fields_view(request): + """ + Allows the editor to view and reorder submission fields. + :param request: HttpRequest object + :return: HttpResponse or HttpRedirect + """ + fields: QuerySet[Field, Field] = logic.get_submission_fields(request) + + if request.POST: + if "delete" in request.POST: + logic.delete_field(request) + return redirect(reverse("submission_fields")) + + elif "order[]" in request.POST: + logic.order_fields(request, fields) + return HttpResponse("Thanks") + + template = "admin/submission/manager/additional_fields/fields.html" + context = { + "fields": fields, + } + + return render(request, template, context) + + +@editor_user_required +def edit_additional_field_view(request, field_id: str | None = None) -> HttpResponse: + """ + Allows the editor to create, edit and delete new submission fields. + :param request: HttpRequest object + :param field_id: The ID of the field. Leave None to add a new field. + :return: HttpResponse + """ + field: Field = logic.get_current_field(request, field_id) + form: FieldForm = forms.FieldForm(instance=field) + + if request.POST: + if "save" in request.POST: + form = forms.FieldForm(request.POST, instance=field) + + if form.is_valid(): + logic.save_field(request, form) + return redirect(reverse("submission_fields")) + + template: str = ( + "admin/submission/manager/additional_fields/edit_additional_field.html" + ) + context = { + "field": field, + "form": form, + } + + return render(request, template, context) diff --git a/src/submission/urls.py b/src/submission/urls.py index 2dbf576b82..684d0d6ea4 100755 --- a/src/submission/urls.py +++ b/src/submission/urls.py @@ -2,9 +2,12 @@ __author__ = "Martin Paul Eve & Andy Byers" __license__ = "AGPL v3" __maintainer__ = "Birkbeck Centre for Technology and Publishing" +from django.conf import settings from django.urls import re_path from submission import views +from submission.additional_field import views as additional_fields_view +from utils.logger import get_logger urlpatterns = [ re_path(r"^start/$", views.start, name="submission_start"), @@ -95,10 +98,19 @@ re_path( r"^manager/configurator/$", views.configurator, name="submission_configurator" ), - re_path(r"^manager/additional_fields/$", views.fields, name="submission_fields"), re_path( - r"^manager/additional_fields/(?P\d+)/$", - views.fields, + r"^manager/additional_fields/$", + additional_fields_view.additional_fields_view, + name="submission_fields", + ), + re_path( + r"^manager/additional_fields/edit/$", + additional_fields_view.edit_additional_field_view, + name="submission_fields_add", + ), + re_path( + r"^manager/additional_fields/edit/(?P\d+)/$", + additional_fields_view.edit_additional_field_view, name="submission_fields_id", ), re_path(r"^manager/licences/$", views.licenses, name="submission_licenses"), diff --git a/src/submission/views.py b/src/submission/views.py index e0f60f7c99..07a8b9ebc3 100755 --- a/src/submission/views.py +++ b/src/submission/views.py @@ -6,46 +6,47 @@ import json import warnings +from django.conf import settings from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required -from django.urls import reverse +from django.core.exceptions import PermissionDenied from django.db.models import Q -from django.http import HttpResponse, Http404 -from django.shortcuts import render, redirect, get_object_or_404 +from django.http import Http404, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse from django.utils import timezone, translation from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ -from django.conf import settings -from django.core.exceptions import PermissionDenied from django.views.decorators.http import require_POST -from core import files, models as core_models -from core.logic import create_organization_name, reverse_with_next -from core.views import GenericFacetedListView +from core import files +from core import models as core_models from core.forms import ( AccountAffiliationForm, ConfirmDeleteForm, OrcidAffiliationForm, OrganizationNameForm, ) +from core.logic import create_organization_name, reverse_with_next +from core.views import GenericFacetedListView +from events import logic as event_logic from repository import models as preprint_models from security.decorators import ( - production_user_or_editor_required, + article_is_not_submitted, editor_user_required, editor_user_required_and_can_see_pii, - submission_authorised, - article_is_not_submitted, + production_user_or_editor_required, role_can_access, - user_can_edit_author, + submission_authorised, user_can_edit_article, + user_can_edit_author, ) -from submission import forms, models, logic, decorators -from events import logic as event_logic -from utils import setting_handler -from utils import shared as utils_shared, orcid -from utils.forms import clean_orcid_id +from submission import decorators, forms, logic, models +from utils import orcid, setting_handler +from utils import shared as utils_shared from utils.decorators import GET_language_override +from utils.forms import clean_orcid_id from utils.shared import create_language_override_redirect @@ -1063,45 +1064,6 @@ def link_author_to_account(request, article_id, author_id): ) -@editor_user_required -def fields(request, field_id=None): - """ - Allows the editor to create, edit and delete new submission fields. - :param request: HttpRequest object - :param field_id: Field object PK, optional - :return: HttpResponse or HttpRedirect - """ - - field = logic.get_current_field(request, field_id) - fields = logic.get_submission_fields(request) - form = forms.FieldForm(instance=field) - - if request.POST: - if "save" in request.POST: - form = forms.FieldForm(request.POST, instance=field) - - if form.is_valid(): - logic.save_field(request, form) - return redirect(reverse("submission_fields")) - - elif "delete" in request.POST: - logic.delete_field(request) - return redirect(reverse("submission_fields")) - - elif "order[]" in request.POST: - logic.order_fields(request, fields) - return HttpResponse("Thanks") - - template = "admin/submission/manager/fields.html" - context = { - "field": field, - "fields": fields, - "form": form, - } - - return render(request, template, context) - - @role_can_access("licenses") def licenses(request, license_pk=None): """ diff --git a/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html b/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html new file mode 100644 index 0000000000..7d8efeb342 --- /dev/null +++ b/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html @@ -0,0 +1,52 @@ +{% extends "admin/core/base.html" %} +{% load static %} +{% load foundation %} + +{% block title %}Submission Field{% endblock %} +{% block title-section %}Submission Field{% endblock %} +{% block title-sub %} + {% if field %} + Edit a field for the submission platform + {% else %} + Add a field to the submission platform + {% endif %} +{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Manager
  • +
  • Submission Fields
  • + {% if field %} +
  • Edit {{ field.name }}
  • + {% else %} +
  • Add New Field
  • + {% endif %} +{% endblock %} + +{% block body %} +
    +
    +
    +
    + {% if field %} +

    Edit {{ field.name }}

    + {% else %} +

    Add New Field

    + {% endif %} +
    +
    + {% include "elements/forms/errors.html" with form=form %} +
    + {% csrf_token %} + {{ form|foundation }} + +
    +
    +
    +
    +
    +{% endblock %} + +{% block js %} + +{% endblock js %} diff --git a/src/templates/admin/submission/manager/fields.html b/src/templates/admin/submission/manager/additional_fields/fields.html similarity index 77% rename from src/templates/admin/submission/manager/fields.html rename to src/templates/admin/submission/manager/additional_fields/fields.html index 62221aa0ec..570ed795a1 100644 --- a/src/templates/admin/submission/manager/fields.html +++ b/src/templates/admin/submission/manager/additional_fields/fields.html @@ -20,9 +20,10 @@ {% block body %}
    -
    +

    Current Fields

    + Create New Field
    @@ -45,23 +46,6 @@

    Current Fields

    -
    -
    - {% if field %} -

    Edit {{ field.name }}

    - {% else %} -

    Add New Field

    - {% endif %} -
    -
    - {% include "elements/forms/errors.html" with form=form %} -
    - {% csrf_token %} - {{ form|foundation }} - -
    -
    -
    {% endblock %} From 1260fdcf1890c8c44c23f778b6eaae69ccc454b9 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Thu, 27 Aug 2026 09:45:54 -0500 Subject: [PATCH 2/6] Update to allow choices to be managed for additional fields such that there is better ordering as well as actual vs display value options. Jira: RCT-731 --- src/submission/additional_field/forms.py | 68 ++++++ src/submission/additional_field/views.py | 110 +++++++++- src/submission/admin.py | 20 +- src/submission/forms.py | 65 +++--- .../migrations/0093_merge_20260827_0810.py | 13 ++ .../0094_fieldchoice_fieldanswer_choice.py | 66 ++++++ src/submission/models.py | 200 +++++++++++++++--- src/submission/tests/test_workflow.py | 18 +- src/submission/urls.py | 10 + .../edit_additional_field.html | 10 +- .../manage_field_choices.html | 178 ++++++++++++++++ .../admin/submission/submit_review.html | 2 +- 12 files changed, 685 insertions(+), 75 deletions(-) create mode 100644 src/submission/additional_field/forms.py create mode 100644 src/submission/migrations/0093_merge_20260827_0810.py create mode 100644 src/submission/migrations/0094_fieldchoice_fieldanswer_choice.py create mode 100644 src/templates/admin/submission/manager/additional_fields/manage_field_choices.html diff --git a/src/submission/additional_field/forms.py b/src/submission/additional_field/forms.py new file mode 100644 index 0000000000..e3f0352f22 --- /dev/null +++ b/src/submission/additional_field/forms.py @@ -0,0 +1,68 @@ +from django import forms +from django.core.exceptions import ValidationError +from submission.models import FieldChoice + + +class FieldChoiceForm(forms.ModelForm): + class Meta: + model = FieldChoice + fields = ['real_value', 'display_value'] + widgets = { + 'real_value': forms.TextInput(attrs={'class': 'form-control'}), + 'display_value': forms.TextInput(attrs={'class': 'form-control'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['real_value'].widget.attrs.update({'class': 'form-control'}) + self.fields['display_value'].widget.attrs.update({'class': 'form-control'}) + + def clean_real_value(self): + real_value = self.cleaned_data.get('real_value') + if not real_value: + raise ValidationError("Real value is required.") + return real_value + + def clean_display_value(self): + display_value = self.cleaned_data.get('display_value') + if not display_value: + raise ValidationError("Display value is required.") + return display_value + + +class FieldChoicesManagementForm(forms.Form): + """ + Form for managing multiple field choices at once. + """ + def __init__(self, field, *args, **kwargs): + super().__init__(*args, **kwargs) + self.field = field + self.choices = field.field_choices.all().order_by('order') + + # Add fields for each existing choice + for choice in self.choices: + self.fields[f'real_value_{choice.id}'] = forms.CharField( + initial=choice.real_value, + required=True, + widget=forms.TextInput(attrs={'class': 'form-control'}) + ) + self.fields[f'display_value_{choice.id}'] = forms.CharField( + initial=choice.display_value, + required=True, + widget=forms.TextInput(attrs={'class': 'form-control'}) + ) + + def clean(self): + cleaned_data = super().clean() + + # Validate that real values are unique within the field + real_values = [] + for choice in self.choices: + real_value_key = f'real_value_{choice.id}' + if real_value_key in cleaned_data: + real_value = cleaned_data[real_value_key] + if real_value in real_values: + raise ValidationError(f"Duplicate real value found: {real_value}") + real_values.append(real_value) + + return cleaned_data \ No newline at end of file diff --git a/src/submission/additional_field/views.py b/src/submission/additional_field/views.py index 074f407dad..ac7b8ecbe1 100644 --- a/src/submission/additional_field/views.py +++ b/src/submission/additional_field/views.py @@ -1,12 +1,14 @@ from django.db.models import QuerySet -from django.http import HttpResponse -from django.shortcuts import redirect, render +from django.http import HttpResponse, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from django.views.decorators.csrf import csrf_exempt from security.decorators import editor_user_required from submission import forms, logic from submission.forms import FieldForm -from submission.models import Field +from submission.models import Field, FieldChoice +from submission.additional_field.forms import FieldChoiceForm, FieldChoicesManagementForm @editor_user_required @@ -63,3 +65,105 @@ def edit_additional_field_view(request, field_id: str | None = None) -> HttpResp } return render(request, template, context) + + +@editor_user_required +def manage_field_choices_view(request, field_id: str) -> HttpResponse: + """ + Allows the editor to manage field choices for a select field. + :param request: HttpRequest object + :param field_id: The ID of the field. + :return: HttpResponse + """ + field: Field = get_object_or_404(Field, pk=field_id) + + # Only allow managing choices for select fields + if field.kind != "select": + return redirect(reverse("submission_fields")) + + choices = field.field_choices.all().order_by('order') + + if request.POST: + if "save" in request.POST: + # Handle saving all choices at once + for choice in choices: + real_value_key = f"real_value_{choice.id}" + display_value_key = f"display_value_{choice.id}" + + if real_value_key in request.POST and display_value_key in request.POST: + choice.real_value = request.POST[real_value_key] + choice.display_value = request.POST[display_value_key] + choice.save() + + # Handle adding a new choice + new_real_value = request.POST.get("new_real_value", "").strip() + new_display_value = request.POST.get("new_display_value", "").strip() + + if new_real_value and new_display_value: + FieldChoice.objects.create( + field=field, + real_value=new_real_value, + display_value=new_display_value, + order=choices.count() + ) + + return redirect(reverse("manage_field_choices", kwargs={"field_id": field_id})) + + elif "delete" in request.POST: + choice_id = request.POST.get("delete") + try: + choice = FieldChoice.objects.get(pk=choice_id, field=field) + choice.delete() + + # Reorder remaining choices + remaining_choices = field.field_choices.all().order_by('order') + for i, choice in enumerate(remaining_choices): + choice.order = i + choice.save() + except FieldChoice.DoesNotExist: + pass + + return redirect(reverse("manage_field_choices", kwargs={"field_id": field_id})) + + template: str = ( + "admin/submission/manager/additional_fields/manage_field_choices.html" + ) + context = { + "field": field, + "choices": choices, + } + + return render(request, template, context) + + +@editor_user_required +@csrf_exempt +def reorder_field_choices_view(request, field_id: str) -> HttpResponse: + """ + Allows the editor to reorder field choices for a select field. + :param request: HttpRequest object + :param field_id: The ID of the field. + :return: JsonResponse + """ + field: Field = get_object_or_404(Field, pk=field_id) + + # Only allow managing choices for select fields + if field.kind != "select": + return JsonResponse({"error": "Invalid field type"}, status=400) + + if request.POST: + choice_ids = request.POST.getlist("choice[]") + choice_ids = [int(_id) for _id in choice_ids] + + # Update the order of choices + for i, choice_id in enumerate(choice_ids): + try: + choice = FieldChoice.objects.get(pk=choice_id, field=field) + choice.order = i + choice.save() + except FieldChoice.DoesNotExist: + pass + + return JsonResponse({"status": "success"}) + + return JsonResponse({"error": "Invalid request"}, status=400) diff --git a/src/submission/admin.py b/src/submission/admin.py index 1c713a15cf..9ea1ad96ab 100755 --- a/src/submission/admin.py +++ b/src/submission/admin.py @@ -3,12 +3,12 @@ __license__ = "AGPL v3" __maintainer__ = "Birkbeck Centre for Technology and Publishing" -from django.contrib import admin from django import forms +from django.contrib import admin from django.template.defaultfilters import truncatewords_html -from utils import admin_utils from submission import models +from utils import admin_utils class LicenseChoiceField(forms.ModelChoiceField): @@ -255,10 +255,24 @@ def apply_select_related(self, qs): return qs.prefetch_related("journal") +class FieldSectionInline(admin.TabularInline): + model = models.FieldSection + extra = 1 + raw_id_fields = ("section",) # Enables popup selector with filtering + ordering = ("order",) + + +class FieldChoiceInline(admin.TabularInline): + model = models.FieldChoice + extra = 1 + ordering = ("order",) + + class FieldAdmin(admin.ModelAdmin): list_display = ("name", "journal", "press", "kind", "width", "required", "display") list_filter = ("journal", "press", "kind", "width", "required", "display") search_fields = ("pk", "name", "help_text", "choices") + inlines = [FieldChoiceInline] class FieldAnswerAdmin(admin_utils.ArticleFKModelAdmin): @@ -267,7 +281,7 @@ class FieldAnswerAdmin(admin_utils.ArticleFKModelAdmin): search_fields = ("pk", "field__name", "article__pk", "article__title", "answer") def _answer(self, obj): - return truncatewords_html(obj.answer, 10) if obj else "" + return truncatewords_html(obj.display_value, 10) if obj else "" class SubmissionConfigAdmin(admin.ModelAdmin): diff --git a/src/submission/forms.py b/src/submission/forms.py index 200f8fdacb..605972de7d 100755 --- a/src/submission/forms.py +++ b/src/submission/forms.py @@ -3,27 +3,25 @@ __license__ = "AGPL v3" __maintainer__ = "Birkbeck Centre for Technology and Publishing" -import re import warnings from django import forms -from django.db.models import Q -from django.utils.translation import gettext, gettext_lazy as _ +from django.db.models import Q, QuerySet +from django.utils.translation import gettext_lazy as _ +from tinymce.widgets import TinyMCE -from submission import models from core import models as core_models from identifiers import models as ident_models -from review.logic import render_choices +from submission import models +from submission.models import Field, FieldAnswer +from utils import setting_handler from utils.forms import ( - KeywordModelForm, - JanewayTranslationModelForm, HTMLDateInput, - clean_orcid_id, + JanewayTranslationModelForm, + KeywordModelForm, YesNoRadio, + clean_orcid_id, ) -from utils import setting_handler - -from tinymce.widgets import TinyMCE class PublisherNoteForm(forms.ModelForm): @@ -200,7 +198,7 @@ def __init__(self, *args, **kwargs): ) elif element.kind == "select": - choices = render_choices(element.choices) + choices = element.get_choices() self.fields[element.name] = forms.ChoiceField( widget=forms.Select(attrs={"div_class": element.width}), choices=choices, @@ -226,7 +224,9 @@ def __init__(self, *args, **kwargs): check_for_answer = models.FieldAnswer.objects.get( field=element, article=article ) - self.fields[element.name].initial = check_for_answer.answer + self.fields[element.name].initial = ( + check_for_answer.answer_value + ) except models.FieldAnswer.DoesNotExist: pass @@ -239,10 +239,12 @@ def save(self, commit=True, request=None): article = super(ArticleInfo, self).save(commit=False) if request: - additional_fields = models.Field.objects.filter(journal=request.journal) + additional_fields: QuerySet[Field, Field] = models.Field.objects.filter( + journal=request.journal + ) - for field in additional_fields: - posted_value = request.POST.get(field.name) + for field in additional_fields: # type: Field + posted_value: str = request.POST.get(field.name) # Determine answer depending on field kind if field.kind == "check": @@ -252,21 +254,36 @@ def save(self, commit=True, request=None): answer = posted_value # Checkbox type inputs should pass here so they are recorded + field_answer: FieldAnswer if answer or field.kind == "check": try: field_answer = models.FieldAnswer.objects.get( article=article, field=field, ) - field_answer.answer = answer - field_answer.save() except models.FieldAnswer.DoesNotExist: - models.FieldAnswer.objects.create( + field_answer = models.FieldAnswer( article=article, field=field, - answer=answer, ) + # If this is a select field with FieldChoice objects, try to reference the choice + if field.kind == "select" and field.field_choices.exists(): + try: + choice = field.field_choices.get(real_value=answer) + field_answer.choice = choice + field_answer.answer = answer # Keep the raw value as backup + except models.FieldChoice.DoesNotExist: + # If the choice doesn't exist, just store the raw value + field_answer.choice = None + field_answer.answer = answer + else: + # For non-select fields or fields without FieldChoice objects, store the raw value + field_answer.choice = None + field_answer.answer = answer + + field_answer.save() + if self.pop_disabled_fields: request.journal.submissionconfiguration.handle_defaults(article) @@ -415,10 +432,10 @@ def __init__(self, *args, **kwargs): self.fields["default_license"].queryset = models.Licence.objects.filter( journal=self.instance.journal, ) - self.fields[ - "open_peer_review_license" - ].queryset = models.Licence.objects.filter( - journal=self.instance.journal, + self.fields["open_peer_review_license"].queryset = ( + models.Licence.objects.filter( + journal=self.instance.journal, + ) ) def clean(self): diff --git a/src/submission/migrations/0093_merge_20260827_0810.py b/src/submission/migrations/0093_merge_20260827_0810.py new file mode 100644 index 0000000000..fab634877f --- /dev/null +++ b/src/submission/migrations/0093_merge_20260827_0810.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.29 on 2026-08-27 13:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("submission", "0090_article_custom_how_to_cite_cy_and_more"), + ("submission", "0092_merge_0089_merge_20260226_1524_0091_alter_field_slug"), + ] + + operations = [] diff --git a/src/submission/migrations/0094_fieldchoice_fieldanswer_choice.py b/src/submission/migrations/0094_fieldchoice_fieldanswer_choice.py new file mode 100644 index 0000000000..3e0cbaab75 --- /dev/null +++ b/src/submission/migrations/0094_fieldchoice_fieldanswer_choice.py @@ -0,0 +1,66 @@ +# Generated by Django 4.2.29 on 2026-08-27 14:00 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("submission", "0093_merge_20260827_0810"), + ] + + operations = [ + migrations.CreateModel( + name="FieldChoice", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "real_value", + models.CharField( + blank=True, + help_text="Value used for exports and internal processing", + max_length=200, + ), + ), + ( + "display_value", + models.CharField( + help_text="Value displayed to users", max_length=200 + ), + ), + ("order", models.PositiveIntegerField(default=0)), + ( + "field", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="field_choices", + related_query_name="field_choices", + to="submission.field", + ), + ), + ], + options={ + "ordering": ("order",), + "unique_together": {("field", "real_value")}, + }, + ), + migrations.AddField( + model_name="fieldanswer", + name="choice", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="submission.fieldchoice", + ), + ), + ] diff --git a/src/submission/models.py b/src/submission/models.py index 2b3a281e83..086bfc50f2 100755 --- a/src/submission/models.py +++ b/src/submission/models.py @@ -3,24 +3,17 @@ __license__ = "AGPL v3" __maintainer__ = "Birkbeck Centre for Technology and Publishing" -from bs4 import BeautifulSoup -from urllib.parse import urlparse -import uuid import os -from dateutil import parser as dateparser -from itertools import chain +import re +import uuid import warnings -from iso639 import Lang +from itertools import chain +from urllib.parse import urlparse +import swapper +from bs4 import BeautifulSoup +from dateutil import parser as dateparser from django.apps import apps -from django.urls import reverse -from django.db import ( - connection, - DEFAULT_DB_ALIAS, - models, -) -from django.db.models.query import RawQuerySet -from django.db.models.sql.query import get_order_dir from django.conf import settings from django.contrib.postgres.search import ( SearchQuery, @@ -28,46 +21,55 @@ SearchVector, SearchVectorField, ) -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ +from django.core import exceptions +from django.core.exceptions import ValidationError +from django.db import ( + DEFAULT_DB_ALIAS, + connection, + models, +) +from django.db.models.query import RawQuerySet +from django.db.models.signals import m2m_changed, pre_delete +from django.db.models.sql.query import get_order_dir +from django.dispatch import receiver from django.template import Context, Template from django.template.loader import render_to_string from django.templatetags.static import static -from django.db.models.signals import pre_delete, m2m_changed -from django.dispatch import receiver -from django.core import exceptions +from django.urls import reverse +from django.utils import timezone from django.utils.functional import cached_property -from django.utils.html import mark_safe -from django.utils.html import strip_tags -import swapper +from django.utils.html import mark_safe, strip_tags +from django.utils.translation import gettext_lazy as _ +from iso639 import Lang +from core import files, model_utils, workflow +from core import models as core_models from core.file_system import JanewayFileSystemStorage from core.model_utils import ( AbstractLastModifiedModel, - DynamicChoiceField, BaseSearchManagerMixin, - JanewayBleachField, + DateTimePickerModelField, + DynamicChoiceField, JanewayBleachCharField, + JanewayBleachField, M2MOrderedThroughField, - DateTimePickerModelField, ) -from core import workflow, model_utils, files, models as core_models -from core.templatetags.truncate import truncatesmart from core.templatetags import alt_text +from core.templatetags.truncate import truncatesmart from identifiers import logic as id_logic from identifiers import models as identifier_models +from journal import models as journal_models from metrics.logic import ArticleMetrics -from review import models as review_models from repository import models as repository_models -from utils.function_cache import cache -from utils.logger import get_logger -from utils.orcid import validate_orcid, COMPILED_ORCID_REGEX -from utils.forms import plain_text_validator -from journal import models as journal_models +from review import models as review_models from review.const import ( ReviewerDecisions as RD, ) from transform import utils as transform_utils +from utils.forms import plain_text_validator +from utils.function_cache import cache +from utils.logger import get_logger +from utils.orcid import COMPILED_ORCID_REGEX, validate_orcid logger = get_logger(__name__) @@ -2396,10 +2398,10 @@ def registration_preview(self): return id_logic.preview_registration_information(self) def close_core_workflow_objects(self): - from review import models as review_models from copyediting import models as copyedit_models from production import models as prod_models from proofing import models as proof_models + from review import models as review_models review_models.ReviewAssignment.objects.filter( article=self, @@ -3260,6 +3262,22 @@ class Field(models.Model): class Meta: ordering = ("order", "name") + def clean(self): + """Ensure slug minimum length.""" + if self.slug: + if len(self.slug) < 4: + raise ValidationError( + "Slug must be at least four characters in length." + ) + elif self.name: + """If not specified, transform name field into slug""" + + self.slug = re.sub(r"\s", "-", self.name.strip().lower()) + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + def __str__(self): return "Field: {0} ({1})".format(self.name, self.kind) @@ -3270,6 +3288,105 @@ def object(self): return self.journal + def is_global(self): + return not self.sections.exists() + + def get_choices(self): + """ + Returns a list of tuples (display_value, real_value) for this field's choices. + If FieldChoice objects exist, they are used; otherwise, the legacy choices field is used. + """ + if self.field_choices.exists(): + # Use FieldChoice objects + return [ + (choice.real_value, choice.display_value) + for choice in self.field_choices.all() + ] + elif self.choices: + # Use legacy choices field + c_split = self.choices.split("|") + return [(choice.capitalize(), choice) for choice in c_split] + else: + # No choices defined + return [] + + def get_choice_display_value(self, real_value): + """ + Returns the display value for a given real value. + If FieldChoice objects exist, they are used; otherwise, the legacy choices field is used. + """ + if self.field_choices.exists(): + # Use FieldChoice objects + try: + choice = self.field_choices.get(real_value=real_value) + return choice.display_value + except FieldChoice.DoesNotExist: + return real_value + elif self.choices: + # Use legacy choices field + c_split = self.choices.split("|") + if real_value in c_split: + return real_value.capitalize() + else: + return real_value + else: + # No choices defined + return real_value + + +class FieldSection(models.Model): + field = models.ForeignKey( + "Field", + on_delete=models.CASCADE, + ) + section = models.ForeignKey( + "submission.Section", + on_delete=models.CASCADE, + ) + order = models.PositiveIntegerField(default=0) + + class Meta: + ordering = ("order",) + unique_together = ("field", "section") + + def clean(self): + """Ensure section and field belong to the same journal.""" + if ( + self.field.journal + and self.section.journal + and self.field.journal != self.section.journal + ): + raise ValidationError("Section journal does not match field journal.") + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + + +class FieldChoice(models.Model): + field = models.ForeignKey( + Field, + on_delete=models.CASCADE, + related_name="field_choices", + related_query_name="field_choices", + ) + real_value = models.CharField( + max_length=200, + help_text="Value used for exports and internal processing", + blank=True, + ) + display_value = models.CharField( + max_length=200, help_text="Value displayed to users" + ) + order = models.PositiveIntegerField(default=0) + + class Meta: + ordering = ("order",) + unique_together = ("field", "real_value") + + def __str__(self): + return f"{self.display_value} ({self.real_value})" + class FieldAnswer(models.Model): field = models.ForeignKey(Field, null=True, blank=True, on_delete=models.SET_NULL) @@ -3278,6 +3395,21 @@ class FieldAnswer(models.Model): on_delete=models.CASCADE, ) answer = JanewayBleachField() + choice = models.ForeignKey( + FieldChoice, null=True, blank=True, on_delete=models.SET_NULL + ) + + @property + def answer_value(self): + if self.choice: + return self.choice.real_value + return self.answer + + @property + def display_value(self): + if self.choice: + return self.choice.display_value + return self.answer class ArticleAuthorOrder(models.Model): diff --git a/src/submission/tests/test_workflow.py b/src/submission/tests/test_workflow.py index a69630f01a..a1b4d6af52 100644 --- a/src/submission/tests/test_workflow.py +++ b/src/submission/tests/test_workflow.py @@ -3,16 +3,16 @@ __license__ = "AGPL v3" __maintainer__ = "Open Library of Humanities" -from mock import Mock import os -from django.http import Http404 -from django.test import TestCase -from django.utils import translation, timezone -from django.urls.base import clear_script_prefix from django.conf import settings +from django.http import Http404 from django.shortcuts import reverse +from django.test import TestCase from django.test.utils import override_settings +from django.urls.base import clear_script_prefix +from django.utils import timezone, translation +from mock import Mock from core.models import Account, File from identifiers import logic as id_logic @@ -25,7 +25,7 @@ models, ) from utils.forms import clean_orcid_id -from utils.install import update_xsl_files, update_settings, update_issue_types +from utils.install import update_issue_types, update_settings, update_xsl_files from utils.shared import clear_cache from utils.testing import helpers from utils.testing.helpers import create_galley @@ -739,7 +739,7 @@ def test_boolean_field_unchecked_sets_false(self): article=self.article, field=self.boolean_field, ) - self.assertEqual(field_answer.answer, "") + self.assertEqual(field_answer.answer_value, "") def test_boolean_field_unchecked_sets_true(self): post_data = { @@ -770,7 +770,7 @@ def test_boolean_field_unchecked_sets_true(self): article=self.article, field=self.boolean_field, ) - self.assertEqual(field_answer.answer, "on") + self.assertEqual(field_answer.answer_value, "on") def test_text_field_sets(self): post_data = { @@ -802,6 +802,6 @@ def test_text_field_sets(self): field=self.text_field, ) self.assertEqual( - field_answer.answer, + field_answer.answer_value, "Sometimes first contact is last contact.", ) diff --git a/src/submission/urls.py b/src/submission/urls.py index 684d0d6ea4..c056ca79ab 100755 --- a/src/submission/urls.py +++ b/src/submission/urls.py @@ -113,6 +113,16 @@ additional_fields_view.edit_additional_field_view, name="submission_fields_id", ), + re_path( + r"^manager/additional_fields/(?P\d+)/choices/$", + additional_fields_view.manage_field_choices_view, + name="manage_field_choices", + ), + re_path( + r"^manager/additional_fields/(?P\d+)/choices/order/$", + additional_fields_view.reorder_field_choices_view, + name="reorder_field_choices", + ), re_path(r"^manager/licences/$", views.licenses, name="submission_licenses"), re_path( r"^manager/licences/(?P\d+)/delete/", diff --git a/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html b/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html index 7d8efeb342..260017d143 100644 --- a/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html +++ b/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html @@ -31,7 +31,7 @@ {% if field %}

    Edit {{ field.name }}

    {% else %} -

    Add New Field

    +

    Add New Field

    {% endif %}
    @@ -39,6 +39,14 @@

    Add New Field

    {% csrf_token %} {{ form|foundation }} +
    + +

    Manage field choices with real and display values.

    + + Manage Field Choices + +
    +
    diff --git a/src/templates/admin/submission/manager/additional_fields/manage_field_choices.html b/src/templates/admin/submission/manager/additional_fields/manage_field_choices.html new file mode 100644 index 0000000000..a62e18d042 --- /dev/null +++ b/src/templates/admin/submission/manager/additional_fields/manage_field_choices.html @@ -0,0 +1,178 @@ +{% extends "admin/core/base.html" %} +{% load static %} +{% load foundation %} + +{% block title %}Manage Field Choices{% endblock %} +{% block title-section %}Manage Field Choices{% endblock %} +{% block title-sub %} + Manage choices for field: {{ field.name }} +{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Manager
  • +
  • Submission Fields
  • +
  • Edit {{ field.name }}
  • +
  • Manage Choices
  • +{% endblock %} + +{% block body %} +
    +
    +
    +
    +

    Manage Choices for "{{ field.name }}"

    +
    +
    + {% if choices %} +
    + {% csrf_token %} + + + + + + + + + + + {% for choice in choices %} + + + + + + + {% endfor %} + +
    OrderDisplay ValueReal ValueActions
    + + + + + +
    + +
    + +
    + +

    Add New Choice

    +
    + {% csrf_token %} +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + {% else %} +

    No choices have been defined for this field yet.

    + +

    Add New Choice

    +
    + {% csrf_token %} +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + {% endif %} + +
    + + Back to Field Edit + +
    +
    +
    +
    +{% endblock %} + +{% block js %} + + + +{% endblock js %} \ No newline at end of file diff --git a/src/templates/admin/submission/submit_review.html b/src/templates/admin/submission/submit_review.html index f8033ad0c1..309152362b 100644 --- a/src/templates/admin/submission/submit_review.html +++ b/src/templates/admin/submission/submit_review.html @@ -96,7 +96,7 @@

    {% trans 'Agreements' %}

    {{ field_answer.field.name }} - {{ field_answer.answer|safe }} + {{ field_answer.display_value|safe }} {% endfor %} From 87b586401a33373ef17924c53cf1640eaaa00fc7 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Thu, 27 Aug 2026 10:44:02 -0500 Subject: [PATCH 3/6] Update how saving and manage choice buttons are accessed. Jira: RCT-731 --- src/submission/additional_field/views.py | 57 +++++++++++-------- src/submission/logic.py | 22 +++---- .../edit_additional_field.html | 17 +++++- 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/submission/additional_field/views.py b/src/submission/additional_field/views.py index ac7b8ecbe1..c273e23aa8 100644 --- a/src/submission/additional_field/views.py +++ b/src/submission/additional_field/views.py @@ -6,9 +6,12 @@ from security.decorators import editor_user_required from submission import forms, logic +from submission.additional_field.forms import ( + FieldChoiceForm, + FieldChoicesManagementForm, +) from submission.forms import FieldForm from submission.models import Field, FieldChoice -from submission.additional_field.forms import FieldChoiceForm, FieldChoicesManagementForm @editor_user_required @@ -53,8 +56,10 @@ def edit_additional_field_view(request, field_id: str | None = None) -> HttpResp form = forms.FieldForm(request.POST, instance=field) if form.is_valid(): - logic.save_field(request, form) - return redirect(reverse("submission_fields")) + field = logic.save_field(request, form) + return redirect( + reverse("submission_fields_id", kwargs={"field_id": field.pk}) + ) template: str = ( "admin/submission/manager/additional_fields/edit_additional_field.html" @@ -76,55 +81,59 @@ def manage_field_choices_view(request, field_id: str) -> HttpResponse: :return: HttpResponse """ field: Field = get_object_or_404(Field, pk=field_id) - + # Only allow managing choices for select fields if field.kind != "select": return redirect(reverse("submission_fields")) - - choices = field.field_choices.all().order_by('order') - + + choices = field.field_choices.all().order_by("order") + if request.POST: if "save" in request.POST: # Handle saving all choices at once for choice in choices: real_value_key = f"real_value_{choice.id}" display_value_key = f"display_value_{choice.id}" - + if real_value_key in request.POST and display_value_key in request.POST: choice.real_value = request.POST[real_value_key] choice.display_value = request.POST[display_value_key] choice.save() - + # Handle adding a new choice new_real_value = request.POST.get("new_real_value", "").strip() new_display_value = request.POST.get("new_display_value", "").strip() - + if new_real_value and new_display_value: FieldChoice.objects.create( field=field, real_value=new_real_value, display_value=new_display_value, - order=choices.count() + order=choices.count(), ) - - return redirect(reverse("manage_field_choices", kwargs={"field_id": field_id})) - + + return redirect( + reverse("manage_field_choices", kwargs={"field_id": field_id}) + ) + elif "delete" in request.POST: choice_id = request.POST.get("delete") try: choice = FieldChoice.objects.get(pk=choice_id, field=field) choice.delete() - + # Reorder remaining choices - remaining_choices = field.field_choices.all().order_by('order') + remaining_choices = field.field_choices.all().order_by("order") for i, choice in enumerate(remaining_choices): choice.order = i choice.save() except FieldChoice.DoesNotExist: pass - - return redirect(reverse("manage_field_choices", kwargs={"field_id": field_id})) - + + return redirect( + reverse("manage_field_choices", kwargs={"field_id": field_id}) + ) + template: str = ( "admin/submission/manager/additional_fields/manage_field_choices.html" ) @@ -146,15 +155,15 @@ def reorder_field_choices_view(request, field_id: str) -> HttpResponse: :return: JsonResponse """ field: Field = get_object_or_404(Field, pk=field_id) - + # Only allow managing choices for select fields if field.kind != "select": return JsonResponse({"error": "Invalid field type"}, status=400) - + if request.POST: choice_ids = request.POST.getlist("choice[]") choice_ids = [int(_id) for _id in choice_ids] - + # Update the order of choices for i, choice_id in enumerate(choice_ids): try: @@ -163,7 +172,7 @@ def reorder_field_choices_view(request, field_id: str) -> HttpResponse: choice.save() except FieldChoice.DoesNotExist: pass - + return JsonResponse({"status": "success"}) - + return JsonResponse({"error": "Invalid request"}, status=400) diff --git a/src/submission/logic.py b/src/submission/logic.py index 40614bdf09..b020a0a0eb 100755 --- a/src/submission/logic.py +++ b/src/submission/logic.py @@ -6,21 +6,23 @@ import warnings from bs4 import BeautifulSoup - +from django.contrib import messages from django.db.models import Q from django.shortcuts import get_object_or_404 -from django.contrib import messages -from django.utils.translation import get_language, gettext_lazy as _ +from django.utils.translation import get_language +from django.utils.translation import gettext_lazy as _ -from core.forms import OrcidAffiliationForm -from core.model_utils import generate_dummy_email from core import files from core import models as core_models -from utils import orcid, setting_handler, shared as utils_shared -from utils.forms import clean_orcid_id +from core.forms import OrcidAffiliationForm +from core.model_utils import generate_dummy_email from submission import models from submission.const import AddAuthorStatus -from submission.forms import EditFrozenAuthor, CreditRecordForm +from submission.forms import CreditRecordForm, EditFrozenAuthor +from submission.models import Field +from utils import orcid, setting_handler +from utils import shared as utils_shared +from utils.forms import clean_orcid_id def add_self_as_author(user, article): @@ -257,7 +259,7 @@ def get_submission_fields(request): return fields -def save_field(request, form): +def save_field(request, form) -> Field: """ Saves a form field and sets the press or journal parameter. :param request: @@ -265,7 +267,7 @@ def save_field(request, form): :return: """ - new_field = form.save(commit=False) + new_field: Field = form.save(commit=False) if request.journal: new_field.journal = request.journal diff --git a/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html b/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html index 260017d143..617738c92e 100644 --- a/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html +++ b/src/templates/admin/submission/manager/additional_fields/edit_additional_field.html @@ -29,7 +29,7 @@
    {% if field %} -

    Edit {{ field.name }}

    +

    Edit {{ field.name }}

    {% else %}

    Add New Field

    {% endif %} @@ -39,10 +39,23 @@

    Add New Field

    {% csrf_token %} {{ form|foundation }} + + From a2db35fa330e451fcf098c2125ee94ebc25092e6 Mon Sep 17 00:00:00 2001 From: Rose Reatherford <24795051-rosereatherford@users.noreply.gitlab.com> Date: Tue, 1 Sep 2026 08:00:42 -0500 Subject: [PATCH 4/6] Adds description and rich text editor to fields --- .../migrations/0095_field_description.py | 23 +++++++++++++++++++ src/submission/models.py | 5 ++++ 2 files changed, 28 insertions(+) create mode 100644 src/submission/migrations/0095_field_description.py diff --git a/src/submission/migrations/0095_field_description.py b/src/submission/migrations/0095_field_description.py new file mode 100644 index 0000000000..ddb31ee009 --- /dev/null +++ b/src/submission/migrations/0095_field_description.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.29 on 2026-09-01 10:16 + +import core.model_utils +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("submission", "0094_fieldchoice_fieldanswer_choice"), + ] + + operations = [ + migrations.AddField( + model_name="field", + name="description", + field=core.model_utils.JanewayBleachField( + blank=True, + help_text="Description of the field to show to the user.", + null=True, + ), + ), + ] diff --git a/src/submission/models.py b/src/submission/models.py index 086bfc50f2..a62775169d 100755 --- a/src/submission/models.py +++ b/src/submission/models.py @@ -3258,6 +3258,11 @@ class Field(models.Model): default=False, help_text="Whether or not display this field in the article page" ) help_text = models.TextField() + description = JanewayBleachField( + blank=True, + null=True, + help_text="Description of the field to show to the user.", + ) class Meta: ordering = ("order", "name") From 0d8b91f2d9f0407908c0146c76ca06d477a21805 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Thu, 3 Sep 2026 09:47:24 -0500 Subject: [PATCH 5/6] Update migrations. Jira: RCT-734 --- ...ieldchoice_fieldanswer_choice_and_more.py} | 47 ++++++++++++++++++- .../migrations/0093_merge_20260827_0810.py | 13 ----- .../migrations/0095_field_description.py | 23 --------- 3 files changed, 45 insertions(+), 38 deletions(-) rename src/submission/migrations/{0094_fieldchoice_fieldanswer_choice.py => 0091_field_description_fieldchoice_fieldanswer_choice_and_more.py} (55%) delete mode 100644 src/submission/migrations/0093_merge_20260827_0810.py delete mode 100644 src/submission/migrations/0095_field_description.py diff --git a/src/submission/migrations/0094_fieldchoice_fieldanswer_choice.py b/src/submission/migrations/0091_field_description_fieldchoice_fieldanswer_choice_and_more.py similarity index 55% rename from src/submission/migrations/0094_fieldchoice_fieldanswer_choice.py rename to src/submission/migrations/0091_field_description_fieldchoice_fieldanswer_choice_and_more.py index 3e0cbaab75..cf6e79b1fb 100644 --- a/src/submission/migrations/0094_fieldchoice_fieldanswer_choice.py +++ b/src/submission/migrations/0091_field_description_fieldchoice_fieldanswer_choice_and_more.py @@ -1,5 +1,6 @@ -# Generated by Django 4.2.29 on 2026-08-27 14:00 +# Generated by Django 4.2.29 on 2026-09-03 14:47 +import core.model_utils from django.db import migrations, models import django.db.models.deletion @@ -7,10 +8,19 @@ class Migration(migrations.Migration): dependencies = [ - ("submission", "0093_merge_20260827_0810"), + ("submission", "0090_article_custom_how_to_cite_cy_and_more"), ] operations = [ + migrations.AddField( + model_name="field", + name="description", + field=core.model_utils.JanewayBleachField( + blank=True, + help_text="Description of the field to show to the user.", + null=True, + ), + ), migrations.CreateModel( name="FieldChoice", fields=[ @@ -63,4 +73,37 @@ class Migration(migrations.Migration): to="submission.fieldchoice", ), ), + migrations.CreateModel( + name="FieldSection", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order", models.PositiveIntegerField(default=0)), + ( + "field", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="submission.field", + ), + ), + ( + "section", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="submission.section", + ), + ), + ], + options={ + "ordering": ("order",), + "unique_together": {("field", "section")}, + }, + ), ] diff --git a/src/submission/migrations/0093_merge_20260827_0810.py b/src/submission/migrations/0093_merge_20260827_0810.py deleted file mode 100644 index fab634877f..0000000000 --- a/src/submission/migrations/0093_merge_20260827_0810.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 4.2.29 on 2026-08-27 13:10 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("submission", "0090_article_custom_how_to_cite_cy_and_more"), - ("submission", "0092_merge_0089_merge_20260226_1524_0091_alter_field_slug"), - ] - - operations = [] diff --git a/src/submission/migrations/0095_field_description.py b/src/submission/migrations/0095_field_description.py deleted file mode 100644 index ddb31ee009..0000000000 --- a/src/submission/migrations/0095_field_description.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.29 on 2026-09-01 10:16 - -import core.model_utils -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("submission", "0094_fieldchoice_fieldanswer_choice"), - ] - - operations = [ - migrations.AddField( - model_name="field", - name="description", - field=core.model_utils.JanewayBleachField( - blank=True, - help_text="Description of the field to show to the user.", - null=True, - ), - ), - ] From 93cc8d9dbb8b5ac55cbe3498b844f4439e4c912d Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Thu, 3 Sep 2026 09:50:10 -0500 Subject: [PATCH 6/6] Formatting. Jira: RCT-734 --- src/submission/additional_field/forms.py | 43 ++++++++++--------- src/submission/forms.py | 14 +++--- ...fieldchoice_fieldanswer_choice_and_more.py | 1 - 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/submission/additional_field/forms.py b/src/submission/additional_field/forms.py index e3f0352f22..bdceb42a81 100644 --- a/src/submission/additional_field/forms.py +++ b/src/submission/additional_field/forms.py @@ -6,25 +6,25 @@ class FieldChoiceForm(forms.ModelForm): class Meta: model = FieldChoice - fields = ['real_value', 'display_value'] + fields = ["real_value", "display_value"] widgets = { - 'real_value': forms.TextInput(attrs={'class': 'form-control'}), - 'display_value': forms.TextInput(attrs={'class': 'form-control'}), + "real_value": forms.TextInput(attrs={"class": "form-control"}), + "display_value": forms.TextInput(attrs={"class": "form-control"}), } - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['real_value'].widget.attrs.update({'class': 'form-control'}) - self.fields['display_value'].widget.attrs.update({'class': 'form-control'}) - + self.fields["real_value"].widget.attrs.update({"class": "form-control"}) + self.fields["display_value"].widget.attrs.update({"class": "form-control"}) + def clean_real_value(self): - real_value = self.cleaned_data.get('real_value') + real_value = self.cleaned_data.get("real_value") if not real_value: raise ValidationError("Real value is required.") return real_value - + def clean_display_value(self): - display_value = self.cleaned_data.get('display_value') + display_value = self.cleaned_data.get("display_value") if not display_value: raise ValidationError("Display value is required.") return display_value @@ -34,35 +34,36 @@ class FieldChoicesManagementForm(forms.Form): """ Form for managing multiple field choices at once. """ + def __init__(self, field, *args, **kwargs): super().__init__(*args, **kwargs) self.field = field - self.choices = field.field_choices.all().order_by('order') - + self.choices = field.field_choices.all().order_by("order") + # Add fields for each existing choice for choice in self.choices: - self.fields[f'real_value_{choice.id}'] = forms.CharField( + self.fields[f"real_value_{choice.id}"] = forms.CharField( initial=choice.real_value, required=True, - widget=forms.TextInput(attrs={'class': 'form-control'}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) - self.fields[f'display_value_{choice.id}'] = forms.CharField( + self.fields[f"display_value_{choice.id}"] = forms.CharField( initial=choice.display_value, required=True, - widget=forms.TextInput(attrs={'class': 'form-control'}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) - + def clean(self): cleaned_data = super().clean() - + # Validate that real values are unique within the field real_values = [] for choice in self.choices: - real_value_key = f'real_value_{choice.id}' + real_value_key = f"real_value_{choice.id}" if real_value_key in cleaned_data: real_value = cleaned_data[real_value_key] if real_value in real_values: raise ValidationError(f"Duplicate real value found: {real_value}") real_values.append(real_value) - - return cleaned_data \ No newline at end of file + + return cleaned_data diff --git a/src/submission/forms.py b/src/submission/forms.py index 605972de7d..475a5b4405 100755 --- a/src/submission/forms.py +++ b/src/submission/forms.py @@ -224,9 +224,9 @@ def __init__(self, *args, **kwargs): check_for_answer = models.FieldAnswer.objects.get( field=element, article=article ) - self.fields[element.name].initial = ( - check_for_answer.answer_value - ) + self.fields[ + element.name + ].initial = check_for_answer.answer_value except models.FieldAnswer.DoesNotExist: pass @@ -432,10 +432,10 @@ def __init__(self, *args, **kwargs): self.fields["default_license"].queryset = models.Licence.objects.filter( journal=self.instance.journal, ) - self.fields["open_peer_review_license"].queryset = ( - models.Licence.objects.filter( - journal=self.instance.journal, - ) + self.fields[ + "open_peer_review_license" + ].queryset = models.Licence.objects.filter( + journal=self.instance.journal, ) def clean(self): diff --git a/src/submission/migrations/0091_field_description_fieldchoice_fieldanswer_choice_and_more.py b/src/submission/migrations/0091_field_description_fieldchoice_fieldanswer_choice_and_more.py index cf6e79b1fb..0a1406cb9d 100644 --- a/src/submission/migrations/0091_field_description_fieldchoice_fieldanswer_choice_and_more.py +++ b/src/submission/migrations/0091_field_description_fieldchoice_fieldanswer_choice_and_more.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [ ("submission", "0090_article_custom_how_to_cite_cy_and_more"), ]