/targets/accept-suggestions", methods=["POST"])
+@login_required
+def accept_suggestions(assembly_id: uuid.UUID) -> ResponseReturnValue:
+ """Apply one or all of the algorithm's recommended target changes.
+
+ The suggestions are re-derived here rather than trusted from the form, so a
+ stale page cannot apply a number the check no longer recommends. Each applied
+ value gets a comment recording where the change came from; for a single
+ accept the organiser may have edited that comment before saving.
+
+ ``then=run`` re-runs the failed selection after saving - a test run as a test
+ run - and is only honoured when the organiser arrived from one
+ (``from=selection``).
+ """
+ provenance = _selection_provenance(request.form)
+ run_after = bool(provenance) and request.form.get("then") == "run"
+ recheck_url = url_for("targets.check_targets", assembly_id=assembly_id, **provenance)
+
+ try:
+ uow = bootstrap.get_flask_uow()
+ task_id = None
+ with uow:
+ get_assembly_with_permissions(uow, assembly_id, current_user.id)
+ check = check_targets_detailed(uow, current_user.id, assembly_id)
+ categories = get_targets_for_assembly(uow, current_user.id, assembly_id)
+
+ if request.form.get("scope") == "all":
+ applied = _accept_all(uow, assembly_id, categories, check)
+ else:
+ applied = _accept_one(uow, assembly_id, categories, check, request.form)
+
+ if not applied:
+ flash(_("That suggestion is no longer available. The targets have been re-checked."), "warning")
+ return redirect_preserving_scroll(recheck_url)
+
+ if run_after:
+ task_id = _start_rerun(uow, assembly_id, provenance)
+
+ flash(_("Recommended changes applied."), "success")
+ return _redirect_after_save(assembly_id, task_id, recheck_url)
+
+ except NotFoundError:
+ flash(_("Assembly not found"), "error")
+ return redirect(url_for("backoffice.dashboard"))
+ except InsufficientPermissions:
+ flash(_("You don't have permission to manage this assembly"), "error")
+ return redirect(url_for("backoffice.dashboard"))
+ except Exception as e:
+ logger.exception("Error accepting target suggestions", assembly_id=str(assembly_id), error=str(e))
+ flash(_("An unexpected error occurred while applying the recommended changes"), "error")
+ return redirect(url_for("targets.view_assembly_targets", assembly_id=assembly_id))
+
+
def _render_targets_edit_errors(
assembly_id: uuid.UUID,
form_data: Any,
@@ -439,6 +640,7 @@ def _render_targets_edit_errors(
pending_categories=pending_categories(form_data),
field_errors=errors_by_field(errors),
editing_all=True,
+ selection_provenance=_selection_provenance(form_data),
value_form=TargetValueForm(),
can_manage=_can_manage(assembly_id),
all_respondent_counts=build_respondent_counts(assembly_id, target_categories, attribute_columns),
@@ -492,6 +694,9 @@ def save_all(assembly_id: uuid.UUID) -> ResponseReturnValue:
if errors:
return _render_targets_edit_errors(assembly_id, request.form, errors)
+ provenance = _selection_provenance(request.form)
+ run_after = bool(provenance) and request.form.get("then") == "run"
+ task_id = None
uow = bootstrap.get_flask_uow()
with uow:
save_all_targets(
@@ -501,11 +706,15 @@ def save_all(assembly_id: uuid.UUID) -> ResponseReturnValue:
edits,
force_unlink=request.form.get("force_unlink") == "1",
)
+ if run_after:
+ task_id = _start_rerun(uow, assembly_id, provenance)
flash(_("Targets saved"), "success")
# The detailed check is the whole point of saving: land on the page that
# runs it, so its annotations arrive without anyone asking for them.
- return redirect(url_for("targets.check_targets", assembly_id=assembly_id))
+ return _redirect_after_save(
+ assembly_id, task_id, url_for("targets.check_targets", assembly_id=assembly_id, **provenance)
+ )
except TargetLinkedError as e:
return _render_force_unlink_confirm(assembly_id, request.form, e.blocks)
diff --git a/backend/src/opendlp/service_layer/target_checking.py b/backend/src/opendlp/service_layer/target_checking.py
index a4bb920df..def006e45 100644
--- a/backend/src/opendlp/service_layer/target_checking.py
+++ b/backend/src/opendlp/service_layer/target_checking.py
@@ -65,6 +65,20 @@ class DetailedCheckResult:
num_features: int = 0
num_people: int = 0
+ @property
+ def has_suggestions(self) -> bool:
+ """True when any value carries an applicable min/max suggestion.
+
+ Drives whether the targets page offers the "accept recommended changes"
+ actions: there is nothing to accept unless the solver relaxed a quota.
+ """
+ return any(
+ annotation.level == "suggestion" and annotation.suggested_value is not None
+ for value_annotations in self.annotations.values()
+ for annotations in value_annotations.values()
+ for annotation in annotations
+ )
+
def _add_annotation(annotations: AnnotationsDict, category: str, value: str, annotation: TargetAnnotation) -> None:
if category not in annotations:
diff --git a/backend/templates/backoffice/assembly_targets.html b/backend/templates/backoffice/assembly_targets.html
index 6225ade8e..135245a48 100644
--- a/backend/templates/backoffice/assembly_targets.html
+++ b/backend/templates/backoffice/assembly_targets.html
@@ -74,9 +74,21 @@
discardUrl is passed in rather than read from the address bar: a
rejected save re-renders this page at the save-all POST url, which
would 405 on the GET a reload makes. #}
+ {# Where a failed selection run sent the organiser from - from, run_id and,
+ for a test run, test - carried on every form and link that leads back
+ here, so a re-run starts the same kind of selection. Empty otherwise. #}
+ {% set provenance = selection_provenance|default({}) %}
+ {% set rerun_is_test = provenance.get("test") == "1" %}
+ {# Discarding edits made against the check's suggestions goes back to the
+ check, suggestions and all, rather than to a page without them. #}
+ {% if check_result is defined and check_result %}
+ {% set discard_url = url_for('targets.check_targets', assembly_id=assembly.id, **provenance) %}
+ {% else %}
+ {% set discard_url = url_for('targets.view_assembly_targets', assembly_id=assembly.id) %}
+ {% endif %}
{# ── Targets Heading and Actions ──────────────── #}
@@ -143,6 +155,49 @@ {{ _("Ta
{% endif %}
+ {# ── Accept recommended changes ──────────────────── #}
+ {# Both views carry the suggestions, so both get the page-level
+ actions. Read-only, they save at once. Editing, "Accept all" only
+ fills the rows in - the save bar keeps them, or saves and re-runs.
+ Re-running is offered only to an organiser who arrived from a
+ failed selection run. #}
+ {% if can_manage and check_result is defined and check_result and check_result.has_suggestions %}
+
+
+
+ {{ _("The algorithm has recommended target changes. Accept them individually below, or all at once here.") }}
+
+
+ {{ button(_("Accept all suggestions"), variant="secondary", attrs='@click="acceptAllSuggestions()"') }}
+
+
+
+ {% if provenance %}
+
+ {% endif %}
+
+
+
+ {% endif %}
+
{# ── Category Blocks ─────────────────────────────── #}
{# Rendered even with nothing to edit yet: "Add target" clones into
this form, so it has to be here before the first target is. #}
@@ -179,6 +234,9 @@
{{ _("Ta
diff --git a/backend/templates/backoffice/components/db_selection_progress_modal.html b/backend/templates/backoffice/components/db_selection_progress_modal.html
index 56a305d94..4f1d0bc18 100644
--- a/backend/templates/backoffice/components/db_selection_progress_modal.html
+++ b/backend/templates/backoffice/components/db_selection_progress_modal.html
@@ -185,4 +185,19 @@
{{ modal_footer_end() }}
{% endif %}
+{# A failed initial or test selection usually means the targets cannot be met. Send the
+ organiser straight to the targets check, with the feasibility suggestions
+ showing and ready to accept, rather than leaving them at a dead end. #}
+{% if run_record.is_failed and run_record.task_type.value in ('select_from_db', 'test_select_from_db') %}
+ {# test=1 is how the targets page knows to re-run a test selection, not the real one. #}
+ {% set adjust_args = {'from': 'selection', 'run_id': current_selection, 'edit': '1'} %}
+ {% if is_test_selection %}{% set adjust_args = dict(adjust_args, test='1') %}{% endif %}
+ {{ modal_footer_start() }}
+
+ {{ button(_("Adjust targets") ,
+ href=url_for('targets.check_targets', assembly_id=assembly.id, **adjust_args),
+ variant="primary") }}
+
+ {{ modal_footer_end() }}
+{% endif %}
{% endcall %}
diff --git a/backend/templates/backoffice/components/suggestion_row.html b/backend/templates/backoffice/components/suggestion_row.html
new file mode 100644
index 000000000..fa3d500f9
--- /dev/null
+++ b/backend/templates/backoffice/components/suggestion_row.html
@@ -0,0 +1,110 @@
+{#
+ABOUTME: A table row offering one suggested change, accepted with a single click
+ABOUTME: Accept either posts to the server or writes the change into inputs elsewhere on the page
+#}
+{% from "backoffice/components/button.html" import button %}
+{% from "backoffice/components/input.html" import input %}
+{#
+ One suggestion - "set the minimum to 3" - as a full-width row under the table
+ row it is about.
+
+ The comment that will be saved with the change is on show and editable from
+ the start, so accepting is one click: there is no step that opens an editor
+ and a second one that saves it.
+
+ What Accept does depends on what is passed:
+
+ action - the row is a form; Accept posts the comment, the csrf token and
+ `hidden_fields` there. For a page that saves each change as it
+ is made.
+ apply - {input id: value}. Accept writes each value into that input on
+ the page, and the comment into the input `comment_target` names,
+ firing `input` on each so totals and dirty-tracking hear of it.
+ Nothing is saved; the page's own Save does that. Accepted rows
+ can be undone, and all of them accept at once on a
+ `suggestions-accept-all` window event.
+ neither - the suggestion is shown with nothing to accept, as for someone
+ who may not change what it is about.
+
+ Args:
+ message: what is suggested, in words
+ colspan: how many columns of the surrounding table the row spans
+ comment: the comment saved with the change, pre-filled and editable
+ comment_id: id of the comment input (needed when it is shown)
+ action: url Accept posts to
+ hidden_fields: {name: value} posted with the comment
+ form_attrs: extra attributes for the form, in action mode
+ apply: {input id: value} written in on Accept, in apply mode
+ comment_target: id of the input the comment is written into
+ accept_label: the Accept button's text
+
+ Usage:
+ {{ suggestion_row(_("Suggested minimum: 3"), colspan=6,
+ comment="Accepted algorithm suggestion", comment_id="c-1",
+ action=url_for(...), hidden_fields={"field": "min"}) }}
+ {{ suggestion_row(_("Suggested minimum: 3"), colspan=6,
+ comment="Accepted algorithm suggestion", comment_id="c-1",
+ apply={"row-1-min": 3}, comment_target="row-1-comment") }}
+#}
+{% macro suggestion_row(message, colspan=1, comment="", comment_id="", action="", hidden_fields={}, form_attrs="", apply={}, comment_target="", accept_label="") %}
+ {% set applies = apply | length > 0 %}
+ {% set actionable = action or applies %}
+ {% set accept_text = accept_label or _("Accept") %}
+ {% set comment_label = _("Comment saved with this change") %}
+ {# tojson escapes quotes and angle brackets, so the map is safe inside a
+ single-quoted attribute - and Alpine never has to parse it. #}
+
+
+
+
+ {{ _("Suggestion") }}
+ {{ message }}
+
+ {% if actionable %}
+ {% set controls %}
+
{{ comment_label }}
+
+ {# Unnamed in apply mode: it is copied into the row, and must not
+ be submitted with whatever form the row sits in. #}
+ {{ input(name="comment" if action else "",
+ id=comment_id,
+ value=comment,
+ attrs='x-ref="comment"' if applies else "") }}
+
+ {% if action %}
+ {{ button(accept_text, type="submit", variant="secondary") }}
+ {% else %}
+ {{ button(accept_text, variant="secondary", attrs='@click="accept()"') }}
+ {% endif %}
+ {% endset %}
+ {% if action %}
+
+ {% else %}
+
{{ controls }}
+
+ {{ _("Accepted. Save to keep the change.") }}
+ {{ button(_("Undo"), variant="tertiary", attrs='@click="undo()"') }}
+
+ {% endif %}
+ {% endif %}
+
+
+
+{% endmacro %}
diff --git a/backend/templates/backoffice/showcase.html b/backend/templates/backoffice/showcase.html
index bb4ba1aee..9a40e3140 100644
--- a/backend/templates/backoffice/showcase.html
+++ b/backend/templates/backoffice/showcase.html
@@ -22,6 +22,7 @@
{% from "backoffice/showcase/tabs_component.html" import tabs_section %}
{% from "backoffice/showcase/stepper_component.html" import stepper_section %}
{% from "backoffice/showcase/alert_component.html" import alert_section %}
+{% from "backoffice/showcase/suggestion_row_component.html" import suggestion_row_section %}
{% from "backoffice/showcase/search_dropdown_component.html" import search_dropdown_section %}
{% from "backoffice/showcase/select_dropdown_component.html" import select_dropdown_section %}
{% from "backoffice/showcase/modal_component.html" import modal_section %}
@@ -111,6 +112,7 @@ Design System<
Tabs
Stepper
Alert
+ Suggestion Row
Alpine.js Interactivity
@@ -177,6 +179,9 @@ Design System<
{{ tabs_section() }}
{{ stepper_section() }}
{{ alert_section() }}
+ {{ suggestion_row_section() }}
{{ interactivity_section() }}
diff --git a/backend/templates/backoffice/showcase/suggestion_row_component.html b/backend/templates/backoffice/showcase/suggestion_row_component.html
new file mode 100644
index 000000000..0e72780c0
--- /dev/null
+++ b/backend/templates/backoffice/showcase/suggestion_row_component.html
@@ -0,0 +1,88 @@
+{#
+ABOUTME: Showcase section for the suggestion row component
+ABOUTME: Shows the message-only, save-on-accept and fill-in-on-accept forms of the row
+#}
+{% from "backoffice/components/showcase_section.html" import showcase_section, showcase_title, showcase_description, showcase_subtitle, showcase_example, showcase_tokens %}
+{% from "backoffice/components/suggestion_row.html" import suggestion_row %}
+{% from "backoffice/components/input.html" import input %}
+
+{% macro suggestion_row_section() %}
+ {% call showcase_section() %}
+ {{ showcase_title("Suggestion Row") }}
+ {{ showcase_description("A full-width table row offering one suggested change to the row above it. The comment saved with the change is pre-filled and editable from the start, so accepting is a single click. Used for the algorithm's target suggestions.") }}
+
+ {{ showcase_subtitle("Fill in on accept (edit forms)") }}
+
+ Pass apply - a map of input id to value - and comment_target. Accept writes them into the
+ row above and fires input on each; the page's own Save keeps them. Undo puts back whatever has not been
+ typed over since. A suggestions-accept-all window event accepts every such row on the page.
+
+ {% call showcase_example('{% from "backoffice/components/suggestion_row.html" import suggestion_row %}
+
+ {{ suggestion_row(_("Suggested minimum: 3"),
+ colspan=3,
+ comment=_("Accepted algorithm suggestion — minimum 5 → 3"),
+ comment_id="accept-comment-1",
+ apply={"row-1-min": 3},
+ comment_target="row-1-comment") }}', transparent=true) %}
+
+
+
+ Value
+ Min
+ Comment
+
+
+
+
+ Male
+ {{ input(name="", id="showcase-suggestion-min", type="number", value=5, classes="w-20", aria_label="Minimum for Male") }}
+ {{ input(name="", id="showcase-suggestion-comment", aria_label="Comment on Male") }}
+
+ {{ suggestion_row("Suggested minimum: 3",
+ colspan=3,
+ comment="Accepted algorithm suggestion — minimum 5 → 3",
+ comment_id="showcase-suggestion-apply",
+ apply={"showcase-suggestion-min": 3},
+ comment_target="showcase-suggestion-comment") }}
+
+
+ {% endcall %}
+
+ {{ showcase_subtitle("Save on accept (read-only views)") }}
+
+ Pass action and hidden_fields. The row is a form: Accept posts the comment, the csrf
+ token and the hidden fields. (Submitting is switched off here.)
+
+ {% call showcase_example('{{ suggestion_row(_("Suggested maximum: 6"),
+ colspan=3,
+ comment=_("Accepted algorithm suggestion — maximum 4 → 6"),
+ comment_id="accept-comment-2",
+ action=url_for("targets.accept_suggestions", assembly_id=assembly_id),
+ hidden_fields={"scope": "one", "field": "max", ...}) }}', transparent=true) %}
+
+
+ {{ suggestion_row("Suggested maximum: 6",
+ colspan=3,
+ comment="Accepted algorithm suggestion — maximum 4 → 6",
+ comment_id="showcase-suggestion-action",
+ action="#",
+ form_attrs="x-data @submit.prevent",
+ hidden_fields={"field": "max"}) }}
+
+
+ {% endcall %}
+
+ {{ showcase_subtitle("Message only") }}
+
+ With neither, the suggestion is shown with nothing to accept - for someone who may not make the change.
+
+ {% call showcase_example('{{ suggestion_row(_("Suggested minimum: 3"), colspan=3) }}', transparent=true) %}
+
+ {{ suggestion_row("Suggested minimum: 3", colspan=3) }}
+
+ {% endcall %}
+
+ {{ showcase_tokens(["--color-warning-100", "--color-warning-400", "--color-warning-600", "--color-secondary-text"]) }}
+{% endcall %}
+{% endmacro %}
diff --git a/backend/templates/backoffice/targets/bulk_edit_form.html b/backend/templates/backoffice/targets/bulk_edit_form.html
index 2202e6dac..ef5883346 100644
--- a/backend/templates/backoffice/targets/bulk_edit_form.html
+++ b/backend/templates/backoffice/targets/bulk_edit_form.html
@@ -7,6 +7,7 @@
{% from "backoffice/components/info_icon.html" import info_icon %}
{% from "backoffice/components/input.html" import input %}
{% from "backoffice/targets/missing_values.html" import missing_values_summary %}
+{% from "backoffice/targets/value_suggestions.html" import value_suggestion_rows %}
{# ────────────────────────────────────────────────────────────────────────
The row controls come from the Figma at node 4908:15647. They are sized and
coloured by `.btn-icon svg`, so they are drawn with no class of their own and
@@ -157,7 +158,7 @@
{% endmacro %}
-{% macro category_block(cat_id, category=None, missing_values=None, errors={}, respondent_counts=None, selected_counts=None, first_row_id="") %}
+{% macro category_block(cat_id, category=None, missing_values=None, errors={}, respondent_counts=None, selected_counts=None, first_row_id="", value_annotations={}) %}
{% set cat_prefix = "cat[" ~ cat_id ~ "]" %}
{# Both count columns are only worth a column when there is something to
count, so a category with no matching respondent data shows neither. #}
@@ -283,6 +284,11 @@
{% for val in (category.values if category else []) or [] %}
{{ value_row(cat_id, val.value_id, val=val, errors=errors, respondent_counts=respondent_counts, selected_counts=selected_counts, show_counts=show_counts, show_selected=show_selected) }}
+ {# The check's suggestions, right under the row they would change.
+ Accept writes into that row's inputs; Save keeps it. #}
+ {{ value_suggestion_rows(category, val, value_annotations.get(val.value, []), column_count,
+ can_manage=true,
+ field_prefix="bulk-" ~ cat_id ~ "-" ~ val.value_id) }}
{% endfor %}
{# A target with no values is not worth saving, so a block
cloned for a new one arrives with the row its author is
@@ -373,6 +379,11 @@
@targets-changed="markEditDirty()"
@submit="allowLeave()">
+ {# Arriving from a failed selection run is carried through the save, so the
+ page it lands on can still offer to re-run - and "Save & re-run" can. #}
+ {% for name, value in (selection_provenance|default({})).items() %}
+
+ {% endfor %}
{% for category in form_categories %}
{% set respondent_counts = all_respondent_counts.get(category.name) if all_respondent_counts is defined and all_respondent_counts else None %}
@@ -386,8 +397,9 @@
{% endif %}
{% endfor %}
{% endif %}
+ {% set value_annotations = check_result.annotations.get(category.name, {}) if check_result is defined and check_result else {} %}
{{ category_block(category.id, category=category, missing_values=missing_values, errors=errors,
- respondent_counts=respondent_counts, selected_counts=selected_counts) }}
+ respondent_counts=respondent_counts, selected_counts=selected_counts, value_annotations=value_annotations) }}
{% endfor %}
{# Blank category blocks are cloned from here by the "Add target" dialog;
diff --git a/backend/templates/backoffice/targets/category_block.html b/backend/templates/backoffice/targets/category_block.html
index 4aab71063..e3fc0f6ec 100644
--- a/backend/templates/backoffice/targets/category_block.html
+++ b/backend/templates/backoffice/targets/category_block.html
@@ -4,6 +4,7 @@
#}
{% from "backoffice/components/icons.html" import icon_hand, icon_sparkles %}
{% from "backoffice/components/info_icon.html" import info_icon %}
+{% from "backoffice/targets/value_suggestions.html" import value_suggestion_rows %}
{% from "backoffice/targets/missing_values.html" import missing_values_summary %}
{% macro provenance_icon(manual) %}
{% if manual %}
@@ -88,12 +89,12 @@ {{ category.na
{% elif ann.level == "suggestion" %}
+ style="background-color: var(--color-warning-100);
+ border: 1px solid var(--color-warning-400)">
{{ _("Suggestion") }}
- {{ ann.message }}
+ {{ ann.message }}
{% endif %}
{% endfor %}
@@ -172,19 +173,15 @@ {{ category.na
{% if val.comment %}{{ val.comment | linkify }}{% endif %}
- {# ── Value Annotations Row ── #}
- {% if value_annotations is defined and value_annotations.get(val.value) %}
+ {# ── Value Annotations Rows ── #}
+ {% set val_annotations = value_annotations.get(val.value, []) if value_annotations is defined and value_annotations else [] %}
+ {% set notes = val_annotations | rejectattr("level", "equalto", "suggestion") | list %}
+ {% if notes %}
- {% for ann in value_annotations[val.value] %}
+ {% for ann in notes %}
{% if ann.level == "error" %}
{{ ann.message }}
- {% elif ann.level == "suggestion" %}
-
- {{ _("Suggestion") }} {{ ann.message }}
-
{% elif ann.level == "warning" %}
{{ category.na
{% endif %}
+ {{ value_suggestion_rows(category, val, val_annotations, col_count,
+ can_manage=can_manage,
+ assembly_id=assembly_id,
+ selection_provenance=selection_provenance|default({})) }}
{% endfor %}
{% if not category.values %}
diff --git a/backend/templates/backoffice/targets/value_suggestions.html b/backend/templates/backoffice/targets/value_suggestions.html
new file mode 100644
index 000000000..d726f5f2e
--- /dev/null
+++ b/backend/templates/backoffice/targets/value_suggestions.html
@@ -0,0 +1,52 @@
+{#
+ABOUTME: The algorithm's min/max suggestions for one target value, as suggestion rows
+ABOUTME: Shared by the read-only view (accept saves) and the edit form (accept fills the row in)
+#}
+{% from "backoffice/components/suggestion_row.html" import suggestion_row %}
+{#
+ One suggestion_row per suggestion the check made for this value.
+
+ In the read-only view (`field_prefix` empty) Accept posts straight to
+ accept_suggestions, which re-derives the suggestion and saves it. In the
+ edit form `field_prefix` is the id stem of the value's inputs, and Accept
+ writes the number and the comment into them, to be saved with everything
+ else.
+
+ Someone who may not change the targets sees the suggestion and no Accept.
+#}
+{% macro value_suggestion_rows(category, val, annotations, colspan, can_manage=false, assembly_id="", field_prefix="", selection_provenance={}) %}
+ {% for ann in annotations if ann.level == "suggestion" %}
+ {% set acceptable = can_manage and ann.suggested_value is not none and ann.field in ("min", "max") %}
+ {% set current_value = val.min if ann.field == "min" else val.max %}
+ {% if ann.field == "min" %}
+ {% set default_comment = _("Accepted algorithm suggestion — minimum %(current)s → %(suggested)s", current=current_value, suggested=ann.suggested_value) %}
+ {% else %}
+ {% set default_comment = _("Accepted algorithm suggestion — maximum %(current)s → %(suggested)s", current=current_value, suggested=ann.suggested_value) %}
+ {% endif %}
+ {% set comment_id = "accept-comment-" ~ val.value_id ~ "-" ~ ann.field ~ ("-edit" if field_prefix else "") %}
+ {% if not acceptable %}
+ {{ suggestion_row(ann.message, colspan=colspan) }}
+ {% elif field_prefix %}
+ {{ suggestion_row(ann.message,
+ colspan=colspan,
+ comment=default_comment,
+ comment_id=comment_id,
+ apply={field_prefix ~ "-" ~ ann.field: ann.suggested_value},
+ comment_target=field_prefix ~ "-comment") }}
+ {% else %}
+ {{ suggestion_row(ann.message,
+ colspan=colspan,
+ comment=default_comment,
+ comment_id=comment_id,
+ action=url_for('targets.accept_suggestions', assembly_id=assembly_id),
+ form_attrs="x-data x-preserve-scroll-on-submit",
+ hidden_fields=dict({
+ "scope": "one",
+ "category_id": category.id,
+ "value_id": val.value_id,
+ "field": ann.field,
+ "then": "recheck",
+ }, **selection_provenance)) }}
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
diff --git a/backend/tests/component/test_target_suggestions.py b/backend/tests/component/test_target_suggestions.py
new file mode 100644
index 000000000..293fd5fd5
--- /dev/null
+++ b/backend/tests/component/test_target_suggestions.py
@@ -0,0 +1,577 @@
+# ABOUTME: Component tests for accepting the algorithm's recommended target changes (sc-281)
+# ABOUTME: Covers the accept-suggestions route (one/all, comment, re-run gating) and the failure-view link
+
+import uuid
+from datetime import UTC, datetime, timedelta
+from unittest.mock import patch
+
+import pytest
+
+from opendlp.adapters import database
+from opendlp.domain.assembly import SelectionRunRecord
+from opendlp.domain.value_objects import SelectionRunStatus, SelectionTaskType
+from opendlp.service_layer.assembly_service import create_assembly, update_csv_config
+from opendlp.service_layer.target_checking import DetailedCheckResult, TargetAnnotation
+from opendlp.service_layer.target_csv_import import import_targets_from_csv
+from opendlp.service_layer.target_service import get_targets_for_assembly
+from tests.fakes import FakeUnitOfWork
+
+CHECK = "opendlp.entrypoints.blueprints.targets.check_targets_detailed"
+START = "opendlp.entrypoints.blueprints.targets.start_db_select_task"
+
+# Gender Male starts manual [5, 7]; the stubbed check suggests lowering the minimum to 3.
+TARGETS_CSV = "feature,value,min,max\nGender,Male,5,7\nGender,Female,2,4\n"
+
+
+@pytest.fixture(autouse=True)
+def _mapped_domain_objects():
+ """update_target_value writes via SQLAlchemy flag_modified, which needs mapped classes."""
+ database.start_mappers()
+
+
+@pytest.fixture
+def assembly_with_targets(fake_store, admin_user):
+ with FakeUnitOfWork(store=fake_store) as uow:
+ assembly = create_assembly(
+ uow=uow,
+ title="Targets Assembly",
+ created_by_user_id=admin_user.id,
+ question="What should we select?",
+ first_assembly_date=(datetime.now(UTC).date() + timedelta(days=30)),
+ number_to_select=10,
+ )
+ assembly_id = assembly.id
+ with FakeUnitOfWork(store=fake_store) as uow:
+ update_csv_config(uow=uow, user_id=admin_user.id, assembly_id=assembly_id, settings_confirmed=True)
+ with FakeUnitOfWork(store=fake_store) as uow:
+ import_targets_from_csv(uow=uow, user_id=admin_user.id, assembly_id=assembly_id, csv_content=TARGETS_CSV)
+ return assembly_id
+
+
+def _ids(fake_store, admin_user, assembly_id, cat_name, val_name):
+ with FakeUnitOfWork(store=fake_store) as uow:
+ categories = get_targets_for_assembly(uow, admin_user.id, assembly_id)
+ category = next(c for c in categories if c.name == cat_name)
+ value = next(v for v in category.values if v.value == val_name)
+ return category.id, value
+
+
+def _value(fake_store, admin_user, assembly_id, cat_name, val_name):
+ _cid, value = _ids(fake_store, admin_user, assembly_id, cat_name, val_name)
+ return value
+
+
+def _min_suggestion(suggested):
+ return TargetAnnotation(
+ level="suggestion",
+ field="min",
+ message=f"Suggested minimum: {suggested}",
+ suggested_value=suggested,
+ )
+
+
+def _accept_url(assembly_id):
+ return f"/backoffice/assembly/{assembly_id}/targets/accept-suggestions"
+
+
+class TestAcceptOne:
+ def test_applies_the_suggested_value_and_the_edited_comment(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ assembly_id = assembly_with_targets
+ category_id, value = _ids(fake_store, admin_user, assembly_id, "Gender", "Male")
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+
+ with patch(CHECK, return_value=check):
+ response = logged_in_admin.post(
+ _accept_url(assembly_id),
+ data={
+ "scope": "one",
+ "category_id": str(category_id),
+ "value_id": str(value.value_id),
+ "field": "min",
+ "comment": "Accepted algorithm suggestion — minimum 5 → 3 — checked with team",
+ "then": "recheck",
+ "from": "selection",
+ "run_id": str(uuid.uuid4()),
+ },
+ follow_redirects=False,
+ )
+
+ assert response.status_code == 302
+ assert "/targets/check" in response.headers["Location"]
+ assert "from=selection" in response.headers["Location"]
+
+ applied = _value(fake_store, admin_user, assembly_id, "Gender", "Male")
+ assert applied.min == 3
+ assert applied.max == 7
+ assert applied.minmax_manual is True
+ assert applied.comment == "Accepted algorithm suggestion — minimum 5 → 3 — checked with team"
+
+ def test_falls_back_to_an_auto_comment_when_none_is_supplied(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ assembly_id = assembly_with_targets
+ category_id, value = _ids(fake_store, admin_user, assembly_id, "Gender", "Male")
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+
+ with patch(CHECK, return_value=check):
+ logged_in_admin.post(
+ _accept_url(assembly_id),
+ data={
+ "scope": "one",
+ "category_id": str(category_id),
+ "value_id": str(value.value_id),
+ "field": "min",
+ "comment": "",
+ "then": "recheck",
+ },
+ follow_redirects=False,
+ )
+
+ applied = _value(fake_store, admin_user, assembly_id, "Gender", "Male")
+ assert applied.min == 3
+ assert "Accepted algorithm suggestion" in applied.comment
+
+ def test_a_suggestion_the_check_no_longer_offers_changes_nothing(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ assembly_id = assembly_with_targets
+ category_id, value = _ids(fake_store, admin_user, assembly_id, "Gender", "Male")
+ empty = DetailedCheckResult(success=True, global_errors=[], annotations={})
+
+ with patch(CHECK, return_value=empty):
+ response = logged_in_admin.post(
+ _accept_url(assembly_id),
+ data={
+ "scope": "one",
+ "category_id": str(category_id),
+ "value_id": str(value.value_id),
+ "field": "min",
+ "then": "recheck",
+ },
+ follow_redirects=False,
+ )
+
+ assert response.status_code == 302
+ with logged_in_admin.session_transaction() as session:
+ flashes = [msg[1] for msg in session.get("_flashes", [])]
+ assert any("no longer available" in msg for msg in flashes)
+
+ unchanged = _value(fake_store, admin_user, assembly_id, "Gender", "Male")
+ assert unchanged.min == 5
+
+
+class TestAcceptAll:
+ def test_applies_every_outstanding_suggestion(self, logged_in_admin, admin_user, fake_store, assembly_with_targets):
+ assembly_id = assembly_with_targets
+ check = DetailedCheckResult(
+ success=False,
+ global_errors=[],
+ annotations={
+ "Gender": {
+ "Male": [_min_suggestion(3)],
+ "Female": [
+ TargetAnnotation(
+ level="suggestion", field="max", message="Suggested maximum: 6", suggested_value=6
+ )
+ ],
+ }
+ },
+ )
+
+ with patch(CHECK, return_value=check):
+ response = logged_in_admin.post(
+ _accept_url(assembly_id),
+ data={"scope": "all", "then": "recheck"},
+ follow_redirects=False,
+ )
+
+ assert response.status_code == 302
+ male = _value(fake_store, admin_user, assembly_id, "Gender", "Male")
+ female = _value(fake_store, admin_user, assembly_id, "Gender", "Female")
+ assert male.min == 3
+ assert female.max == 6
+ assert "Accepted algorithm suggestion" in male.comment
+ assert "Accepted algorithm suggestion" in female.comment
+
+
+class TestReRunGating:
+ def test_accept_all_and_rerun_starts_a_selection_when_from_a_run(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ assembly_id = assembly_with_targets
+ task_id = uuid.uuid4()
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+
+ with patch(CHECK, return_value=check), patch(START, return_value=task_id) as start:
+ response = logged_in_admin.post(
+ _accept_url(assembly_id),
+ data={"scope": "all", "then": "run", "from": "selection", "run_id": str(uuid.uuid4())},
+ follow_redirects=False,
+ )
+
+ assert start.called
+ assert response.status_code == 302
+ assert f"current_selection={task_id}" in response.headers["Location"]
+
+ def test_accept_all_and_rerun_starts_a_real_selection_for_an_initial_run(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+
+ with patch(CHECK, return_value=check), patch(START, return_value=uuid.uuid4()) as start:
+ logged_in_admin.post(
+ _accept_url(assembly_with_targets),
+ data={"scope": "all", "then": "run", "from": "selection", "run_id": str(uuid.uuid4())},
+ )
+
+ assert start.call_args.kwargs["test_selection"] is False
+
+ def test_a_failed_test_selection_is_rerun_as_a_test_selection(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+
+ with patch(CHECK, return_value=check), patch(START, return_value=uuid.uuid4()) as start:
+ logged_in_admin.post(
+ _accept_url(assembly_with_targets),
+ data={"scope": "all", "then": "run", "from": "selection", "run_id": str(uuid.uuid4()), "test": "1"},
+ )
+
+ assert start.call_args.kwargs["test_selection"] is True
+
+ def test_recheck_after_accepting_keeps_the_test_flag(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+
+ with patch(CHECK, return_value=check):
+ response = logged_in_admin.post(
+ _accept_url(assembly_with_targets),
+ data={"scope": "all", "then": "recheck", "from": "selection", "run_id": "r", "test": "1"},
+ )
+
+ assert "test=1" in response.headers["Location"]
+
+ def test_run_is_ignored_without_the_selection_provenance(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ assembly_id = assembly_with_targets
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+
+ with patch(CHECK, return_value=check), patch(START) as start:
+ response = logged_in_admin.post(
+ _accept_url(assembly_id),
+ data={"scope": "all", "then": "run"}, # no from=selection
+ follow_redirects=False,
+ )
+
+ assert not start.called
+ assert "/targets/check" in response.headers["Location"]
+
+
+def _flashes(client):
+ with client.session_transaction() as session:
+ return [msg[1] for msg in session.get("_flashes", [])]
+
+
+class TestAcceptFailures:
+ """What accept-suggestions does when the form or the assembly is not what it expects."""
+
+ @pytest.mark.parametrize(
+ "form",
+ [
+ {"category_id": "not-a-uuid", "value_id": "also-not"},
+ {"category_id": str(uuid.uuid4()), "value_id": str(uuid.uuid4())},
+ ],
+ ids=["malformed ids", "unknown target"],
+ )
+ def test_a_target_that_cannot_be_found_changes_nothing(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets, form
+ ):
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+
+ with patch(CHECK, return_value=check):
+ response = logged_in_admin.post(
+ _accept_url(assembly_with_targets),
+ data={"scope": "one", "field": "min", "then": "recheck", **form},
+ )
+
+ assert "/targets/check" in response.headers["Location"]
+ assert any("no longer available" in msg for msg in _flashes(logged_in_admin))
+ assert _value(fake_store, admin_user, assembly_with_targets, "Gender", "Male").min == 5
+
+ def test_rerun_waits_for_unconfirmed_selection_settings(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ with FakeUnitOfWork(store=fake_store) as uow:
+ update_csv_config(
+ uow=uow, user_id=admin_user.id, assembly_id=assembly_with_targets, settings_confirmed=False
+ )
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+
+ with patch(CHECK, return_value=check), patch(START) as start:
+ response = logged_in_admin.post(
+ _accept_url(assembly_with_targets),
+ data={"scope": "all", "then": "run", "from": "selection", "run_id": "r"},
+ )
+
+ assert not start.called
+ assert "/targets/check" in response.headers["Location"]
+ assert any("review and save the selection settings" in msg for msg in _flashes(logged_in_admin))
+ # The accepted change is still saved; only the re-run waits.
+ assert _value(fake_store, admin_user, assembly_with_targets, "Gender", "Male").min == 3
+
+ def test_an_unknown_assembly_goes_back_to_the_dashboard(self, logged_in_admin):
+ response = logged_in_admin.post(_accept_url(uuid.uuid4()), data={"scope": "all", "then": "recheck"})
+
+ assert response.headers["Location"].endswith("/dashboard")
+ assert "Assembly not found" in _flashes(logged_in_admin)
+
+ def test_someone_who_may_not_manage_the_assembly_is_turned_away(
+ self, logged_in_user, admin_user, fake_store, assembly_with_targets
+ ):
+ with patch(CHECK) as check:
+ response = logged_in_user.post(_accept_url(assembly_with_targets), data={"scope": "all", "then": "recheck"})
+
+ assert not check.called
+ assert response.headers["Location"].endswith("/dashboard")
+ assert any("permission" in msg for msg in _flashes(logged_in_user))
+ assert _value(fake_store, admin_user, assembly_with_targets, "Gender", "Male").min == 5
+
+ def test_an_unexpected_error_goes_back_to_the_targets_page(self, logged_in_admin, assembly_with_targets):
+ with patch(CHECK, side_effect=RuntimeError("boom")):
+ response = logged_in_admin.post(
+ _accept_url(assembly_with_targets), data={"scope": "all", "then": "recheck"}
+ )
+
+ assert response.headers["Location"].endswith(f"/backoffice/assembly/{assembly_with_targets}/targets")
+ assert any("unexpected error" in msg for msg in _flashes(logged_in_admin))
+
+
+class TestCheckPageControls:
+ def _check_url(self, assembly_id, query=""):
+ return f"/backoffice/assembly/{assembly_id}/targets/check{query}"
+
+ def test_renders_inline_accept_editor_and_rerun_when_from_a_run(
+ self, logged_in_admin, fake_store, assembly_with_targets
+ ):
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+ with patch(CHECK, return_value=check):
+ response = logged_in_admin.get(
+ self._check_url(assembly_with_targets, f"?from=selection&run_id={uuid.uuid4()}")
+ )
+
+ assert response.status_code == 200
+ assert b"accept-comment-" in response.data # inline, editable comment field
+ assert b"Accept all suggestions" in response.data
+ assert b"re-run selection" in response.data
+
+ def test_direct_visit_omits_the_rerun_button(self, logged_in_admin, fake_store, assembly_with_targets):
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+ with patch(CHECK, return_value=check):
+ response = logged_in_admin.get(self._check_url(assembly_with_targets))
+
+ assert response.status_code == 200
+ assert b"Accept all suggestions" in response.data
+ assert b"re-run selection" not in response.data
+
+
+class TestEditModeEntry:
+ """ "Adjust targets" opens the editor, each suggestion under the row it would change."""
+
+ def _check_url(self, assembly_id, query=""):
+ return f"/backoffice/assembly/{assembly_id}/targets/check{query}"
+
+ def _get(self, client, assembly_id, query):
+ check = DetailedCheckResult(
+ success=False, global_errors=[], annotations={"Gender": {"Male": [_min_suggestion(3)]}}
+ )
+ with patch(CHECK, return_value=check):
+ return client.get(self._check_url(assembly_id, query))
+
+ def test_edit_flag_opens_the_editor_with_suggestions_filling_the_row(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ category_id, male = _ids(fake_store, admin_user, assembly_with_targets, "Gender", "Male")
+ response = self._get(logged_in_admin, assembly_with_targets, f"?from=selection&run_id={uuid.uuid4()}&edit=1")
+
+ html = response.data.decode()
+ assert response.status_code == 200
+ assert "editingAll: true" in html
+ # The edit-form suggestion fills the row's own min and comment inputs in.
+ prefix = f"bulk-{category_id}-{male.value_id}"
+ assert f""{prefix}-min": 3" in html or f'"{prefix}-min": 3' in html
+ assert f'data-comment-target="{prefix}-comment"' in html
+ assert "Save & re-run selection" in html
+
+ def test_without_the_edit_flag_the_page_opens_read_only(self, logged_in_admin, fake_store, assembly_with_targets):
+ response = self._get(logged_in_admin, assembly_with_targets, f"?from=selection&run_id={uuid.uuid4()}")
+
+ assert "editingAll: false" in response.data.decode()
+
+ def test_a_test_run_is_offered_a_test_rerun_and_carries_the_flag(
+ self, logged_in_admin, fake_store, assembly_with_targets
+ ):
+ response = self._get(logged_in_admin, assembly_with_targets, "?from=selection&run_id=r&test=1&edit=1")
+
+ html = response.data.decode()
+ assert "Save & re-run test selection" in html
+ assert "Accept all & re-run test selection" in html
+ assert ' ' in html
+
+ def test_save_and_rerun_is_only_offered_from_a_run(self, logged_in_admin, fake_store, assembly_with_targets):
+ response = self._get(logged_in_admin, assembly_with_targets, "?edit=1")
+
+ assert b"Save & re-run selection" not in response.data
+
+
+class TestSaveAllFromSelection:
+ def _post(self, client, fake_store, admin_user, assembly_id, extra):
+ category_id, male = _ids(fake_store, admin_user, assembly_id, "Gender", "Male")
+ prefix = f"cat[{category_id}]"
+ data = {
+ f"{prefix}[name]": "Gender",
+ f"{prefix}[values][{male.value_id}][value]": "Male",
+ f"{prefix}[values][{male.value_id}][min]": "3",
+ f"{prefix}[values][{male.value_id}][max]": "7",
+ f"{prefix}[values][{male.value_id}][comment]": "Accepted algorithm suggestion — minimum 5 → 3",
+ **extra,
+ }
+ return client.post(f"/backoffice/assembly/{assembly_id}/targets/save-all", data=data)
+
+ def test_save_and_rerun_saves_then_starts_a_selection(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ task_id = uuid.uuid4()
+ with patch(START, return_value=task_id) as start:
+ response = self._post(
+ logged_in_admin,
+ fake_store,
+ admin_user,
+ assembly_with_targets,
+ {"from": "selection", "run_id": str(uuid.uuid4()), "then": "run"},
+ )
+
+ assert start.called
+ assert f"current_selection={task_id}" in response.headers["Location"]
+ assert _value(fake_store, admin_user, assembly_with_targets, "Gender", "Male").min == 3
+
+ def test_save_and_rerun_after_a_test_run_starts_a_test_selection(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ with patch(START, return_value=uuid.uuid4()) as start:
+ self._post(
+ logged_in_admin,
+ fake_store,
+ admin_user,
+ assembly_with_targets,
+ {"from": "selection", "run_id": str(uuid.uuid4()), "then": "run", "test": "1"},
+ )
+
+ assert start.call_args.kwargs["test_selection"] is True
+
+ def test_plain_save_from_a_run_keeps_the_provenance(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ run_id = str(uuid.uuid4())
+ with patch(START) as start:
+ response = self._post(
+ logged_in_admin, fake_store, admin_user, assembly_with_targets, {"from": "selection", "run_id": run_id}
+ )
+
+ assert not start.called
+ location = response.headers["Location"]
+ assert "/targets/check" in location
+ assert "from=selection" in location
+ assert run_id in location
+
+ def test_run_is_ignored_without_the_selection_provenance(
+ self, logged_in_admin, admin_user, fake_store, assembly_with_targets
+ ):
+ with patch(START) as start:
+ response = self._post(logged_in_admin, fake_store, admin_user, assembly_with_targets, {"then": "run"})
+
+ assert not start.called
+ assert "/targets/check" in response.headers["Location"]
+
+
+def _add_failed_run(fake_store, assembly_id, task_type):
+ with FakeUnitOfWork(store=fake_store) as uow:
+ uow.selection_run_records.add(
+ SelectionRunRecord(
+ assembly_id=assembly_id,
+ task_id=uuid.uuid4(),
+ status=SelectionRunStatus.FAILED,
+ task_type=task_type,
+ log_messages=["Task failed"],
+ error_message="The targets cannot all be met from the pool.",
+ completed_at=datetime.now(UTC),
+ )
+ )
+ uow.commit()
+ return uow.selection_run_records.get_by_assembly_id(assembly_id)[-1].task_id
+
+
+class TestFailureViewLink:
+ def _modal(self, client, assembly_id, run_id):
+ return client.get(f"/backoffice/assembly/{assembly_id}/selection/db/modal-progress/{run_id}")
+
+ def test_failed_initial_selection_offers_adjust_targets(self, logged_in_admin, fake_store, assembly_with_targets):
+ run_id = _add_failed_run(fake_store, assembly_with_targets, SelectionTaskType.SELECT_FROM_DB)
+ response = self._modal(logged_in_admin, assembly_with_targets, run_id)
+
+ assert response.status_code == 200
+ assert b"Adjust targets" in response.data
+ assert b"from=selection" in response.data
+ assert b"edit=1" in response.data
+
+ def test_failed_test_selection_offers_adjust_targets_marked_as_a_test(
+ self, logged_in_admin, fake_store, assembly_with_targets
+ ):
+ run_id = _add_failed_run(fake_store, assembly_with_targets, SelectionTaskType.TEST_SELECT_FROM_DB)
+ response = self._modal(logged_in_admin, assembly_with_targets, run_id)
+
+ assert response.status_code == 200
+ assert b"Adjust targets" in response.data
+ assert b"test=1" in response.data
+
+ def test_failed_initial_selection_link_is_not_marked_as_a_test(
+ self, logged_in_admin, fake_store, assembly_with_targets
+ ):
+ run_id = _add_failed_run(fake_store, assembly_with_targets, SelectionTaskType.SELECT_FROM_DB)
+ response = self._modal(logged_in_admin, assembly_with_targets, run_id)
+
+ assert b"test=1" not in response.data
+
+ def test_failed_replacement_run_has_no_adjust_targets_link(
+ self, logged_in_admin, fake_store, assembly_with_targets
+ ):
+ run_id = _add_failed_run(fake_store, assembly_with_targets, SelectionTaskType.SELECT_REPLACEMENT_FROM_DB)
+ response = self._modal(logged_in_admin, assembly_with_targets, run_id)
+
+ assert response.status_code == 200
+ assert b"Adjust targets" not in response.data
diff --git a/backend/translations/hu/LC_MESSAGES/messages.po b/backend/translations/hu/LC_MESSAGES/messages.po
index ef82ac506..92a743a40 100644
--- a/backend/translations/hu/LC_MESSAGES/messages.po
+++ b/backend/translations/hu/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-10-19 16:31+0100\n"
+"POT-Creation-Date: 2026-10-20 12:53+0200\n"
"PO-Revision-Date: 2026-09-16 14:15+0000\n"
"Last-Translator: Hamish Downer \n"
"Language: hu\n"
@@ -73,8 +73,8 @@ msgstr "Feldolgozás…"
#: templates/backoffice/assembly_respondents.html:71
#: templates/backoffice/components/assembly_tabs.html:58
#: templates/backoffice/respondents/export_modal.html:64
-#: templates/backoffice/targets/bulk_edit_form.html:259
-#: templates/backoffice/targets/category_block.html:127
+#: templates/backoffice/targets/bulk_edit_form.html:260
+#: templates/backoffice/targets/category_block.html:128
#: templates/db_selection/select.html:43
#: templates/main/view_assembly_base.html:42
#: templates/respondents/view_respondents.html:2
@@ -559,8 +559,8 @@ msgstr "Jelentkezők"
#: templates/backoffice/assembly_dashboard.html:114
#: templates/backoffice/assembly_view_respondent.html:226
#: templates/backoffice/components/table.html:209
-#: templates/backoffice/targets/bulk_edit_form.html:268
-#: templates/backoffice/targets/category_block.html:136
+#: templates/backoffice/targets/bulk_edit_form.html:269
+#: templates/backoffice/targets/category_block.html:137
#: templates/respondents/view_respondents.html:132
msgid "Selected"
msgstr "Kiválasztva"
@@ -1115,7 +1115,7 @@ msgid "Select a CSV file containing targets"
msgstr "Válassz ki egy kiválasztási szempontokat tartalmazó CSV fájlt"
#: src/opendlp/entrypoints/forms.py:655 src/opendlp/entrypoints/forms.py:665
-#: templates/backoffice/targets/bulk_edit_form.html:213
+#: templates/backoffice/targets/bulk_edit_form.html:214
msgid "Target Name"
msgstr "Kiválasztási szempont neve"
@@ -1148,8 +1148,8 @@ msgstr "Nem kötelező – link arra, hogy honnan származnak a százalékos ar
#: templates/backoffice/components/db_replacement_modal.html:129
#: templates/backoffice/components/db_selection_progress_modal.html:84
#: templates/backoffice/components/db_selection_progress_modal.html:92
-#: templates/backoffice/targets/bulk_edit_form.html:252
-#: templates/backoffice/targets/category_block.html:120
+#: templates/backoffice/targets/bulk_edit_form.html:253
+#: templates/backoffice/targets/category_block.html:121
#: templates/targets/components/category_block.html:69
#: templates/targets/components/category_block.html:136
#: templates/targets/components/category_block.html:263
@@ -1171,8 +1171,8 @@ msgstr "Nem kötelező – a minimum és a maximum ebből adódik"
#: src/opendlp/entrypoints/forms.py:699
#: src/opendlp/service_layer/selection_report.py:221
#: templates/backoffice/components/db_selection_progress_modal.html:93
-#: templates/backoffice/targets/bulk_edit_form.html:274
-#: templates/backoffice/targets/category_block.html:142
+#: templates/backoffice/targets/bulk_edit_form.html:275
+#: templates/backoffice/targets/category_block.html:143
#: templates/targets/components/category_block.html:70
#: templates/targets/components/category_block.html:147
#: templates/targets/components/category_block.html:275
@@ -1182,8 +1182,8 @@ msgstr "Min."
#: src/opendlp/entrypoints/forms.py:704
#: src/opendlp/service_layer/selection_report.py:222
#: templates/backoffice/components/db_selection_progress_modal.html:94
-#: templates/backoffice/targets/bulk_edit_form.html:276
-#: templates/backoffice/targets/category_block.html:144
+#: templates/backoffice/targets/bulk_edit_form.html:277
+#: templates/backoffice/targets/category_block.html:145
#: templates/targets/components/category_block.html:71
#: templates/targets/components/category_block.html:158
#: templates/targets/components/category_block.html:286
@@ -1713,8 +1713,8 @@ msgstr "Hiba történt a közösségi gyűlés létrehozásakor"
#: src/opendlp/entrypoints/blueprints/target_sources.py:667
#: src/opendlp/entrypoints/blueprints/target_sources.py:689
#: src/opendlp/entrypoints/blueprints/target_sources.py:870
-#: src/opendlp/entrypoints/blueprints/targets.py:154
-#: src/opendlp/entrypoints/blueprints/targets.py:405
+#: src/opendlp/entrypoints/blueprints/targets.py:157
+#: src/opendlp/entrypoints/blueprints/targets.py:414
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:115
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:791
msgid "You don't have permission to view this assembly"
@@ -1808,10 +1808,11 @@ msgstr "Nincs jogosultságod a közösségi gyűlés megtekintéséhez"
#: src/opendlp/entrypoints/blueprints/target_sources.py:924
#: src/opendlp/entrypoints/blueprints/target_sources.py:988
#: src/opendlp/entrypoints/blueprints/target_sources.py:1004
-#: src/opendlp/entrypoints/blueprints/targets.py:145
-#: src/opendlp/entrypoints/blueprints/targets.py:219
-#: src/opendlp/entrypoints/blueprints/targets.py:274
-#: src/opendlp/entrypoints/blueprints/targets.py:402
+#: src/opendlp/entrypoints/blueprints/targets.py:148
+#: src/opendlp/entrypoints/blueprints/targets.py:222
+#: src/opendlp/entrypoints/blueprints/targets.py:277
+#: src/opendlp/entrypoints/blueprints/targets.py:411
+#: src/opendlp/entrypoints/blueprints/targets.py:603
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:112
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:182
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:788
@@ -2266,6 +2267,7 @@ msgstr "Váratlan hiba történt az adatok ellenőrzése közben"
#: src/opendlp/entrypoints/blueprints/db_selection_backoffice.py:114
#: src/opendlp/entrypoints/blueprints/db_selection_backoffice.py:161
+#: src/opendlp/entrypoints/blueprints/targets.py:534
msgid "Please review and save the selection settings before running selection"
msgstr ""
"A kiválasztás futtatása előtt nézd át és mentsd el a kiválasztási "
@@ -2298,6 +2300,7 @@ msgstr "A kiválasztás futtatásának indítása sikertelen: %(error)s"
#: src/opendlp/entrypoints/blueprints/gsheets_legacy.py:945
#: src/opendlp/entrypoints/blueprints/gsheets_legacy.py:1252
#: src/opendlp/entrypoints/blueprints/gsheets_legacy.py:1295
+#: src/opendlp/entrypoints/blueprints/targets.py:606
msgid "You don't have permission to manage this assembly"
msgstr "Nincs jogosultságod a közösségi gyűlés kezeléséhez"
@@ -3004,7 +3007,7 @@ msgstr ""
#: src/opendlp/entrypoints/blueprints/respondents.py:200
#: src/opendlp/entrypoints/blueprints/respondents_legacy.py:176
-#: src/opendlp/entrypoints/blueprints/targets.py:225
+#: src/opendlp/entrypoints/blueprints/targets.py:228
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:188
msgid "Could not read CSV file. Please ensure it is UTF-8 encoded."
msgstr "Nem sikerült beolvasni a CSV fájlt. Győződj meg róla, hogy UTF-8 kódolású."
@@ -3170,7 +3173,7 @@ msgid "%(count)s rows were skipped: %(errors)s"
msgstr "%(count)s sor kihagyva: %(errors)s"
#: src/opendlp/entrypoints/blueprints/respondents_legacy.py:167
-#: src/opendlp/entrypoints/blueprints/targets.py:216
+#: src/opendlp/entrypoints/blueprints/targets.py:219
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:179
#, python-format
msgid "CSV import failed: %(error)s"
@@ -3181,7 +3184,7 @@ msgid "You don't have permission to import respondents"
msgstr "Nincs jogosultságod jelentkezők importálásához"
#: src/opendlp/entrypoints/blueprints/respondents_legacy.py:180
-#: src/opendlp/entrypoints/blueprints/targets.py:229
+#: src/opendlp/entrypoints/blueprints/targets.py:232
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:192
msgid "An unexpected error occurred during import"
msgstr "Váratlan hiba történt az importálás közben"
@@ -3374,11 +3377,11 @@ msgstr "Számított kérdés törölve"
msgid "Question unlinked from its target"
msgstr "A kérdés leválasztva a kiválasztási szempontról"
-#: src/opendlp/entrypoints/blueprints/targets.py:160
+#: src/opendlp/entrypoints/blueprints/targets.py:163
msgid "An error occurred while loading assembly targets"
msgstr "Hiba történt a közösségi gyűlés célszámainak betöltése közben"
-#: src/opendlp/entrypoints/blueprints/targets.py:202
+#: src/opendlp/entrypoints/blueprints/targets.py:205
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:167
#, python-format
msgid "Successfully imported %(cats)s targets with %(vals)s values from %(file)s"
@@ -3386,64 +3389,96 @@ msgstr ""
"Sikeresen importálva %(cats)s kiválasztási szempont %(vals)s értékkel a "
"következő fájlból: %(file)s"
-#: src/opendlp/entrypoints/blueprints/targets.py:222
+#: src/opendlp/entrypoints/blueprints/targets.py:225
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:185
msgid "You don't have permission to import targets"
msgstr "Nincs jogosultságod célszámok importálásához"
-#: src/opendlp/entrypoints/blueprints/targets.py:247
+#: src/opendlp/entrypoints/blueprints/targets.py:250
#, python-format
msgid "Targets deleted: %(count)d"
msgstr "Kiválasztási szempontok törölve: %(count)d"
-#: src/opendlp/entrypoints/blueprints/targets.py:268
+#: src/opendlp/entrypoints/blueprints/targets.py:271
msgid "You don't have permission to delete targets"
msgstr "Nincs jogosultságod célszámok törléséhez"
-#: src/opendlp/entrypoints/blueprints/targets.py:280
+#: src/opendlp/entrypoints/blueprints/targets.py:283
msgid "An error occurred while deleting targets"
msgstr "Hiba történt a célszámok törlése közben"
-#: src/opendlp/entrypoints/blueprints/targets.py:296
+#: src/opendlp/entrypoints/blueprints/targets.py:299
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:663
msgid "No columns selected"
msgstr "Nincs kiválasztott oszlop"
-#: src/opendlp/entrypoints/blueprints/targets.py:338
+#: src/opendlp/entrypoints/blueprints/targets.py:341
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:731
msgid "No new targets were created"
msgstr "Nem jött létre új kiválasztási szempont"
-#: src/opendlp/entrypoints/blueprints/targets.py:344
+#: src/opendlp/entrypoints/blueprints/targets.py:347
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:718
#, python-format
msgid "Created %(count)s targets with %(values)s values: %(names)s"
msgstr "%(count)s kiválasztási szempont létrehozva %(values)s értékkel: %(names)s"
-#: src/opendlp/entrypoints/blueprints/targets.py:353
+#: src/opendlp/entrypoints/blueprints/targets.py:356
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:727
#, python-format
msgid "Created %(count)s targets: %(names)s"
msgstr "%(count)s kiválasztási szempont létrehozva: %(names)s"
-#: src/opendlp/entrypoints/blueprints/targets.py:409
+#: src/opendlp/entrypoints/blueprints/targets.py:418
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:795
msgid "An unexpected error occurred while checking targets"
msgstr "Váratlan hiba történt a célszámok ellenőrzése közben"
-#: src/opendlp/entrypoints/blueprints/targets.py:433
+#: src/opendlp/entrypoints/blueprints/targets.py:425
+#, python-format
+msgid "minimum %(current)s → %(suggested)s"
+msgstr ""
+
+#: src/opendlp/entrypoints/blueprints/targets.py:426
+#, python-format
+msgid "maximum %(current)s → %(suggested)s"
+msgstr ""
+
+#: src/opendlp/entrypoints/blueprints/targets.py:493
+#, python-format
+msgid "Accepted algorithm suggestion — %(change)s"
+msgstr ""
+
+#: src/opendlp/entrypoints/blueprints/targets.py:517
+#, python-format
+msgid "Accepted algorithm suggestion — %(changes)s"
+msgstr ""
+
+#: src/opendlp/entrypoints/blueprints/targets.py:593
+msgid "That suggestion is no longer available. The targets have been re-checked."
+msgstr ""
+
+#: src/opendlp/entrypoints/blueprints/targets.py:599
+msgid "Recommended changes applied."
+msgstr ""
+
+#: src/opendlp/entrypoints/blueprints/targets.py:610
+msgid "An unexpected error occurred while applying the recommended changes"
+msgstr ""
+
+#: src/opendlp/entrypoints/blueprints/targets.py:634
msgid "Your targets were not saved. Please correct the errors below."
msgstr "A célszámaid nem lettek elmentve. Javítsd az alábbi hibákat."
-#: src/opendlp/entrypoints/blueprints/targets.py:487
+#: src/opendlp/entrypoints/blueprints/targets.py:689
msgid "Please try again"
msgstr "Kérjük, próbáld újra"
-#: src/opendlp/entrypoints/blueprints/targets.py:505
+#: src/opendlp/entrypoints/blueprints/targets.py:712
msgid "Targets saved"
msgstr "A célszámok mentve"
-#: src/opendlp/entrypoints/blueprints/targets.py:519
+#: src/opendlp/entrypoints/blueprints/targets.py:728
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:253
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:335
#: src/opendlp/entrypoints/blueprints/targets_legacy.py:457
@@ -3452,11 +3487,11 @@ msgstr "A célszámok mentve"
msgid "Error: %(error)s"
msgstr "Hiba: %(error)s"
-#: src/opendlp/entrypoints/blueprints/targets.py:522
+#: src/opendlp/entrypoints/blueprints/targets.py:731
msgid "You don't have permission to edit these targets"
msgstr "Nincs jogosultságod ezeknek a célszámoknak a szerkesztéséhez"
-#: src/opendlp/entrypoints/blueprints/targets.py:530
+#: src/opendlp/entrypoints/blueprints/targets.py:739
msgid "An error occurred while saving the targets"
msgstr "Hiba történt a célszámok mentése közben"
@@ -4569,7 +4604,7 @@ msgstr ""
"%(letter)s oszlop %(row)d. sorában null karakter van, amely nem menthető "
"el. Távolítsd el, majd töltsd fel újra a fájlt."
-#: src/opendlp/service_layer/target_checking.py:120
+#: src/opendlp/service_layer/target_checking.py:134
#, python-format
msgid ""
"Sum of max values (%(value)s) is the smallest across all targets — "
@@ -4578,7 +4613,7 @@ msgstr ""
"A maximumok összege (%(value)s) a legkisebb az összes kiválasztási "
"szempont közül – ütközik '%(other)s' kiválasztási szemponttal"
-#: src/opendlp/service_layer/target_checking.py:133
+#: src/opendlp/service_layer/target_checking.py:147
#, python-format
msgid ""
"Sum of min values (%(value)s) is the largest across all targets — "
@@ -4587,38 +4622,38 @@ msgstr ""
"A minimumok összege (%(value)s) a legnagyobb az összes kiválasztási "
"szempont közül – ütközik '%(other)s' kiválasztási szemponttal"
-#: src/opendlp/service_layer/target_checking.py:147
+#: src/opendlp/service_layer/target_checking.py:161
#, python-format
msgid "Sum of minimums (%(sum)s) exceeds number to select (%(limit)s)"
msgstr ""
"A minimumok összege (%(sum)s) meghaladja a kiválasztandók számát "
"(%(limit)s)"
-#: src/opendlp/service_layer/target_checking.py:160
+#: src/opendlp/service_layer/target_checking.py:174
#, python-format
msgid "Sum of maximums (%(sum)s) is less than number to select (%(limit)s)"
msgstr ""
"A maximumok összege (%(sum)s) kevesebb, mint a kiválasztandók száma "
"(%(limit)s)"
-#: src/opendlp/service_layer/target_checking.py:181
+#: src/opendlp/service_layer/target_checking.py:195
#, python-format
msgid "Need minimum %(min)s but only %(count)s respondents match"
msgstr "Legalább %(min)s szükséges, de csak %(count)s jelentkező felel meg"
-#: src/opendlp/service_layer/target_checking.py:208
+#: src/opendlp/service_layer/target_checking.py:222
#: templates/backoffice/components/db_replacement_modal.html:197
#, python-format
msgid "Suggested minimum: %(value)s (currently %(current)s)"
msgstr "Javasolt minimum: %(value)s (jelenleg %(current)s)"
-#: src/opendlp/service_layer/target_checking.py:224
+#: src/opendlp/service_layer/target_checking.py:238
#: templates/backoffice/components/db_replacement_modal.html:199
#, python-format
msgid "Suggested maximum: %(value)s (currently %(current)s)"
msgstr "Javasolt maximum: %(value)s (jelenleg %(current)s)"
-#: src/opendlp/service_layer/target_checking.py:275
+#: src/opendlp/service_layer/target_checking.py:289
#, python-format
msgid "Percentages for this target add up to %(total)s%%, not 100%%"
msgstr ""
@@ -5114,7 +5149,7 @@ msgid "Unknown"
msgstr "Ismeretlen"
#: templates/admin/invite_view.html:105 templates/admin/user_edit.html:107
-#: templates/backoffice/targets/category_block.html:192
+#: templates/backoffice/targets/category_block.html:189
#: templates/db_selection/components/progress.html:91
#: templates/db_selection/select.html:24 templates/db_selection/select.html:54
#: templates/gsheets/components/progress.html:101
@@ -5203,7 +5238,7 @@ msgstr "Lejárat"
#: templates/backoffice/components/db_selection_progress_modal.html:124
#: templates/backoffice/components/registration_page_list.html:38
#: templates/backoffice/respondent_field_schema/_editor.html:51
-#: templates/backoffice/targets/bulk_edit_form.html:280
+#: templates/backoffice/targets/bulk_edit_form.html:281
#: templates/main/view_assembly_data.html:60
#: templates/main/view_assembly_members.html:13
#: templates/targets/components/category_block.html:76
@@ -5306,7 +5341,7 @@ msgid "View details"
msgstr "Részletek megtekintése"
#: templates/admin/user_2fa_audit_log.html:82
-#: templates/backoffice/targets/bulk_edit_form.html:176
+#: templates/backoffice/targets/bulk_edit_form.html:177
#: templates/main/view_assembly_data.html:105
msgid "None"
msgstr "Nincs"
@@ -6078,7 +6113,7 @@ msgstr "Tölts fel CSV fájlokat a célszámok és az emberek adatainak importá
#: templates/backoffice/assembly_data.html:312
#: templates/backoffice/assembly_targets.html:13
-#: templates/backoffice/assembly_targets.html:84
+#: templates/backoffice/assembly_targets.html:96
#: templates/backoffice/components/assembly_tabs.html:45
#: templates/main/view_assembly_base.html:36
#: templates/targets/view_targets.html:2
@@ -6608,9 +6643,9 @@ msgstr "Befejezve"
#: templates/backoffice/assembly_selection.html:104
#: templates/backoffice/assembly_selection.html:231
#: templates/backoffice/assembly_view_respondent.html:242
-#: templates/backoffice/targets/bulk_edit_form.html:227
-#: templates/backoffice/targets/bulk_edit_form.html:278
-#: templates/backoffice/targets/category_block.html:146
+#: templates/backoffice/targets/bulk_edit_form.html:228
+#: templates/backoffice/targets/bulk_edit_form.html:279
+#: templates/backoffice/targets/category_block.html:147
#: templates/main/view_assembly_data.html:59
msgid "Comment"
msgstr "Megjegyzés"
@@ -6685,27 +6720,27 @@ msgstr "Kezdeti kiválasztás kiválasztási szempontok munkalapja:"
msgid "Replacement Categories Tab:"
msgstr "Pótláshoz tartozó kiválasztási szempontok munkalapja:"
-#: templates/backoffice/assembly_targets.html:88
+#: templates/backoffice/assembly_targets.html:100
#, python-format
msgid "Number to select: %(number)s"
msgstr "Kiválasztandók száma: %(number)s"
-#: templates/backoffice/assembly_targets.html:92
+#: templates/backoffice/assembly_targets.html:104
#: templates/targets/view_targets.html:8
msgid "No targets defined yet."
msgstr "Még nincsenek meghatározva kiválasztási szempontok."
-#: templates/backoffice/assembly_targets.html:105
+#: templates/backoffice/assembly_targets.html:117
#: templates/backoffice/targets/add_target_modal.html:14
#: templates/targets/components/add_category_form.html:28
msgid "Add target"
msgstr "Célszám hozzáadása"
-#: templates/backoffice/assembly_targets.html:111
+#: templates/backoffice/assembly_targets.html:123
msgid "Edit targets"
msgstr "Célszámok szerkesztése"
-#: templates/backoffice/assembly_targets.html:123
+#: templates/backoffice/assembly_targets.html:135
#, python-format
msgid ""
"All checks passed. Found %(num_features)s targets and %(num_people)s "
@@ -6715,14 +6750,14 @@ msgstr ""
"%(num_people)s jogosult jelentkező található. A célszámok készen állnak a"
" kiválasztásra."
-#: templates/backoffice/assembly_targets.html:132
+#: templates/backoffice/assembly_targets.html:144
#: templates/targets/view_targets.html:41
#: templates/targets/view_targets.html:52
#: templates/targets/view_targets.html:61
msgid "Target check found problems"
msgstr "A célszámok ellenőrzése problémákat talált"
-#: templates/backoffice/assembly_targets.html:140
+#: templates/backoffice/assembly_targets.html:152
msgid ""
"Target check found problems. See details below next to the affected "
"targets."
@@ -6730,12 +6765,39 @@ msgstr ""
"A célszámok ellenőrzése problémákat talált. A részleteket alább, az "
"érintett célszámok mellett találod."
-#: templates/backoffice/assembly_targets.html:181
+#: templates/backoffice/assembly_targets.html:170
+msgid ""
+"The algorithm has recommended target changes. Accept them individually "
+"below, or all at once here."
+msgstr ""
+
+#: templates/backoffice/assembly_targets.html:173
+#: templates/backoffice/assembly_targets.html:184
+msgid "Accept all suggestions"
+msgstr ""
+
+#: templates/backoffice/assembly_targets.html:193
+msgid "Accept all & re-run test selection"
+msgstr ""
+
+#: templates/backoffice/assembly_targets.html:193
+msgid "Accept all & re-run selection"
+msgstr ""
+
+#: templates/backoffice/assembly_targets.html:236
#: templates/backoffice/components/leave_guard_modal.html:17
msgid "Discard changes"
msgstr "Módosítások elvetése"
-#: templates/backoffice/assembly_targets.html:182
+#: templates/backoffice/assembly_targets.html:238
+msgid "Save & re-run test selection"
+msgstr ""
+
+#: templates/backoffice/assembly_targets.html:238
+msgid "Save & re-run selection"
+msgstr ""
+
+#: templates/backoffice/assembly_targets.html:240
#: templates/backoffice/components/edit_number_to_select_modal.html:41
#: templates/backoffice/registration/_step_email.html:95
#: templates/backoffice/registration/_step_form.html:23
@@ -7195,6 +7257,7 @@ msgstr "%(target)s: %(value)s, maximum %(current)s helyett %(suggested)s"
#: templates/backoffice/components/db_replacement_modal.html:90
#: templates/backoffice/components/db_replacement_modal.html:201
+#: templates/backoffice/components/suggestion_row.html:52
msgid "Accept"
msgstr "Elfogadás"
@@ -7402,6 +7465,10 @@ msgstr "Feladat megszakítása"
msgid "Put Task in Background"
msgstr "Feladat háttérbe helyezése"
+#: templates/backoffice/components/db_selection_progress_modal.html:197
+msgid "Adjust targets"
+msgstr ""
+
#: templates/backoffice/components/edit_number_to_select_modal.html:11
msgid "Edit Number to Select"
msgstr "Kiválasztandók számának szerkesztése"
@@ -7916,8 +7983,9 @@ msgid "Delete table"
msgstr "Táblázat törlése"
#: templates/backoffice/components/rich_editor.html:149
-#: templates/backoffice/targets/bulk_edit_form.html:155
-#: templates/backoffice/targets/bulk_edit_form.html:201
+#: templates/backoffice/components/suggestion_row.html:103
+#: templates/backoffice/targets/bulk_edit_form.html:156
+#: templates/backoffice/targets/bulk_edit_form.html:202
msgid "Undo"
msgstr "Mégse"
@@ -8031,6 +8099,21 @@ msgstr "befejezve"
msgid "has errors"
msgstr "hibás"
+#: templates/backoffice/components/suggestion_row.html:53
+msgid "Comment saved with this change"
+msgstr ""
+
+#: templates/backoffice/components/suggestion_row.html:66
+#: templates/backoffice/targets/category_block.html:96
+#: templates/targets/components/category_block.html:59
+#: templates/targets/components/category_block.html:183
+msgid "Suggestion"
+msgstr "Javaslat"
+
+#: templates/backoffice/components/suggestion_row.html:102
+msgid "Accepted. Save to keep the change."
+msgstr ""
+
#: templates/backoffice/components/url_display.html:24
msgid "Copied to clipboard"
msgstr "Vágólapra másolva"
@@ -8953,12 +9036,12 @@ msgid "Edit %(question)s"
msgstr "%(question)s szerkesztése"
#: templates/backoffice/respondent_field_schema/_editor.html:100
-#: templates/backoffice/targets/bulk_edit_form.html:243
+#: templates/backoffice/targets/bulk_edit_form.html:244
msgid "Move up"
msgstr "Mozgatás felfelé"
#: templates/backoffice/respondent_field_schema/_editor.html:100
-#: templates/backoffice/targets/bulk_edit_form.html:239
+#: templates/backoffice/targets/bulk_edit_form.html:240
msgid "Move down"
msgstr "Mozgatás lefelé"
@@ -9656,74 +9739,74 @@ msgstr "például: Nem"
msgid "Add"
msgstr "Hozzáadás"
-#: templates/backoffice/targets/bulk_edit_form.html:43
+#: templates/backoffice/targets/bulk_edit_form.html:44
msgid "the new value"
msgstr "az új érték"
-#: templates/backoffice/targets/bulk_edit_form.html:69
+#: templates/backoffice/targets/bulk_edit_form.html:70
#, python-format
msgid "Name of %(row)s"
msgstr "%(row)s neve"
-#: templates/backoffice/targets/bulk_edit_form.html:81
+#: templates/backoffice/targets/bulk_edit_form.html:82
msgid "Min and max were set by hand, not calculated from the percentage"
msgstr ""
"A minimum és a maximum kézzel beállított érték, nem a százalékból "
"számított"
-#: templates/backoffice/targets/bulk_edit_form.html:82
+#: templates/backoffice/targets/bulk_edit_form.html:83
msgid "Set by hand"
msgstr "Kézzel beállítva"
-#: templates/backoffice/targets/bulk_edit_form.html:88
+#: templates/backoffice/targets/bulk_edit_form.html:89
msgid "Min and max will be recalculated when you save."
msgstr "A minimum és a maximum mentéskor újraszámítódik."
-#: templates/backoffice/targets/bulk_edit_form.html:99
+#: templates/backoffice/targets/bulk_edit_form.html:100
#, python-format
msgid "Population share for %(row)s, in percent"
msgstr "%(row)s népességi aránya, százalékban"
-#: templates/backoffice/targets/bulk_edit_form.html:111
+#: templates/backoffice/targets/bulk_edit_form.html:112
#, python-format
msgid "Minimum for %(row)s"
msgstr "%(row)s minimuma"
-#: templates/backoffice/targets/bulk_edit_form.html:121
+#: templates/backoffice/targets/bulk_edit_form.html:122
#, python-format
msgid "Maximum for %(row)s"
msgstr "%(row)s maximuma"
-#: templates/backoffice/targets/bulk_edit_form.html:129
+#: templates/backoffice/targets/bulk_edit_form.html:130
msgid "Why min and max were set by hand"
msgstr "Miért lett kézzel beállítva a minimum és a maximum"
-#: templates/backoffice/targets/bulk_edit_form.html:130
+#: templates/backoffice/targets/bulk_edit_form.html:131
#, python-format
msgid "Comment on %(row)s"
msgstr "Megjegyzés a következőhöz: %(row)s"
-#: templates/backoffice/targets/bulk_edit_form.html:135
-#: templates/backoffice/targets/bulk_edit_form.html:145
+#: templates/backoffice/targets/bulk_edit_form.html:136
+#: templates/backoffice/targets/bulk_edit_form.html:146
msgid "Recalculate min and max from the percentage"
msgstr "A minimum és a maximum újraszámolása a százalékból"
-#: templates/backoffice/targets/bulk_edit_form.html:138
-#: templates/backoffice/targets/bulk_edit_form.html:152
+#: templates/backoffice/targets/bulk_edit_form.html:139
+#: templates/backoffice/targets/bulk_edit_form.html:153
msgid "Delete value"
msgstr "Érték törlése"
-#: templates/backoffice/targets/bulk_edit_form.html:198
+#: templates/backoffice/targets/bulk_edit_form.html:199
#, python-format
msgid "'%(name)s' and all its values will be deleted when you save."
msgstr "\"%(name)s\" és az összes értéke törlődik, amikor mentesz."
-#: templates/backoffice/targets/bulk_edit_form.html:220
+#: templates/backoffice/targets/bulk_edit_form.html:221
msgid "Source of population figures"
msgstr "Népességi adatok forrása"
-#: templates/backoffice/targets/bulk_edit_form.html:221
-#: templates/backoffice/targets/category_block.html:60
+#: templates/backoffice/targets/bulk_edit_form.html:222
+#: templates/backoffice/targets/category_block.html:61
msgid ""
"A link to the report the percentages come from, such as census tables or "
"a poll."
@@ -9731,26 +9814,26 @@ msgstr ""
"Link ahhoz a jelentéshez, amelyből a százalékos arányok származnak, "
"például népszámlálási táblák vagy közvélemény-kutatás."
-#: templates/backoffice/targets/bulk_edit_form.html:231
+#: templates/backoffice/targets/bulk_edit_form.html:232
msgid "Move this target down"
msgstr "Célszám mozgatása lefelé"
-#: templates/backoffice/targets/bulk_edit_form.html:232
+#: templates/backoffice/targets/bulk_edit_form.html:233
msgid "Move this target up"
msgstr "Célszám mozgatása felfelé"
-#: templates/backoffice/targets/bulk_edit_form.html:245
+#: templates/backoffice/targets/bulk_edit_form.html:246
msgid "Delete target"
msgstr "Célszám törlése"
-#: templates/backoffice/targets/bulk_edit_form.html:254
-#: templates/backoffice/targets/category_block.html:122
+#: templates/backoffice/targets/bulk_edit_form.html:255
+#: templates/backoffice/targets/category_block.html:123
#, python-format
msgid "Population (%%)"
msgstr "Népesség (%%)"
-#: templates/backoffice/targets/bulk_edit_form.html:260
-#: templates/backoffice/targets/category_block.html:128
+#: templates/backoffice/targets/bulk_edit_form.html:261
+#: templates/backoffice/targets/category_block.html:129
msgid ""
"Counts people in the pool, and those selected or confirmed. Withdrawn "
"people and test submissions are not counted."
@@ -9759,8 +9842,8 @@ msgstr ""
"embereket számolja. A visszalépett emberek és a tesztbeküldések nem "
"számítanak bele."
-#: templates/backoffice/targets/bulk_edit_form.html:269
-#: templates/backoffice/targets/category_block.html:137
+#: templates/backoffice/targets/bulk_edit_form.html:270
+#: templates/backoffice/targets/category_block.html:138
msgid ""
"Counts people who have been selected, including those who have since "
"confirmed their place."
@@ -9768,58 +9851,51 @@ msgstr ""
"A kiválasztott embereket számolja, beleértve azokat is, akik azóta "
"megerősítették a részvételüket."
-#: templates/backoffice/targets/bulk_edit_form.html:312
+#: templates/backoffice/targets/bulk_edit_form.html:318
#: templates/targets/components/category_block.html:247
msgid "Add value"
msgstr "Érték hozzáadása"
-#: templates/backoffice/targets/bulk_edit_form.html:317
+#: templates/backoffice/targets/bulk_edit_form.html:323
msgid "Add values found in respondent data"
msgstr "A jelentkezői adatokban talált értékek hozzáadása"
-#: templates/backoffice/targets/bulk_edit_form.html:322
+#: templates/backoffice/targets/bulk_edit_form.html:328
#, python-format
msgid "Not yet targeted: %(values)s"
msgstr "Még nincs hozzájuk célszám: %(values)s"
-#: templates/backoffice/targets/bulk_edit_form.html:332
-#: templates/backoffice/targets/category_block.html:211
+#: templates/backoffice/targets/bulk_edit_form.html:338
+#: templates/backoffice/targets/category_block.html:212
#: templates/targets/components/category_block.html:195
msgid "Total"
msgstr "Összesen"
-#: templates/backoffice/targets/category_block.html:11
-#: templates/backoffice/targets/category_block.html:14
+#: templates/backoffice/targets/category_block.html:12
+#: templates/backoffice/targets/category_block.html:15
msgid "Manually modified"
msgstr "Kézzel módosítva"
-#: templates/backoffice/targets/category_block.html:18
-#: templates/backoffice/targets/category_block.html:21
+#: templates/backoffice/targets/category_block.html:19
+#: templates/backoffice/targets/category_block.html:22
msgid "Auto calculated"
msgstr "Automatikusan számolva"
-#: templates/backoffice/targets/category_block.html:59
+#: templates/backoffice/targets/category_block.html:60
msgid "Source of population figures:"
msgstr "Népességi adatok forrása:"
-#: templates/backoffice/targets/category_block.html:95
-#: templates/backoffice/targets/category_block.html:186
-#: templates/targets/components/category_block.html:59
-#: templates/targets/components/category_block.html:183
-msgid "Suggestion"
-msgstr "Javaslat"
-
-#: templates/backoffice/targets/category_block.html:204
+#: templates/backoffice/targets/category_block.html:205
#: templates/targets/components/category_block.html:208
msgid "No values defined."
msgstr "Nincsenek meghatározott értékek."
-#: templates/backoffice/targets/category_block.html:250
+#: templates/backoffice/targets/category_block.html:251
#: templates/targets/components/category_block.html:223
msgid "Values in respondent data not yet targeted:"
msgstr "A jelentkezői adatokban szereplő, még célszám nélküli értékek:"
-#: templates/backoffice/targets/category_block.html:252
+#: templates/backoffice/targets/category_block.html:253
msgid "Add them under \"Edit targets\"."
msgstr "Add hozzá őket a „Célszámok szerkesztése” alatt."
@@ -9887,6 +9963,16 @@ msgid_plural "and %(num)s others"
msgstr[0] "és még %(num)s"
msgstr[1] "és még %(num)s"
+#: templates/backoffice/targets/value_suggestions.html:22
+#, python-format
+msgid "Accepted algorithm suggestion — minimum %(current)s → %(suggested)s"
+msgstr ""
+
+#: templates/backoffice/targets/value_suggestions.html:24
+#, python-format
+msgid "Accepted algorithm suggestion — maximum %(current)s → %(suggested)s"
+msgstr ""
+
#: templates/db_selection/replace.html:2
msgid "Replacements (DB)"
msgstr "Résztvevők pótlása (adatbázis)"
@@ -11476,14 +11562,14 @@ msgid_plural "%(count)s rows"
msgstr[0] "%(count)s sor"
msgstr[1] "%(count)s sor"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:33
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:33
#, python-format
msgid "No '%(column)s' column %(error_label)s found in %(data_container)s!"
msgstr ""
"Nem található '%(column)s' oszlop %(error_label)s ebben: "
"%(data_container)s!"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:34
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:34
#, python-format
msgid ""
"MORE THAN 1 '%(column)s' column %(error_label)s found in "
@@ -11492,7 +11578,7 @@ msgstr ""
"TÖBB MINT 1 '%(column)s' oszlop %(error_label)s található ebben: "
"%(data_container)s!"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:36
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:36
#, python-format
msgid ""
"Failed to find person at position %(position)s in "
@@ -11501,16 +11587,16 @@ msgstr ""
"Nem található jelölt %(position)s pozícióban itt: "
"%(feature_name)s/%(feature_value)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:37
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:37
msgid "Logic error - trying to create new tab before choosing suffix"
msgstr "Logikai hiba – új munkalap létrehozása az utótag kiválasztása előtt"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:38
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:38
#, python-format
msgid "Google spreadsheet not found: %(spreadsheet_name)s."
msgstr "A Google Táblázat nem található: %(spreadsheet_name)s."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:40
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:40
#, python-format
msgid ""
"Google spreadsheet %(spreadsheet_name)s exists but is not shared with the"
@@ -11521,7 +11607,7 @@ msgstr ""
"szolgáltatásfiókkal: %(service_account_email)s. Oszd meg ezzel az e-mail "
"címmel, és próbáld újra."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:44
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:44
#, python-format
msgid ""
"Google spreadsheet '%(title)s' is shared read-only with "
@@ -11532,7 +11618,7 @@ msgstr ""
"címmel: %(service_account_email)s. Adj neki szerkesztési jogot, hogy a "
"kimeneti munkalapok írhatók legyenek."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:48
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:48
#, python-format
msgid ""
"Error in Google sheet: no tab called '%(tab_name)s' found in spreadsheet "
@@ -11541,7 +11627,7 @@ msgstr ""
"Hiba a Google Táblázatban: nem található '%(tab_name)s' nevű munkalap "
"'%(spreadsheet_title)s' táblázatban."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:51
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:51
#, python-format
msgid ""
"The file '%(file_name)s' is a %(common_name)s (mimetype %(mimetype)s), "
@@ -11553,19 +11639,19 @@ msgstr ""
" Mentés Google-táblázatként lehetőséget, majd használd az így létrejött "
"új fájlt."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:55
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:55
#, python-format
msgid ""
"It seems like some variables do not have a value. Original exception: "
"%(error)s."
msgstr "Úgy tűnik, néhány változónak nincs értéke. Eredeti kivétel: %(error)s."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:57
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:57
#, python-format
msgid "the header row in the %(tab_name)s tab contains duplicates: %(duplicates)s"
msgstr "%(tab_name)s munkalap fejlécsora ismétlődéseket tartalmaz: %(duplicates)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:61
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:61
msgid ""
"Running the test selection does not support generating a transparent "
"lottery, so, if `test_selection` is true, `number_selections` must be 1."
@@ -11574,7 +11660,7 @@ msgstr ""
"előállítását, ezért ha a `test_selection` értéke igaz, a "
"`number_selections` értékének 1-nek kell lennie."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:65
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:65
msgid ""
"Currently, the legacy algorithm does not support generating a transparent"
" lottery, so `number_selections` must be set to 1."
@@ -11582,7 +11668,7 @@ msgstr ""
"A régi algoritmus jelenleg nem támogatja az átlátható sorsolás "
"előállítását, ezért a `number_selections` értékét 1-re kell állítani."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:69
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:69
msgid ""
"The diversimax algorithm does not support generating multiple committees,"
" so `number_selections` must be set to 1."
@@ -11590,7 +11676,7 @@ msgstr ""
"A diversimax algoritmus nem támogatja több csoport-összeállítás "
"előállítását, ezért a `number_selections` értékét 1-re kell állítani."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:73
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:73
#, python-format
msgid ""
"Unknown selection algorithm %(algorithm)r, must be either 'legacy', "
@@ -11599,24 +11685,24 @@ msgstr ""
"Ismeretlen kiválasztási algoritmus: %(algorithm)r, a következők egyike "
"kell legyen: 'legacy', 'leximin', 'maximin', 'diversimax' vagy 'nash'."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:75
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:75
#, python-format
msgid "solver_backend %(backend)s is not one of: %(valid_backends)s"
msgstr "a solver_backend %(backend)s nem szerepel ezek között: %(valid_backends)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:76
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:76
#, python-format
msgid "Unknown solver backend: %(backend)s"
msgstr "Ismeretlen megoldó backend: %(backend)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:77
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:77
#, python-format
msgid "Could not find feature column, looked for column headers: %(column_names)s"
msgstr ""
"Nem található a kiválasztási szempont oszlopa, a keresett oszlopfejlécek:"
" %(column_names)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:79
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:79
#, python-format
msgid ""
"Could not find feature value column, looked for column headers: "
@@ -11625,12 +11711,12 @@ msgstr ""
"Nem található a szempontérték oszlopa, a keresett oszlopfejlécek: "
"%(column_names)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:81
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:81
#, python-format
msgid "Did not find required column name '%(field_name)s' in the input"
msgstr "A kötelező '%(field_name)s' oszlopnév nem található a bemenetben"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:82
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:82
#, python-format
msgid ""
"Found MORE THAN 1 column named '%(field_name)s' in the input (found "
@@ -11639,41 +11725,41 @@ msgstr ""
"TÖBB MINT 1 '%(field_name)s' nevű oszlop található a bemenetben (talált: "
"%(count)s)"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:83
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:83
#, python-format
msgid "Unexpected error in set of column names: %(headers)s"
msgstr "Váratlan hiba az oszlopnevek halmazában: %(headers)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:85
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:85
msgid "check_same_address_columns must be a LIST of strings"
msgstr "a check_same_address_columns értékének szövegek LISTÁJÁNAK kell lennie"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:86
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:86
msgid "check_same_address_columns must be a list of STRINGS"
msgstr "a check_same_address_columns értékének SZÖVEGEK listájának kell lennie"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:87
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:87
msgid "check_same_address is TRUE but there are no columns listed to check."
msgstr ""
"a check_same_address értéke IGAZ, de nincs ellenőrizendő oszlop "
"felsorolva."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:88
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:88
msgid "columns_to_keep must be a LIST of strings"
msgstr "a columns_to_keep értékének szövegek LISTÁJÁNAK kell lennie"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:89
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:89
msgid "columns_to_keep must be a list of STRINGS"
msgstr "a columns_to_keep értékének SZÖVEGEK listájának kell lennie"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:90
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:90
#, python-format
msgid "selection_algorithm %(algorithm)s is not one of: %(valid_algorithms)s"
msgstr ""
"a selection_algorithm %(algorithm)s nem szerepel ezek között: "
"%(valid_algorithms)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:93
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:93
msgid ""
"Diversimax algorithm requires the optional 'diversimax' dependencies, "
"which are not available"
@@ -11681,99 +11767,99 @@ msgstr ""
"A diversimax algoritmushoz szükség van az opcionális 'diversimax' "
"függőségekre, amelyek nem érhetők el"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:95
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:95
msgid "Leximin algorithm requires Gurobi solver which is not available"
msgstr "A leximin algoritmushoz a Gurobi megoldó szükséges, amely nem érhető el"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:100
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:100
#, python-format
msgid "There is no %(field)s value set"
msgstr "Nincs beállítva %(field)s érték"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:101
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:101
#, python-format
msgid "'%(value)s' is not a number"
msgstr "'%(value)s' nem szám"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:102
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:102
#, python-format
msgid "Empty %(value_column_name)s in %(feature_column_name)s %(feature_name)s"
msgstr "Üres %(value_column_name)s ebben: %(feature_column_name)s %(feature_name)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:103
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:103
#, python-format
msgid "Minimum (%(min)s) should not be greater than maximum (%(max)s)"
msgstr "A minimum (%(min)s) nem lehet nagyobb a maximumnál (%(max)s)"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:104
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:104
#, python-format
msgid "min_flex (%(min_flex)s) should not be greater than min (%(min)s)"
msgstr "a min_flex (%(min_flex)s) nem lehet nagyobb a min értéknél (%(min)s)"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:105
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:105
#, python-format
msgid "max_flex (%(max_flex)s) should not be less than max (%(max)s)"
msgstr "a max_flex (%(max_flex)s) nem lehet kisebb a max értéknél (%(max)s)"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:107
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:107
#, python-format
msgid "Value '%(value)s' not in %(feature_column_name)s %(feature_name)s"
msgstr ""
"'%(value)s' érték nem szerepel itt: %(feature_column_name)s "
"%(feature_name)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:108
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:108
#, python-format
msgid "Empty value in %(feature_column_name)s %(feature_name)s"
msgstr "Üres érték itt: %(feature_column_name)s %(feature_name)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:112
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:112
#, python-format
msgid "%(msg)s: for row %(row)s, column header %(key)s"
msgstr "%(msg)s: %(row)s. sorban, %(key)s oszlopfejlécnél"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:113
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:113
#, python-format
msgid "%(msg)s: for row %(row)s, column headers %(keys)s"
msgstr "%(msg)s: %(row)s. sorban, %(keys)s oszlopfejléceknél"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:117
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:117
#, python-format
msgid "Parser error(s) while reading features from '%(tab_name)s' worksheet"
msgstr ""
"Feldolgozási hiba a kiválasztási szempontok beolvasása közben "
"'%(tab_name)s' munkalapról"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:118
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:119
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:118
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:119
#, python-format
msgid "Parser error(s) while reading people from '%(tab_name)s' worksheet"
msgstr "Feldolgozási hiba az emberek beolvasása közben '%(tab_name)s' munkalapról"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:120
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:120
#, python-format
msgid "%(msg)s - see cell %(cell_name)s"
msgstr "%(msg)s – lásd %(cell_name)s cellát"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:121
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:121
#, python-format
msgid "%(msg)s - see cells %(cell_names)s"
msgstr "%(msg)s – lásd %(cell_names)s cellákat"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:125
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:109
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:125
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:109
#, python-format
msgid "Found %(count)s IDs that have more than one row with different data"
msgstr ""
"%(count)s olyan azonosító található, amelyhez több, eltérő adatú sor "
"tartozik"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:126
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:126
#, python-format
msgid "For ID '%(person_id)s' one row of data is: %(row_data)s"
msgstr "'%(person_id)s' azonosítóhoz tartozó egyik adatsor: %(row_data)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:128
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:128
#, python-format
msgid ""
"Not enough people with the value '%(feature_value)s' in category "
@@ -11782,7 +11868,7 @@ msgstr ""
"Nincs elég ember '%(feature_name)s' kiválasztási szempont "
"'%(feature_value)s' értékével – a minimum %(min)s, de csak %(count)s van"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:132
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:132
#, python-format
msgid ""
"The number of people to select (%(desired)s) is out of the range of the "
@@ -11793,7 +11879,7 @@ msgstr ""
"szempontnál szereplő létszámok tartományán. A [%(min)s, %(max)s] "
"tartományba kell esnie."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:138
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:138
msgid ""
"It is not possible to hit all the targets with the current set of people."
" I suggest the following steps:"
@@ -11801,12 +11887,12 @@ msgstr ""
"A jelenlegi emberekkel nem lehet minden célszámot elérni. A következő "
"lépéseket javaslom:"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:141
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:141
#, python-format
msgid "Inconsistent numbers in min and max in the %(feature_column_name)s input:"
msgstr "Ellentmondásos min. és max. számok %(feature_column_name)s bemenetben:"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:142
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:142
#, python-format
msgid ""
"The smallest maximum is %(max_val)s for %(feature_column_name)s "
@@ -11815,7 +11901,7 @@ msgstr ""
"A legkisebb maximum %(max_val)s '%(max_feature)s' %(feature_column_name)s"
" értéknél"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:143
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:143
#, python-format
msgid ""
"The largest minimum is %(min_val)s for %(feature_column_name)s "
@@ -11824,7 +11910,7 @@ msgstr ""
"A legnagyobb minimum %(min_val)s '%(min_feature)s' "
"%(feature_column_name)s értéknél"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/error_messages.py:145
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/error_messages.py:145
#, python-format
msgid ""
"You need to reduce the minimums for %(min_feature)s or increase the "
@@ -11833,38 +11919,38 @@ msgstr ""
"Csökkentened kell %(min_feature)s minimumait, vagy növelned "
"%(max_feature)s maximumait."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:32
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:32
msgid "Loading features from string."
msgstr "Kiválasztási szempontok betöltése szövegből."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:33
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:33
msgid "Loading people from string."
msgstr "Emberek betöltése szövegből."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:34
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:34
msgid "Loading already selected people from string."
msgstr "A már kiválasztott emberek betöltése szövegből."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:35
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:35
msgid "No already selected data provided, using empty data."
msgstr "Nem adtál meg már kiválasztott adatokat, üres adatokkal folytatódik."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:36
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:36
#, python-format
msgid "Loading features from file %(file_path)s."
msgstr "Kiválasztási szempontok betöltése %(file_path)s fájlból."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:37
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:37
#, python-format
msgid "Loading people from file %(file_path)s."
msgstr "Emberek betöltése %(file_path)s fájlból."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:38
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:38
#, python-format
msgid "Loading already selected people from file %(file_path)s."
msgstr "A már kiválasztott emberek betöltése %(file_path)s fájlból."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:39
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:39
msgid ""
"No already selected file provided or file does not exist, using empty "
"data."
@@ -11872,23 +11958,23 @@ msgstr ""
"Nem adtál meg már kiválasztottakat tartalmazó fájlt, vagy a fájl nem "
"létezik; üres adatokkal folytatódik."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:40
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:40
#, python-format
msgid "Reading in '%(tab_name)s' tab in above Google sheet."
msgstr "'%(tab_name)s' munkalap beolvasása a fenti Google Táblázatból."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:41
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:41
msgid "No already selected tab specified, using empty data."
msgstr ""
"Nem adtál meg már kiválasztottakat tartalmazó munkalapot, üres adatokkal "
"folytatódik."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:42
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:42
#, python-format
msgid "Number of features found: %(count)s"
msgstr "Talált kiválasztási szempontok száma: %(count)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:44
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:44
#, python-format
msgid ""
"Reading in '%(tab_name)s' tab (header at row %(header_row)s) in above "
@@ -11897,32 +11983,32 @@ msgstr ""
"'%(tab_name)s' munkalap beolvasása (fejléc %(header_row)s. sorban) a "
"fenti Google Táblázatból."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:46
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:46
#, python-format
msgid "Opened Google Sheet: '%(title)s'."
msgstr "Megnyitott Google Táblázat: '%(title)s'."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:50
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:50
msgid "Using legacy algorithm."
msgstr "A régi algoritmus fut."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:51
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:51
msgid "Using maximin algorithm."
msgstr "A maximin algoritmus fut."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:52
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:52
msgid "Using leximin algorithm."
msgstr "A leximin algoritmus fut."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:53
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:53
msgid "Using Nash algorithm."
msgstr "A Nash-algoritmus fut."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:54
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:54
msgid "Had to switch to ECOS solver."
msgstr "Váltás szükséges volt az ECOS megoldóra."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:56
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:56
msgid ""
"The leximin algorithm requires the optimization library Gurobi to be "
"installed (commercial, free academic licenses available). Switching to "
@@ -11934,7 +12020,7 @@ msgstr ""
"az egyszerűbb maximin algoritmusra, amely nyílt forráskódú megoldókkal is"
" futtatható."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:61
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:61
#, python-format
msgid ""
"Algorithm produced distribution over %(total_committees)s committees, out"
@@ -11944,7 +12030,7 @@ msgstr ""
" állított elő, amelyek közül %(non_zero_committees)s kerül kiválasztásra "
"pozitív valószínűséggel."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:65
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:65
#, python-format
msgid ""
"INFO: The distribution over panels is what is known as a 'basic "
@@ -11960,19 +12046,19 @@ msgstr ""
"adatokkal: algorithm=%(algorithm)s, num_panels=%(num_panels)s, "
"num_agents=%(num_agents)s, min_probs=%(min_probs)s."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:70
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:70
#, python-format
msgid "Agent %(agent_id)s not contained in any feasible committee."
msgstr ""
"%(agent_id)s azonosítójú jelölt egyetlen megvalósítható csoport-"
"összeállításban sem szerepel."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:71
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:71
#, python-format
msgid "Scaled Nash welfare is now: %(scaled_welfare)s."
msgstr "A skálázott Nash-jólét mostantól: %(scaled_welfare)s."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:73
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:73
#, python-format
msgid ""
"Maximin is at most %(at_most)s, can do %(upper_str)s with "
@@ -11981,7 +12067,7 @@ msgstr ""
"A maximin legfeljebb %(at_most)s, %(num_committees)s csoport-"
"összeállítással %(upper_str)s érhető el. Eltérés: %(gap_str)s."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:76
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:76
#, python-format
msgid ""
"Leximin is at most %(at_most)s, can do %(dual_obj_str)s with "
@@ -11990,38 +12076,38 @@ msgstr ""
"A leximin legfeljebb %(at_most)s, %(num_committees)s csoport-"
"összeállítással %(dual_obj_str)s érhető el. Eltérés: %(gap_str)s."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:81
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:81
msgid "WARNING: Panel is not selected at random! Only use for testing!"
msgstr ""
"FIGYELEM: A csoport-összeállítás nem véletlenszerűen lett kiválasztva! "
"Csak tesztelésre használd!"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:82
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:82
msgid "Initial: (selected = 0)"
msgstr "Kezdet: (kiválasztva = 0)"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:83
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:83
#, python-format
msgid "Trial number: %(trial)s"
msgstr "Kísérlet száma: %(trial)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:84
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:84
msgid "SUCCESS!! Final:"
msgstr "SIKER!! Végeredmény:"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:85
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:85
#, python-format
msgid "Failed %(attempts)s times. Gave up."
msgstr "%(attempts)s alkalommal sikertelen volt. A futás megszakadt."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:86
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:86
#, python-format
msgid "Failed one attempt. Selection Error raised - will retry. %(error)s"
msgstr ""
"Egy kísérlet sikertelen volt. Kiválasztási hiba történt – újrapróbálkozás"
" következik. %(error)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:87
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:87
msgid ""
"No target checks done for multiple selections - please see your output "
"files."
@@ -12029,7 +12115,7 @@ msgstr ""
"Több kiválasztás esetén nem történik célszám-ellenőrzés – nézd meg a "
"kimeneti fájljaidat."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:89
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:89
msgid ""
"We do not calculate target details for multiple selections - please see "
"your output files."
@@ -12037,27 +12123,27 @@ msgstr ""
"Több kiválasztás esetén nem készül célszám-részletezés – nézd meg a "
"kimeneti fájljaidat."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:94
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:94
#, python-format
msgid "Writing selected rows to %(file_path)s"
msgstr "A kiválasztott sorok írása ide: %(file_path)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:95
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:95
#, python-format
msgid "Writing remaining rows to %(file_path)s"
msgstr "A fennmaradó sorok írása ide: %(file_path)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:96
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:96
#, python-format
msgid "Writing selected people to tab: %(tab_name)s"
msgstr "A kiválasztott emberek írása erre a munkalapra: %(tab_name)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:97
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:97
#, python-format
msgid "Writing remaining people to tab: %(tab_name)s"
msgstr "A fennmaradó emberek írása erre a munkalapra: %(tab_name)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:99
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:99
msgid ""
"Finished writing selected (only, because writing remaining is deselected "
"in the configuration)"
@@ -12065,66 +12151,66 @@ msgstr ""
"A kiválasztottak kiírása kész (csak azoké, mert a fennmaradók kiírása ki "
"van kapcsolva a beállításokban)"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:101
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:101
msgid "Finished writing both selected and remaining"
msgstr "A kiválasztottak és a fennmaradók kiírása is kész"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:105
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:105
#, python-format
msgid "WARNING: blank cell found in ID column in row %(row)s - skipped that line!"
msgstr ""
"FIGYELEM: üres cella az azonosító oszlopban %(row)s. sorban – ez a sor "
"kimaradt!"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:106
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:106
msgid "WARNING: Duplicate people found:"
msgstr "FIGYELEM: Ismétlődő emberek találhatók:"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:107
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:107
#, python-format
msgid "Person with ID '%(person_id)s' appears %(count)s times in rows: %(rows)s"
msgstr ""
"'%(person_id)s' azonosítójú személy %(count)s alkalommal szerepel a "
"következő sorokban: %(rows)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:108
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:108
#, python-format
msgid "Found %(count)s IDs that have more than one row"
msgstr "%(count)s olyan azonosító található, amelyhez több sor tartozik"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:110
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:110
#, python-format
msgid "Duplicated IDs are: %(ids)s"
msgstr "Az ismétlődő azonosítók: %(ids)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:111
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:111
msgid "All duplicate rows have identical data - processing continuing."
msgstr ""
"Minden ismétlődő sor azonos adatokat tartalmaz – a feldolgozás "
"folytatódik."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:112
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:112
msgid "All agents are contained in some feasible committee."
msgstr "Minden jelölt szerepel valamelyik megvalósítható csoport-összeállításban."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:113
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:113
#, python-format
msgid "Heuristic successfully generated %(count)s additional committees."
msgstr ""
"A heurisztika sikeresen létrehozott %(count)s további csoport-"
"összeállítást."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:117
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:117
#, python-format
msgid "Using these settings: %(settings)s"
msgstr "A használt beállítások: %(settings)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:118
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:118
#, python-format
msgid "Random seed set to: %(seed)s"
msgstr "A véletlenszám-mag beállítva erre: %(seed)s"
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:120
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:120
#, python-format
msgid ""
"Wrote default settings to '%(file_path)s' - if editing is required, "
@@ -12133,7 +12219,7 @@ msgstr ""
"Az alapértelmezett beállítások ide íródtak: '%(file_path)s' – ha "
"szerkesztésre van szükség, indítsd újra ezt az alkalmazást."
-#: .venv/lib/python3.12/site-packages/sortition_algorithms/report_messages.py:123
+#: .venv/lib/python3.14/site-packages/sortition_algorithms/report_messages.py:123
msgid ""
"WARNING: Settings file is such that we do NOT check if respondents have "
"same address."