diff --git a/dataedit/templates/dataedit/opr_contributor.html b/dataedit/templates/dataedit/opr_contributor.html index 0e7d9c148..8a6b14a7e 100644 --- a/dataedit/templates/dataedit/opr_contributor.html +++ b/dataedit/templates/dataedit/opr_contributor.html @@ -112,107 +112,356 @@
-
-
- {% for item in meta.general %} -
+
+
+ + {% for item in meta.general.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} + {{ item.additional_comment }} + {% endif %} +

+
+ {% endfor %} + + {% for prefix, items in meta.general.grouped.items %} + {% with prefix|slugify as safe_id %} +
+

+ +

+
+
+ {% for item in items %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ reviewer_suggestions.item.field }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} {{ item.additional_comment }} - {% endif %}

- {% endfor %} -
+ {% endfor %}
+
+
+ {% endwith %} + {% endfor %} + +
+
+
-
-

Spatial

- {% for item in meta.spatial %} -
+
+ +

Spatial

+ + {% for item in meta.spatial.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} + {{ item.additional_comment }} + {% endif %} +

+
+ {% endfor %} + + {% for prefix, items in meta.spatial.grouped.items %} + {% with prefix|slugify as safe_id %} +
+

+ +

+
+
+ {% for item in items %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ reviewer_suggestions.item.field }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} {{ item.additional_comment }} {% endif %}

- {% endfor %} -

Temporal

- {% for item in meta.temporal %} -
+ {% endfor %} +
+
+
+ {% endwith %} + {% endfor %} + +

Temporal

+ + {% for item in meta.temporal.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} + {{ item.additional_comment }} + {% endif %} +

+
+ {% endfor %} + + {% for prefix, items in meta.temporal.grouped.items %} + {% with prefix|slugify as safe_id %} +
+

+ +

+
+
+ {% for item in items %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} {{ item.additional_comment }} {% endif %}

- {% endfor %} -
+ {% endfor %}
-
-
- {% for item in meta.source %} -
+
+
+ {% endwith %} + {% endfor %} + +
+
+ +
+
+ + {# --- flat dataset‑level source fields (if any) --------------------- #} + {% for item in meta.source.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} +

+
+ {% endfor %} + + {# ---------- 1st level accordion: Source 0, Source 1 … -------------- #} + {% for src_prefix, src_content in meta.source.grouped.items %} + {% with src_prefix|slugify as src_id %} +
+

+ +

+
+
+ + {# ---- flat fields inside this Source N -------------------- #} + {% for item in src_content.flat %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
- {{ item.additional_comment }} - {% endif %} + {{ item.display_field }} + {{ item.newValue|default:item.value }}

{% endfor %} -
+ + {# ---- nested accordion for sub-groups (generic) ---------------- #} +
+ {% for sub_prefix, sub_items in src_content.grouped.items %} + {% with sub_prefix|slugify as sub_id %} +
+

+ +

+
+
+ {% for item in sub_items %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} +

+
+ {% endfor %} +
+
+
+ {% endwith %} + {% endfor %} +
{# /accordion-inner #} +
+
+
+ {% endwith %} + {% endfor %} + +
+
+
-
- {% for item in meta.license %} -
+
+ + {# --- flat dataset‑level license fields (if any) -------------------- #} + {% for item in meta.license.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} +

+
+ {% endfor %} + + {# ---------- 1st level accordion: License 0, License 1 … ------------ #} + {% for lic_prefix, lic_content in meta.license.grouped.items %} + {% with lic_prefix|slugify as lic_id %} +
+

+ +

+
+
+ + {# ---- flat fields inside this License N ------------------- #} + {% for item in lic_content.flat %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
- {{ item.additional_comment }} - {% endif %} + {{ item.display_field }} + {{ item.newValue|default:item.value }}

{% endfor %} -
+ + {# ---- nested accordion for sub‑lists (generic) ------------ #} +
+ {% for sub_prefix, sub_items in lic_content.grouped.items %} + {% with sub_prefix|slugify as sub_id %} +
+

+ +

+
+
+ {% for item in sub_items %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} +

+
+ {% endfor %} +
+
+
+ {% endwith %} + {% endfor %} +
{# /accordion-inner #} +
+
+
+ {% endwith %} + {% endfor %} + +
+
diff --git a/dataedit/templates/dataedit/opr_review.html b/dataedit/templates/dataedit/opr_review.html index 0f9bb3a20..6d5a015de 100644 --- a/dataedit/templates/dataedit/opr_review.html +++ b/dataedit/templates/dataedit/opr_review.html @@ -118,123 +118,413 @@
-
- {% for item in meta.general %} -
+
+ + {% for item in meta.general.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} + {{ item.additional_comment }} + {% endif %} +

+
+ {% endfor %} + + {% for prefix, items in meta.general.grouped.items %} + {% with prefix|slugify as safe_id %} +
+

+ +

+
+
+ {% for item in items %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} {{ item.additional_comment }} {% endif %}

- {% endfor %} -
+ {% endfor %}
+
+
+ {% endwith %} + {% endfor %} + +
+
+
-
-

Spatial

- {% for item in meta.spatial %} -
+
+ +

Spatial

+ + {% for item in meta.spatial.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} + {{ item.additional_comment }} + {% endif %} +

+
+ {% endfor %} + + {% for prefix, items in meta.spatial.grouped.items %} + {% with prefix|slugify as safe_id %} +
+

+ +

+
+
+ {% for item in items %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} {{ item.additional_comment }} {% endif %}

- {% endfor %} -

Temporal

- {% for item in meta.temporal %} -
+ {% endfor %} +
+
+
+ {% endwith %} + {% endfor %} + +

Temporal

+ + {% for item in meta.temporal.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} + {{ item.additional_comment }} + {% endif %} +

+
+ {% endfor %} + + {% for prefix, items in meta.temporal.grouped.items %} + {% with prefix|slugify as safe_id %} +
+

+ +

+
+
+ {% for item in items %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} {{ item.additional_comment }} {% endif %}

- {% endfor %} -
+ {% endfor %}
+
+
+ {% endwith %} + {% endfor %} + +
+
+
-
- {% for item in meta.source %} -
+
+ + {# --- flat dataset‑level source fields (if any) --------------------- #} + {% for item in meta.source.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} +

+
+ {% endfor %} + + {# ---------- 1st level accordion: Source 0, Source 1 … -------------- #} + {% for src_prefix, src_content in meta.source.grouped.items %} + {% with src_prefix|slugify as src_id %} +
+

+ +

+
+
+ + {# ---- flat fields inside this Source N -------------------- #} + {% for item in src_content.flat %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
- {{ item.additional_comment }} - {% endif %} + {{ item.display_field }} + {{ item.newValue|default:item.value }}

{% endfor %} -
+ + {# ---- nested accordion for sub-groups (generic) ---------------- #} +
+ {% for sub_prefix, sub_items in src_content.grouped.items %} + {% with sub_prefix|slugify as sub_id %} +
+

+ +

+
+
+ {% for item in sub_items %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} +

+
+ {% endfor %} +
+
+
+ {% endwith %} + {% endfor %} +
{# /accordion-inner #} +
+
+
+ {% endwith %} + {% endfor %} + +
+
+
-
- {% for item in meta.license %} -
+
+ + {# --- flat dataset‑level license fields (if any) -------------------- #} + {% for item in meta.license.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} +

+
+ {% endfor %} + + {# ---------- 1st level accordion: License 0, License 1 … ------------ #} + {% for lic_prefix, lic_content in meta.license.grouped.items %} + {% with lic_prefix|slugify as lic_id %} +
+

+ +

+
+
+ + {# ---- flat fields inside this License N ------------------- #} + {% for item in lic_content.flat %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
- {{ item.additional_comment }} - {% endif %} + {{ item.display_field }} + {{ item.newValue|default:item.value }}

{% endfor %} -
+ + {# ---- nested accordion for sub‑lists (generic) ------------ #} +
+ {% for sub_prefix, sub_items in lic_content.grouped.items %} + {% with sub_prefix|slugify as sub_id %} +
+

+ +

+
+
+ {% for item in sub_items %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} +

+
+ {% endfor %} +
+
+
+ {% endwith %} + {% endfor %} +
{# /accordion-inner #} +
+
+
+ {% endwith %} + {% endfor %} + +
+
+
-
- {% for item in meta.resource %} -
+
+ + {% for item in meta.resource.flat %} +
+

+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} + {{ item.additional_comment }} + {% endif %} +

+
+ {% endfor %} + + {% for prefix, items in meta.resource.grouped.items %} + {% with prefix|slugify as safe_id %} +
+

+ +

+
+
+ {% for item in items %} +

- {% if item.field is Null %} - {{item.field}} - {{ item.value }} - {% else %} - {{item.field}} - {{ item.newValue|default:item.value }} - {{ item.reviewer_suggestion }} - {{ item.suggestion_comment }}
+ {{ item.display_field }} + {{ item.newValue|default:item.value }} + {% if item.reviewer_suggestion %} + {{ item.reviewer_suggestion }} + {% endif %} + {% if item.suggestion_comment %} + {{ item.suggestion_comment }}
+ {% endif %} + {% if item.additional_comment %} {{ item.additional_comment }} {% endif %}

{% endfor %} -
+
+
+ {% endwith %} + {% endfor %} + +
+
+
@@ -245,7 +535,7 @@ {{ field_descriptions_json }}
- +
- +
diff --git a/dataedit/views.py b/dataedit/views.py index f66c945ca..50a2fa645 100644 --- a/dataedit/views.py +++ b/dataedit/views.py @@ -1,30 +1,30 @@ -# SPDX-FileCopyrightText: 2025 Pierre Francois © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Pierre Francois © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Christian Winger © Öko-Institut e.V. -# SPDX-FileCopyrightText: 2025 Daryna Barabanova © Reiner Lemoine Institut +# SPDX-FileCopyrightText: 2025 Pierre Francois © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Pierre Francois © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Christian Winger © Öko-Institut e.V. # noqa: E501 +# SPDX-FileCopyrightText: 2025 Daryna Barabanova © Reiner Lemoine Institut # noqa: E501 # SPDX-FileCopyrightText: 2025 Eike Broda -# SPDX-FileCopyrightText: 2025 Hendrik Huyskens © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Johann Wagner © Otto-von-Guericke-Universität Magdeburg -# SPDX-FileCopyrightText: 2025 Johann Wagner © Otto-von-Guericke-Universität Magdeburg -# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Kirann Bhavaraju © Otto-von-Guericke-Universität Magdeburg -# SPDX-FileCopyrightText: 2025 Ludwig Hülk © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Ludwig Hülk © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Martin Glauer © Otto-von-Guericke-Universität Magdeburg -# SPDX-FileCopyrightText: 2025 Martin Glauer © Otto-von-Guericke-Universität Magdeburg -# SPDX-FileCopyrightText: 2025 Martin Glauer © Otto-von-Guericke-Universität Magdeburg -# SPDX-FileCopyrightText: 2025 Martin Glauer © Otto-von-Guericke-Universität Magdeburg +# SPDX-FileCopyrightText: 2025 Hendrik Huyskens © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Johann Wagner © Otto-von-Guericke-Universität Magdeburg # noqa: E501 +# SPDX-FileCopyrightText: 2025 Johann Wagner © Otto-von-Guericke-Universität Magdeburg # noqa: E501 +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Kirann Bhavaraju © Otto-von-Guericke-Universität Magdeburg # noqa: E501 +# SPDX-FileCopyrightText: 2025 Ludwig Hülk © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Ludwig Hülk © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Martin Glauer © Otto-von-Guericke-Universität Magdeburg # noqa: E501 +# SPDX-FileCopyrightText: 2025 Martin Glauer © Otto-von-Guericke-Universität Magdeburg # noqa: E501 +# SPDX-FileCopyrightText: 2025 Martin Glauer © Otto-von-Guericke-Universität Magdeburg # noqa: E501 +# SPDX-FileCopyrightText: 2025 Martin Glauer © Otto-von-Guericke-Universität Magdeburg # noqa: E501 # SPDX-FileCopyrightText: 2025 Tom Heimbrodt -# SPDX-FileCopyrightText: 2025 Christian Winger © Öko-Institut e.V. -# SPDX-FileCopyrightText: 2025 Christian Hofmann © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Daryna Barabanova © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 shara © Otto-von-Guericke-Universität Magdeburg -# SPDX-FileCopyrightText: 2025 Stephan Uller © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 user © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 Christian Winger © Öko-Institut e.V. +# SPDX-FileCopyrightText: 2025 Christian Winger © Öko-Institut e.V. # noqa: E501 +# SPDX-FileCopyrightText: 2025 Christian Hofmann © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Daryna Barabanova © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 shara © Otto-von-Guericke-Universität Magdeburg # noqa: E501 +# SPDX-FileCopyrightText: 2025 Stephan Uller © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 user © Reiner Lemoine Institut # noqa: E501 +# SPDX-FileCopyrightText: 2025 Christian Winger © Öko-Institut e.V. # noqa: E501 # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -33,6 +33,7 @@ import logging import os import re +from collections import defaultdict from functools import reduce from io import TextIOWrapper from itertools import chain @@ -2014,82 +2015,261 @@ def parse_keys(self, val, old=""): def sort_in_category(self, schema, table, oemetadata): """ - Sorts the metadata of a table into categories and adds the value - suggestion and comment that were added during the review, to facilitate - Further processing easier. + Groups metadata fields by top-level categories and subgroups within them. + If a field has no dot (.), it's considered flat and shown directly. + If a field has a dot and includes an index (e.g., sources.0.name), + then all fields with the same index are grouped together and shown in order. + Adds display_prefix for human-readable (1-based) indexing. + """ + import re - Note: - The categories spatial & temporal are often combined during visualization. + def _plus_one_if_digit(txt: str) -> str: + return str(int(txt) + 1) if txt.isdigit() else txt - Args: - schema (str): The schema of the table. - table (str): The name of the table. + val = self.parse_keys(oemetadata) - Returns: + main_categories = { + "general": [], + "spatial": [], + "temporal": [], + "source": [], + "license": [], + "resource": [], + } + for item in val: + field = item["field"] + top_key = field.split(".")[0] + + if top_key in { + "name", + "title", + "id", + "description", + "language", + "subject", + "keywords", + "publicationDate", + "context", + }: + main_categories["general"].append(item) + elif top_key == "spatial": + main_categories["spatial"].append(item) + elif top_key == "temporal": + main_categories["temporal"].append(item) + elif top_key == "sources": + main_categories["source"].append(item) + elif top_key == "licenses": + main_categories["license"].append(item) + elif top_key == "resources": + main_categories["resource"].append(item) + + def extract_index(prefix): + """ + Return the numeric list index found at the end of *prefix*. + + Works for both 'sources.0' *and* 'Sources 0'. + If no trailing index exists, ``-1`` is returned to keep such + prefixes at the beginning of the ordered result. + """ + match = re.search(r"(?:\.|\s)([0-9]+)$", prefix) + return int(match.group(1)) if match else -1 + + # ------------------------------------------------------------------ + # Helper: inside each "Sources N" split into second‑level list groups + # e.g. sources..licenses.0.url → "Licenses 0" + # sources..contacts.1.name → "Contacts 1" + # Any path with pattern . after the 2‑nd segment + # becomes its own accordion; everything else stays flat. + # ------------------------------------------------------------------ + def nest_sublist_groups(source_items): + """ + Turn one Source‑block (list of dicts) into: + { + "flat": [ ... items without . ... ], + "grouped": { " 1": [...], " 2": [...], ... } + } + + Accepts arbitrary list names, not just 'licenses'. + """ + nested = {"flat": [], "grouped": defaultdict(list)} + + for itm in source_items: + parts = itm["field"].split(".") + # expect pattern: sources.... + if ( + len(parts) >= 4 + and parts[2].isidentifier() # list name + and parts[3].isdigit() # index + ): + list_name = parts[2] # e.g. "licenses" + idx = parts[3] # e.g. "0" + display_idx = int(idx) + 1 + group_key = f"{list_name.capitalize()} {display_idx}" + display_field = ".".join(parts[4:]) or "value" + + enriched = itm.copy() + enriched["display_field"] = display_field + enriched["display_prefix"] = group_key + enriched["display_index"] = idx + nested["grouped"][group_key].append(enriched) + else: + # keep as flat inside this Source block + trimmed = ".".join(parts[2:]) if len(parts) > 2 else itm["field"] + enriched = itm.copy() + enriched["display_field"] = _plus_one_if_digit(trimmed) + nested["flat"].append(enriched) + + # sort groups numerically (… 1, 2, 3 …) within each list name + nested["grouped"] = dict( + sorted( + nested["grouped"].items(), + key=lambda kv: ( + kv[0].split()[0], # list name + int(kv[0].split()[-1]), # numeric index + ), + ) + ) + return nested + + def group_index_only(items): + result = {"flat": [], "grouped": defaultdict(list)} + + for item in items: + field = item["field"] + parts = field.split(".") + + list_idx = None + list_name = None + idx_pos = None + for pos in range(1, len(parts)): + if parts[pos].isdigit(): + list_idx = parts[pos] # '0' + list_name = parts[pos - 1] # 'timeseries' + idx_pos = pos + break - Examples: - A return value can look like the below dictionary: + if list_idx is not None: + # «Timeseries 1», «Bbox 2» … + display_idx = int(list_idx) + 1 # show 1‑based index + group_key = f"{list_name.capitalize()} {display_idx}" + display_field = ( + ".".join(parts[idx_pos + 1 :]) + if idx_pos + 1 < len(parts) + else "" + ) - >>> - { - "general": [ - { - "field": "id", - "value": "http: //127.0.0.1:8000/dataedit/view/model_draft/test2", - "newValue": "", - "reviewer_suggestion": "", - "suggestion_comment": "" - } - ], - "spatial": [...], - "temporal": [...], - "source": [...], - "license": [...], - } + enriched = item.copy() + enriched["display_field"] = display_field + enriched["display_prefix"] = group_key + enriched["display_index"] = list_idx + result["grouped"][group_key].append(enriched) + else: + trimmed = field.split(".", 1)[1] if "." in field else field + item["display_field"] = _plus_one_if_digit(trimmed) + item.pop("display_index", None) + result["flat"].append(item) - """ + result["grouped"] = dict( + sorted(result["grouped"].items(), key=lambda kv: int(kv[0].split()[-1])) + ) + return result + + def group_by_index(items): + """ + Organise *items* into + * ``flat`` – fields without any nesting, + * ``grouped`` – dict whose keys are human‑readable list titles + such as 'Timeseries 1', 'Sources 2', … + + All fields that share the same list index (e.g. timeseries.0.*) + are collected under one group. The groups are ordered by their + numeric index so that 1, 2, 3 … appear in sequence. + """ + result = {"flat": [], "grouped": defaultdict(list)} + + for item in items: + field = item["field"] + parts = field.split(".") + + # Handle list elements like timeseries.0.start + if len(parts) >= 3 and parts[1].isdigit(): + index = parts[1] # '0' + display_idx = int(index) + 1 + group_key = ( + f"{parts[0].capitalize()} {display_idx}" # 'Timeseries 1' + ) + display_field = ".".join(parts[2:]) # 'start' - val = self.parse_keys(oemetadata) - gen_key_list = [] - spatial_key_list = [] - temporal_key_list = [] - source_key_list = [] - license_key_list = [] - - for i in val: - fieldKey = list(i.values())[0] - if fieldKey.split(".")[0] == "spatial": - spatial_key_list.append(i) - elif fieldKey.split(".")[0] == "temporal": - temporal_key_list.append(i) - elif fieldKey.split(".")[0] == "sources": - source_key_list.append(i) - elif fieldKey.split(".")[0] == "licenses": - license_key_list.append(i) - - elif ( - fieldKey.split(".")[0] == "name" - or fieldKey.split(".")[0] == "title" - or fieldKey.split(".")[0] == "id" - or fieldKey.split(".")[0] == "description" - or fieldKey.split(".")[0] == "language" - or fieldKey.split(".")[0] == "subject" - or fieldKey.split(".")[0] == "keywords" - or fieldKey.split(".")[0] == "publicationDate" - or fieldKey.split(".")[0] == "context" - ): - gen_key_list.append(i) - - meta = { - "general": gen_key_list, - "spatial": spatial_key_list, - "temporal": temporal_key_list, - "source": source_key_list, - "license": license_key_list, - } + enriched = item.copy() + enriched["display_field"] = display_field + enriched["display_prefix"] = group_key + enriched["display_index"] = index + + result["grouped"][group_key].append(enriched) + + # Handle nested (but non‑list) structures, e.g. spatial.epsg + elif "." in field: + group_key = field.split(".")[0] # 'spatial' + enriched = item.copy() + raw_tail = ".".join(field.split(".")[1:]) + enriched["display_field"] = _plus_one_if_digit(raw_tail) + enriched["display_prefix"] = group_key + enriched.pop("display_index", None) + + result["grouped"][group_key].append(enriched) + + # Handle completely flat fields + else: + item["display_field"] = field + item["display_field"] = _plus_one_if_digit(item["display_field"]) + item.pop("display_index", None) + result["flat"].append(item) + + # Sort grouped entries by their numeric index (Timeseries 1, 2, 3 …) + sorted_grouped = dict( + sorted(result["grouped"].items(), key=lambda kv: extract_index(kv[0])) + ) + return {"flat": result["flat"], "grouped": sorted_grouped} + + grouped_meta = {} + for cat, items in main_categories.items(): + if cat in {"spatial", "temporal"}: + grouped = group_index_only(items) # only list‑index grouping + elif cat == "source": + # First‑level grouping: Source 0, Source 1, … + src_level = group_index_only(items) + + # For every 'Sources N' list build inner sublist groups + nested_grouped = {} + for src_key, src_items in src_level["grouped"].items(): + nested_grouped[src_key] = nest_sublist_groups(src_items) + + grouped = { + "flat": src_level["flat"], + "grouped": nested_grouped, + } + elif cat == "license": + # First‑level grouping: License 0, License 1, … + lic_level = group_index_only(items) + + # For every 'Licenses N' entry build inner sub‑list groups + nested_grouped_lic = {} + for lic_key, lic_items in lic_level["grouped"].items(): + nested_grouped_lic[lic_key] = nest_sublist_groups(lic_items) + + grouped = { + "flat": lic_level["flat"], + "grouped": nested_grouped_lic, + } + else: + grouped = group_by_index(items) # previous behaviour + grouped_meta[cat] = { + "flat": grouped["flat"], + "grouped": grouped["grouped"], + } - return meta + return grouped_meta def get_all_field_descriptions(self, json_schema, prefix=""): """