diff --git a/dataedit/helper.py b/dataedit/helper.py index 3fcabf5ba..0f6bc6d4e 100644 --- a/dataedit/helper.py +++ b/dataedit/helper.py @@ -1,9 +1,15 @@ -# SPDX-FileCopyrightText: 2025 Christian Winger © Öko-Institut e.V. -# 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 Jonas Huber © Reiner Lemoine Institut -# SPDX-FileCopyrightText: 2025 user © Reiner Lemoine Institut +# SPDX-FileCopyrightText: 2025 Christian Winger +# © Öko-Institut e.V. +# 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 Jonas Huber +# © Reiner Lemoine Institut +# SPDX-FileCopyrightText: 2025 user +# © Reiner Lemoine Institut # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -85,9 +91,9 @@ def merge_field_reviews(current_json, new_json): Note: If the same key is present in both the contributor's and - reviewer's reviews, the function will merge the field - evaluations. Otherwise, it will create a new entry in - the Review-Dict. + reviewer's reviews, the function will merge the field + evaluations. Otherwise, it will create a new entry in + the Review-Dict. """ merged_json = new_json.copy() review_dict = {} @@ -124,18 +130,7 @@ def merge_field_reviews(current_json, new_json): def get_review_for_key(key, review_data): - """ - Retrieve the review for a specific key from the review data. - - Args: - key (str): The key for which to retrieve the review. - review_data (dict): The review data containing - reviews for various keys. - - Returns: - Any: The new value associated with the specified key - in the review data, or None if the key is not found. - """ + """Retrieve the review for a specific key from the review data.""" for review in review_data["reviewData"]["reviews"]: if review["key"] == key: @@ -144,31 +139,13 @@ def get_review_for_key(key, review_data): def recursive_update(metadata, review_data): - """ - Recursively updates metadata with new values from review_data, - skipping or removing fields with status 'rejected'. + """Recursively updates metadata with new values from review_data. - Args: - metadata (dict): The original metadata dictionary to update. - review_data (dict): The review data containing the new values - for various keys. - - Note: - The function iterates through the review data and for each key - updates the corresponding value in metadata if the new value is - present and is not an empty string, and if the field status is - not 'rejected'. + Skips or removes fields with status 'rejected'. """ def delete_nested_field(data, keys): - """ - Removes a nested field from a dictionary based on a list of keys. - - Args: - data (dict or list): The dictionary or list from which - to remove the field. - keys (list): A list of keys pointing to the field to remove. - """ + """Removes a nested field from a dictionary based on a list of keys.""" for key in keys[:-1]: if isinstance(data, list): key = int(key) @@ -220,18 +197,7 @@ def delete_nested_field(data, keys): def set_nested_value(metadata, keys, value): - """ - Set a nested value in a dictionary given a sequence of keys. - - Args: - metadata (dict): The dictionary in which to set the value. - keys (list): A list of keys representing the path to the nested value. - value (Any): The value to set. - - Note: - The function navigates through the dictionary using the keys - and sets the value at the position indicated by the last key in the list. - """ + """Set a nested value in a dictionary given a sequence of keys.""" for key in keys[:-1]: if key.isdigit(): @@ -244,73 +210,93 @@ def set_nested_value(metadata, keys, value): def process_review_data(review_data, metadata, categories): - state_dict = {} + """Attach reviewer fields (suggestions/comments/newValue) to metadata items. - # Initialize fields - for category in categories: - for item in metadata[category]: - item["reviewer_suggestion"] = "" - item["suggestion_comment"] = "" - item["additional_comment"] = "" - item["newValue"] = "" + The `metadata[category]` structures may be nested (dict/list) and can contain + non-dict leaf values (e.g. strings). We therefore walk the structure + recursively and only mutate leaf dicts that represent a field item. + """ + + state_dict: dict[str, str | None] = {} - for review in review_data: + def iter_field_items(node): + """Yield all leaf field-item dicts inside nested list/dict structures. + + A leaf field item is a dict with a string key `field`. + """ + if isinstance(node, list): + for el in node: + yield from iter_field_items(el) + elif isinstance(node, dict): + if isinstance(node.get("field"), str): + yield node + else: + for v in node.values(): + yield from iter_field_items(v) + # Ignore other node types (e.g. str/int/None) + + # Initialize fields safely + for category in categories: + cat_node = metadata.get(category) + if cat_node is None: + continue + for item in iter_field_items(cat_node): + item.setdefault("reviewer_suggestion", "") + item.setdefault("suggestion_comment", "") + item.setdefault("additional_comment", "") + item.setdefault("newValue", "") + + # Apply review values + for review in review_data or []: field_key = review.get("key") field_review = review.get("fieldReview") - category = review.get("category") # Get the category from the review + category = review.get("category") + + state = None + reviewer_suggestion = "" + reviewer_suggestion_comment = "" + newValue = "" + additional_comment = "" if isinstance(field_review, list): - # Sort and get the latest field review sorted_field_review = sorted( - field_review, key=lambda x: x.get("timestamp"), reverse=True - ) - latest_field_review = ( - sorted_field_review[0] if sorted_field_review else None + field_review, + key=lambda x: (x.get("timestamp") or 0) if isinstance(x, dict) else 0, + reverse=True, ) + latest = sorted_field_review[0] if sorted_field_review else None + if isinstance(latest, dict): + state = latest.get("state") + reviewer_suggestion = latest.get("reviewerSuggestion") or "" + reviewer_suggestion_comment = latest.get("comment") or "" + newValue = latest.get("newValue") or "" + additional_comment = latest.get("additionalComment") or "" - if latest_field_review: - state = latest_field_review.get("state") - reviewer_suggestion = latest_field_review.get("reviewerSuggestion") - reviewer_suggestion_comment = latest_field_review.get("comment") - newValue = latest_field_review.get("newValue") - additional_comment = latest_field_review.get("additionalComment") - else: - state = None - reviewer_suggestion = "" - reviewer_suggestion_comment = "" - newValue = "" - additional_comment = "" - else: + elif isinstance(field_review, dict): state = field_review.get("state") - reviewer_suggestion = field_review.get("reviewerSuggestion") - reviewer_suggestion_comment = field_review.get("comment") - newValue = field_review.get("newValue") - additional_comment = field_review.get("additionalComment") - - # Update the item in the correct category - if category in metadata: - for item in metadata[category]: - if item["field"] == field_key: - item["reviewer_suggestion"] = reviewer_suggestion or "" - item["suggestion_comment"] = reviewer_suggestion_comment or "" - item["additional_comment"] = additional_comment or "" - item["newValue"] = newValue or "" + reviewer_suggestion = field_review.get("reviewerSuggestion") or "" + reviewer_suggestion_comment = field_review.get("comment") or "" + newValue = field_review.get("newValue") or "" + additional_comment = field_review.get("additionalComment") or "" + + # Update the matching item in metadata for this category + if category in metadata and field_key: + for item in iter_field_items(metadata.get(category)): + if item.get("field") == field_key: + item["reviewer_suggestion"] = reviewer_suggestion + item["suggestion_comment"] = reviewer_suggestion_comment + item["additional_comment"] = additional_comment + item["newValue"] = newValue break - state_dict[field_key] = state + if field_key: + state_dict[field_key] = state return state_dict def delete_peer_review(review_id): - """ - Remove Peer Review by review_id. - Args: - review_id (int): ID review. - - Returns: - JsonResponse: JSON response about successful deletion or error. - """ + """Remove Peer Review by review_id.""" if review_id: peer_review = PeerReview.objects.filter(id=review_id).first() if peer_review: diff --git a/dataedit/static/peer_review/main.js b/dataedit/static/peer_review/main.js index 43fc7e653..f006ffcd7 100644 --- a/dataedit/static/peer_review/main.js +++ b/dataedit/static/peer_review/main.js @@ -12,7 +12,18 @@ import { selectPreviousField } from './navigation.js' window.selectPreviousField = selectPreviousField; import { setGetFieldState } from './state_current_review.js'; -import './opr_reviewer.js'; +// Load only the role-specific bundle for the current page. +// Templates set:
+const oprPage = document.getElementById('opr-page-marker')?.dataset?.oprPage; + +if (oprPage === 'reviewer') { + await import('./opr_reviewer.js'); +} else if (oprPage === 'contributor') { + await import('./opr_contributor.js'); +} else { + console.warn('OPR page marker not found; skipping role-specific bundle'); +} + setGetFieldState((fieldKey) => { return window.state_dict?.[fieldKey] ?? null; }); diff --git a/dataedit/static/peer_review/opr_contributor.js b/dataedit/static/peer_review/opr_contributor.js index 186f6c683..b924cead8 100644 --- a/dataedit/static/peer_review/opr_contributor.js +++ b/dataedit/static/peer_review/opr_contributor.js @@ -10,7 +10,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import * as common from './peer_review.js'; import { hideReviewerOptions, setSelectedField, @@ -22,22 +21,69 @@ import { selectedCategory, setSelectedCategory, checkReviewComplete, - showToast, updateFieldDescription, - highlightSelectedField, initializeEventBindings, + highlightSelectedField, + initializeEventBindings, + selectState, } from './peer_review.js'; + +// expose selectState for any legacy inline handlers +window.selectState = selectState; import {selectNextField, switchCategoryTab} from "./navigation.js"; import {getFieldState, setGetFieldState} from "./state_current_review.js"; -import {updateFieldColor} from "./utilities.js"; -import {renderSummaryPageFields, updateTabProgressIndicatorClasses} from "./summary.js"; -window.selectState = common.selectState; -var selectedField; +import {isEmptyValue, updateFieldColor} from "./utilities.js"; +import {updateTabProgressIndicatorClasses} from "./summary.js"; + +let selectedField = null; +let actionsEnabled = false; + +// Track whether the contributor has already interacted with a field (local UI state) +const fieldEvaluations = {}; + // OK Field View Change $('#button').bind('click', hideReviewerOptions); +// Resolve the reviewer decision for a field (independent from later contributor actions) +function getReviewerState(fieldKey) { + // Prefer the backend-computed state dict (represents reviewer outcome) + if (window.state_dict && Object.prototype.hasOwnProperty.call(window.state_dict, fieldKey)) { + return window.state_dict[fieldKey]; + } + + // Fallback: derive from current_review by picking the latest entry with role === 'reviewer' + try { + const reviews = current_review?.reviews; + if (!Array.isArray(reviews)) return undefined; + + const review = reviews.find((r) => r && r.key === fieldKey); + if (!review || !review.fieldReview) return undefined; + + const frArr = Array.isArray(review.fieldReview) ? review.fieldReview : [review.fieldReview]; + const reviewerEntries = frArr.filter((x) => x && x.role === 'reviewer'); + const pickFrom = reviewerEntries.length ? reviewerEntries : frArr; + + const latest = pickFrom + .slice() + .sort((a, b) => (b?.timestamp ?? 0) - (a?.timestamp ?? 0))[0]; + + return latest?.state; + } catch (_e) { + return undefined; + } +} + function click_field(fieldKey, fieldValue, category) { + // Keep the original value (may be null/undefined if the template does not provide it) + const rawFieldValue = fieldValue; + + // Ensure we always work with a string value for the UI + const fieldValueStr = (fieldValue ?? '').toString(); + + // Reset UI first; some helpers may toggle/disable controls + clearInputFields(); + hideReviewerOptions(); const cleanedFieldKey = fieldKey.replace(/\.\d+/g, ''); @@ -45,42 +91,119 @@ function click_field(fieldKey, fieldValue, category) { setSelectedField(fieldKey); - setselectedFieldValue(fieldValue); + // Keep a local copy for this module (peer_review.js stores the canonical value on window.selectedField) + selectedField = fieldKey; + + setselectedFieldValue(fieldValueStr); setSelectedCategory(category); - updateFieldDescription(cleanedFieldKey, fieldValue); + updateFieldDescription(cleanedFieldKey, fieldValueStr); highlightSelectedField(fieldKey); - const fieldState = getFieldState(fieldKey); + const fieldState = getReviewerState(fieldKey); + const fieldWasEvaluated = !!fieldEvaluations[fieldKey]; - if (fieldState === 'ok' || !fieldState || fieldState === 'rejected') { - ["ok-button", "rejected-button"].forEach(btn => { - document.getElementById(btn).disabled = true; - }); - } else if (fieldState === 'suggestion') { - ["ok-button", "rejected-button"].forEach(btn => { - document.getElementById(btn).disabled = false; - }); + // IMPORTANT: Do NOT disable buttons just because we couldn't read the value from the DOM. + // Some fields don't have `.value` / `data-fieldvalue` in the list, but are still reviewable. + const isEmpty = (rawFieldValue !== null && rawFieldValue !== undefined) + ? isEmptyValue(fieldValueStr) + : false; + + const okBtn = document.getElementById('ok-button'); + const suggestionBtn = document.getElementById('suggestion-button'); + const rejectedBtn = document.getElementById('rejected-button'); + + let enableActions = false; + + if (fieldState) { + // Contributor rule: buttons active ONLY for reviewer suggestion fields. + if (fieldState === 'suggestion' || fieldState === 'suggest') { + enableActions = !isEmpty; + } else if (fieldState === 'ok' && !fieldWasEvaluated) { + enableActions = false; + } else { + enableActions = false; + } } else { - ["ok-button", "rejected-button", "suggestion-button"].forEach(btn => { - document.getElementById(btn).disabled = false; - }); + // If there is no reviewer state, keep buttons disabled. + enableActions = false; } - clearInputFields(); - hideReviewerOptions(); + actionsEnabled = enableActions; + + [okBtn, suggestionBtn, rejectedBtn].forEach((btn) => { + const buttonEl = btn; + if (buttonEl) { + buttonEl.disabled = !enableActions; + } + }); } document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('.field').forEach((field) => { + const fieldKey = field.getAttribute('data-fieldkey'); + const reviewerState = getReviewerState(fieldKey); + + // Optional visual hint (still clickable) + if (reviewerState !== 'suggestion') { + field.classList.add('field-locked'); + field.setAttribute('aria-disabled', 'true'); + // DO NOT disable pointer events; contributors should be able to open/view all fields. + } + field.addEventListener('click', () => { - const fieldKey = field.getAttribute('data-fieldkey'); - const fieldValue = field.getAttribute('data-fieldvalue'); - const category = field.getAttribute('data-category'); + // Some templates do not provide data-fieldvalue; fall back to rendered DOM text + const fieldValueAttr = field.getAttribute('data-fieldvalue'); + const fieldValueDom = + field.querySelector('.value, .field-value, .field__value, .field__content, .field-content')?.textContent; + + // Allow null if we can't reliably extract a value from the field list DOM + const fieldValue = fieldValueAttr ?? (fieldValueDom != null ? fieldValueDom.trim() : null); + + // category is usually present; fall back to the parent tab pane id + const categoryAttr = field.getAttribute('data-category'); + const categoryDom = field.closest('.tab-pane')?.id; + const category = (categoryAttr ?? categoryDom ?? 'general').toString(); click_field(fieldKey, fieldValue, category); }); }); + + // Bind action buttons once. They are enabled/disabled per-field via `actionsEnabled`. + const okBtn = document.getElementById('ok-button'); + const suggestionBtn = document.getElementById('suggestion-button'); + const rejectedBtn = document.getElementById('rejected-button'); + + const bindAction = (btn, state) => { + if (!btn) return; + btn.addEventListener( + 'click', + (e) => { + if (!actionsEnabled) { + // Keep UI inert for non-suggestion fields + e.preventDefault(); + e.stopPropagation(); + return; + } + + const selectedKey = window.selectedField || selectedField; + if (selectedKey) { + fieldEvaluations[selectedKey] = state; + } + + if (typeof selectState === 'function') { + selectState(state); + } else if (typeof window.selectState === 'function') { + window.selectState(state); + } + }, + true + ); + }; + + bindAction(okBtn, 'ok'); + bindAction(rejectedBtn, 'rejected'); + bindAction(suggestionBtn, 'suggestion'); }); /** * Saves selected state @@ -88,63 +211,165 @@ document.addEventListener('DOMContentLoaded', function() { */ export function getFieldStateForContributor(fieldKey) { - // This function gets the state of a field - return state_dict[fieldKey]; + // 1) Preferred: server-provided state_dict (if attached to window) + if (window.state_dict && Object.prototype.hasOwnProperty.call(window.state_dict, fieldKey)) { + return window.state_dict[fieldKey]; + } - function selectState(state) { // eslint-disable-line no-unused-vars - selectedState = state; + // 2) Fallback: derive from current_review (works even if state_dict is not on window) + try { + const reviews = current_review?.reviews; + if (!Array.isArray(reviews)) return undefined; + + const review = reviews.find((r) => r && r.key === fieldKey); + if (!review || !review.fieldReview) return undefined; + + // fieldReview can be an object or a list of objects + const fr = review.fieldReview; + const latest = Array.isArray(fr) + ? fr + .slice() + .sort((a, b) => (b?.timestamp ?? 0) - (a?.timestamp ?? 0))[0] + : fr; + + return latest?.state; + } catch (_e) { + return undefined; + } } + /** * Renders fields on the Summary page, sorted by review state */ /** * Displays fields based on selected category */ - function renderSummaryPageFields() { - const categoriesMap = {}; + const acceptedFields = []; + const suggestingFields = []; + const rejectedFields = []; + const missingFields = []; + const emptyFields = []; - function addFieldToCategory(category, field) { - if (!categoriesMap[category]) categoriesMap[category] = []; - categoriesMap[category].push(field); + const processedFields = new Set(); - // Removed incorrect use of 'continue' and refactored loop variable declaration - const category_fields = category.querySelectorAll(".field"); - for (const field of category_fields) { + if (window.state_dict && Object.keys(window.state_dict).length > 0) { + const fields = document.querySelectorAll('.field'); + for (let field of fields) { const field_id = field.id.slice(6); const fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); - const found = current_review.reviews.some((review) => review.key === field_id); const fieldState = getFieldState(field_id); const fieldCategory = field.getAttribute('data-category'); + const fieldSuggestion = field.querySelector('.suggestion.suggestion--highlight')?.textContent.trim() || ""; + + // remove the numbers and replace the dots with spaces let fieldName = field_id.replace(/\./g, ' '); + + if (fieldCategory !== "general") { + fieldName = fieldName.split(' ').slice(1).join(' '); // remove first word + } + const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`; - if (isEmptyValue(fieldValue) && !processedFields.has(uniqueFieldIdentifier)) { - emptyFields.push({ fieldName, fieldValue, fieldCategory: "emptyFields", fieldSuggestion }); - } else if (!found && fieldState !== 'ok' && fieldState !== 'rejected' && !isEmptyValue(fieldValue)) { - missingFields.push({ fieldName, fieldValue, fieldCategory }); + if (isEmptyValue(fieldValue)) { + emptyFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); + } else if (fieldState === 'ok') { + acceptedFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); processedFields.add(uniqueFieldIdentifier); } } } - const fields = document.querySelectorAll('.field'); - fields.forEach(field => { - const field_id = field.id.slice(6); - const fieldValue = $(field).find('.value').text().trim(); - const fieldState = getFieldState(field_id); - const fieldCategory = field.getAttribute('data-category'); - let fieldName = field_id.replace(/\./g, ' '); + for (const review of current_review.reviews) { + const fieldDomId = `field_${review.key}`; + const fieldEl = document.getElementById(fieldDomId); + const fieldValue = fieldEl + ? $(fieldEl).find('.value').text().replace(/\s+/g, ' ').trim() + : ''; + const fieldState = review.fieldReview.state; + const fieldCategory = review.category; + const fieldSuggestion = review.fieldReview.reviewerSuggestion || ""; + + let fieldName = review.key.replace(/\./g, ' '); + if (fieldCategory !== "general") { fieldName = fieldName.split(' ').slice(1).join(' '); } - let fieldStatus = isEmptyValue(fieldValue) ? 'Empty' : - fieldState === 'ok' ? 'Accepted' : - fieldState === 'rejected' ? 'Rejected' : 'Missing'; + const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`; + + if (processedFields.has(uniqueFieldIdentifier)) { + continue; + } + + if (isEmptyValue(fieldValue)) { + emptyFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); + } else if (fieldState === 'ok') { + acceptedFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); + } else if (fieldState === 'suggestion') { + suggestingFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); + } else if (fieldState === 'rejected') { + rejectedFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); + } + + processedFields.add(uniqueFieldIdentifier); + } + + const categories = document.querySelectorAll(".tab-pane"); + + for (const category of categories) { + const category_name = category.id; + + if (category_name === "summary") { + continue; + } + const category_fields = category.querySelectorAll(".field"); + for (let field of category_fields) { + const field_id = field.id.slice(6); + const fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); + const found = current_review.reviews.some((review) => review.key === field_id); + const fieldState = getFieldState(field_id); + const fieldCategory = field.getAttribute('data-category'); + const fieldSuggestion = field.querySelector('.suggestion.suggestion--highlight')?.textContent.trim() || ""; + + let fieldName = field_id.replace(/\./g, ' '); + + if (fieldCategory !== "general") { + fieldName = fieldName.split(' ').slice(1).join(' '); + } + + const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`; + + if ( + !found && + fieldState !== 'ok' && + !isEmptyValue(fieldValue) && + !processedFields.has(uniqueFieldIdentifier) + ) { + missingFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); + processedFields.add(uniqueFieldIdentifier); + } + } + } + + const allData = []; + allData.push(...missingFields.map((item) => ({ ...item, fieldStatus: 'Missing' }))); + allData.push(...acceptedFields.map((item) => ({ ...item, fieldStatus: 'Accepted' }))); + allData.push(...suggestingFields.map((item) => ({ ...item, fieldStatus: 'Suggested' }))); + allData.push(...rejectedFields.map((item) => ({ ...item, fieldStatus: 'Rejected' }))); + allData.push(...emptyFields.map((item) => ({ ...item, fieldStatus: 'Empty' }))); + + const categoriesMap = {}; - addFieldToCategory(fieldCategory, { fieldName, fieldValue, fieldStatus }); + function addFieldToCategory(category, field) { + if (!categoriesMap[category]) categoriesMap[category] = []; + categoriesMap[category].push(field); + } + + allData.forEach(item => { + const category = item.fieldCategory || 'general'; + addFieldToCategory(category, item); }); const summaryContainer = document.getElementById("summary"); @@ -163,18 +388,22 @@ function renderSummaryPageFields() { const navItem = document.createElement('li'); navItem.className = 'nav-item'; - navItem.innerHTML = ``; + navItem.innerHTML = ` + + `; tabsNav.appendChild(navItem); const tabPane = document.createElement('div'); tabPane.className = `tab-pane fade${firstTab ? ' show active' : ''}`; tabPane.id = tabId; - const fields = categoriesMap[category]; + const fieldsForCategory = categoriesMap[category]; const singleFields = []; const groupedFields = {}; - fields.forEach(field => { + fieldsForCategory.forEach(field => { const words = field.fieldName.split(' '); if (words.length === 1) { singleFields.push(field); @@ -193,28 +422,32 @@ function renderSummaryPageFields() { } else { groupedFields[prefix].noIndex.push({ ...field, fieldName: nameWithoutIndices }); } - - let th = document.createElement('th'); - th.scope = "row"; - th.className = "status"; - if (item.fieldStatus === "Pending") { - th.className = "status missing"; - } - } // <-- Add closing brace here for else block - }); // <-- Closing fields.forEach + } + }); if (singleFields.length > 0) { const table = document.createElement('table'); table.className = 'table review-summary'; table.innerHTML = ` - StatusField NameField Value - ${singleFields.map(f => ` + - ${f.fieldStatus} - ${f.fieldName} - ${f.fieldValue} - `).join('')} - `; + Status + Field Name + Field Value + Field Suggestion + + + + ${singleFields.map(f => ` + + ${f.fieldStatus} + ${f.fieldName} + ${f.fieldValue} + ${f.fieldSuggestion || ''} + + `).join('')} + + `; tabPane.appendChild(table); } @@ -237,15 +470,26 @@ function renderSummaryPageFields() { if (noIndex.length > 0) { innerHTML += ` - - ${noIndex.map(f => ` + - - - - `).join('')} + + + + + + + + ${noIndex.map(f => ` + + + + + + + `).join('')} -
StatusField NameField Value
${f.fieldStatus}${f.fieldName}${f.fieldValue}
StatusField NameField ValueField Suggestion
${f.fieldStatus}${f.fieldName}${f.fieldValue}${f.fieldSuggestion || ''}
`; + + `; } if (Object.keys(indexed).length > 0) { @@ -268,62 +512,102 @@ function renderSummaryPageFields() {
- - ${idxFields.map(f => ` + - - - - `).join('')} + + + + + + + + ${idxFields.map(f => ` + + + + + + + `).join('')}
StatusField NameField Value
${f.fieldStatus}${f.fieldName}${f.fieldValue}
StatusField NameField ValueField Suggestion
${f.fieldStatus}${f.fieldName}${f.fieldValue}${f.fieldSuggestion || ''}
-
`; + + `; }); innerHTML += ``; } + accordionItem.innerHTML = ` +

+ +

+
+
+ ${innerHTML} +
+
+ `; + + accordionContainer.appendChild(accordionItem); + accordionIndex++; + } + + tabPane.appendChild(accordionContainer); + } + tabsContent.appendChild(tabPane); firstTab = false; } + const viewsNavItem = document.createElement('li'); viewsNavItem.className = 'nav-item'; - viewsNavItem.innerHTML = ''; - - + viewsNavItem.innerHTML = ` + + `; tabsNav.appendChild(viewsNavItem); const viewsPane = document.createElement('div'); viewsPane.className = 'tab-pane fade'; viewsPane.id = 'tab-views'; - const allFields = Object.entries(categoriesMap).flatMap(([category, fields]) => - fields.map(f => ({...f, category})) - ); - - viewsPane.innerHTML = + viewsPane.innerHTML = ` - - - ${allFields.map(f => - - - - - ).join('')} + + + + + + + + + ${allData.map(f => ` + + + + + + + + `).join('')} -
StatusCategoryField NameField Value
${f.fieldStatus}${f.category}${f.fieldName}${f.fieldValue}
StatusCategoryField NameField ValueField Suggestion
${f.fieldStatus}${f.fieldCategory}${f.fieldName}${f.fieldValue}${f.fieldSuggestion || ''}
; + + `; tabsContent.appendChild(viewsPane); summaryContainer.appendChild(tabsNav); summaryContainer.appendChild(tabsContent); + updateTabProgressIndicatorClasses(); } - /** * Creates an HTML list of fields with their categories * @param {Array} fields Array of field objects @@ -367,13 +651,12 @@ function showToast(title, message, type) { setGetFieldState(getFieldStateForContributor); function saveEntrancesForContributor() { + const selectedKey = window.selectedField || selectedField; if (selectedState !== "ok" && selectedState !== "rejected") { // Get the valuearea element const valuearea = document.getElementById('valuearea'); - // const validityState = valuearea.validity; - // Validate the valuearea before proceeding if (valuearea.value.trim() === '') { valuearea.setCustomValidity('Value suggestion is required'); @@ -384,30 +667,28 @@ function saveEntrancesForContributor() { } valuearea.reportValidity(); - } else if (initialReviewerSuggestions[selectedField]) { // Check if the state is "ok" and if there's a valid suggestion - var fieldElement = document.getElementById("field_" + selectedField); + } else if (initialReviewerSuggestions[selectedKey]) { // Check if the state is "ok" and if there's a valid suggestion + var fieldElement = document.getElementById("field_" + selectedKey); if (fieldElement) { var valueElement = fieldElement.querySelector('.value'); if (valueElement) { - valueElement.innerText = initialReviewerSuggestions[selectedField]; + valueElement.innerText = initialReviewerSuggestions[selectedKey]; } } } - if (Object.keys(current_review["reviews"]).length === 0 && current_review["reviews"].constructor === Object) { current_review["reviews"] = []; } - if (selectedField) { + if (selectedKey) { var reviewFound = false; for (let i = 0; i < current_review["reviews"].length; i++) { - if (current_review["reviews"][i]["key"] === selectedField) { + if (current_review["reviews"][i]["key"] === selectedKey) { reviewFound = true; - // console.log("review" + current_review.reviews["reviews"][i]["fieldReview"]) //undefined "reviews" - console.log("review" + current_review["reviews"][i]["fieldReview"]) //undefined "reviews" + console.log("review" + current_review["reviews"][i]["fieldReview"]); if (!Array.isArray(current_review["reviews"][i]["fieldReview"])) { current_review["reviews"][i]["fieldReview"] = [current_review["reviews"][i]["fieldReview"]]; } @@ -418,14 +699,14 @@ function saveEntrancesForContributor() { "user": "oep_contributor", // TODO put actual username "role": "contributor", "contributorValue": selectedFieldValue, - "newValue": selectedState === "ok" ? initialReviewerSuggestions[selectedField] : "", + "newValue": selectedState === "ok" ? initialReviewerSuggestions[selectedKey] : "", "comment": document.getElementById("commentarea").value, "additionalComment": document.getElementById("comments").value, "reviewerSuggestion": document.getElementById("valuearea").value, "state": selectedState, }); // Aktualisiere die HTML-Elemente mit den eingegebenen Werten - var fieldElement = document.getElementById("field_" + selectedField); + var fieldElement = document.getElementById("field_" + selectedKey); var suggestionElement = fieldElement.querySelector('.suggestion--highlight'); var commentElement = fieldElement.querySelector('.suggestion--comment'); // var additionalCommentElement = fieldElement.querySelector('.suggestion--additional-comment'); @@ -441,14 +722,14 @@ function saveEntrancesForContributor() { var category = element.getAttribute("data-bs-target"); current_review["reviews"].push({ "category": selectedCategory, - "key": selectedField, + "key": selectedKey, "fieldReview": [ { "timestamp": Date.now(), "user": "oep_contributor", // TODO put actual username "role": "contributor", "contributorValue": selectedFieldValue, - "newValue": selectedState === "ok" ? initialReviewerSuggestions[selectedField] : "", + "newValue": selectedState === "ok" ? initialReviewerSuggestions[selectedKey] : "", "comment": document.getElementById("commentarea").value, "additionalComment": document.getElementById("comments").value, "reviewerSuggestion": document.getElementById("valuearea").value, @@ -457,7 +738,7 @@ function saveEntrancesForContributor() { ], }); // Aktualisiere die HTML-Elemente mit den eingegebenen Werten - var fieldElement = document.getElementById("field_" + selectedField); + var fieldElement = document.getElementById("field_" + selectedKey); var suggestionElement = fieldElement.querySelector('.suggestion--highlight'); var commentElement = fieldElement.querySelector('.suggestion--comment'); var additionalCommentElement = fieldElement.querySelector('.suggestion--additional-comment'); // For new comment @@ -465,10 +746,9 @@ function saveEntrancesForContributor() { suggestionElement.innerText = document.getElementById("valuearea").value; commentElement.innerText = document.getElementById("commentarea").value; additionalCommentElement.innerText = document.getElementById("comments").value; // Update new comment - } } - document.getElementById("comments").value = ""; + document.getElementById("comments").value = ""; updateFieldColor(); checkReviewComplete(); selectNextField(); @@ -476,5 +756,59 @@ function saveEntrancesForContributor() { updateTabProgressIndicatorClasses(); } initializeEventBindings(saveEntrancesForContributor); -}}} +function calculateOkPercentage(stateDict) { + let totalCount = 0; + let okCount = 0; + + if (!stateDict) { + return "0.00"; + } + + for (let key in stateDict) { + const fieldElement = document.getElementById(`field_${key}`); + if (!fieldElement) continue; + + const fieldValue = $(fieldElement).find('.value').text().replace(/\s+/g, ' ').trim(); + if (!isEmptyValue(fieldValue)) { + totalCount++; + if (stateDict[key] === "ok") { + okCount++; + } + } + } + + const percentage = totalCount === 0 ? 0 : (okCount / totalCount) * 100; + return percentage.toFixed(2); +} + +function updatePercentageDisplay() { + if (!window.state_dict) return; + + const percentage = parseFloat(calculateOkPercentage(window.state_dict)); + + // Circle elements + const circle = document.getElementById("okProgressCircle"); + const textEl = + document.getElementById("okPercentageText") || + document.getElementById("percentageDisplay"); + + if (circle) { + // radius must match the SVG circle (r="52" in CSS) + const radius = 52; + const circumference = 2 * Math.PI * radius; + + // ensure dasharray is set + circle.style.strokeDasharray = `${circumference}`; + + const offset = circumference - (percentage / 100) * circumference; + circle.style.strokeDashoffset = `${offset}`; + } + + if (textEl) { + textEl.textContent = `${percentage.toFixed(2)}%`; + } +} + +// Expose for other modules (e.g. summary.js) +window.updatePercentageDisplay = updatePercentageDisplay; diff --git a/dataedit/static/peer_review/opr_reviewer.js b/dataedit/static/peer_review/opr_reviewer.js index d8f8eb61c..7fd46153b 100644 --- a/dataedit/static/peer_review/opr_reviewer.js +++ b/dataedit/static/peer_review/opr_reviewer.js @@ -126,18 +126,33 @@ function click_field(fieldKey, fieldValue, category) { } }); -const explanationContainer = document.getElementById("explanation-container"); - -if (explanationContainer) { - const existingExplanation = explanationContainer.querySelector('.explanation'); - - if (isEmpty && !existingExplanation) { - const explanationElement = document.createElement('p'); - explanationElement.textContent = 'Field is empty. Reviewing is not possible.'; - explanationElement.classList.add('explanation'); - explanationContainer.appendChild(explanationElement); - } else if (!isEmpty && existingExplanation) { - explanationContainer.removeChild(existingExplanation); +const fieldElementForMsg = document.querySelector(`.field[data-fieldkey="${fieldKey}"]`); +if (fieldElementForMsg) { + const safeKey = fieldKey.replace(/[^a-zA-Z0-9_-]/g, '_'); + let explanationElement = document.getElementById(`explanation_${safeKey}`); + + if (isEmpty) { + if (!explanationElement) { + explanationElement = document.createElement('p'); + explanationElement.id = `explanation_${safeKey}`; + explanationElement.classList.add('explanation', 'text-muted', 'mt-1'); + explanationElement.innerText = 'Field is empty. Reviewing is not possible.'; + fieldElementForMsg.appendChild(explanationElement); + } else if (explanationElement.parentElement !== fieldElementForMsg) { + fieldElementForMsg.appendChild(explanationElement); + } + // Style label and value in light gray + const labelEl = fieldElementForMsg.querySelector('.field-label, .field__label, .label, .key, .field-name'); + const valueEl = fieldElementForMsg.querySelector('.field-value, .value'); + if (labelEl) labelEl.style.color = '#6c757d'; + if (valueEl) valueEl.style.color = '#6c757d'; + } else if (explanationElement) { + explanationElement.remove(); + // Reset label/value color + const labelEl = fieldElementForMsg.querySelector('.field-label, .field__label, .label, .key, .field-name'); + const valueEl = fieldElementForMsg.querySelector('.field-value, .value'); + if (labelEl) labelEl.style.color = ''; + if (valueEl) valueEl.style.color = ''; } } @@ -319,6 +334,9 @@ function saveEntrancesForReviewer() { } updateFieldColor(); + if (window.updatePercentageDisplay) { + window.updatePercentageDisplay(); + } if (selectedState === "ok" ) { document.getElementById("valuearea").value = ""; document.getElementById("commentarea").value = ""; @@ -375,19 +393,63 @@ function getTotalFieldCount() { function calculateOkPercentage(stateDict) { - let totalCount = getTotalFieldCount(); + let totalCount = 0; let okCount = 0; + if (!stateDict) { + return "0.00"; + } + for (let key in stateDict) { - if (stateDict[key] === "ok") { - okCount++; + const fieldElement = document.getElementById(`field_${key}`); + if (!fieldElement) continue; + + const fieldValue = $(fieldElement).find('.value').text().replace(/\s+/g, ' ').trim(); + if (!isEmptyValue(fieldValue)) { + totalCount++; + if (stateDict[key] === "ok") { + okCount++; + } } } - let percentage = (okCount / totalCount) * 100; + const percentage = totalCount === 0 ? 0 : (okCount / totalCount) * 100; return percentage.toFixed(2); } function updatePercentageDisplay() { - document.getElementById("percentageDisplay").textContent = calculateOkPercentage(window.state_dict); + if (!window.state_dict) return; + + const percentage = parseFloat(calculateOkPercentage(window.state_dict)); + + // Circle elements + const circle = document.getElementById("okProgressCircle"); + const textEl = + document.getElementById("okPercentageText") || + document.getElementById("percentageDisplay"); + + if (circle) { + // radius must match the SVG circle (r="52" in CSS) + const radius = 52; + const circumference = 2 * Math.PI * radius; + + // ensure dasharray is set + circle.style.strokeDasharray = `${circumference}`; + + const offset = circumference - (percentage / 100) * circumference; + circle.style.strokeDashoffset = `${offset}`; + } + + if (textEl) { + textEl.textContent = `${percentage.toFixed(2)}%`; + } } + +// Expose for other modules (e.g. summary.js) +window.updatePercentageDisplay = updatePercentageDisplay; + +window.addEventListener('DOMContentLoaded', () => { + if (window.updatePercentageDisplay) { + window.updatePercentageDisplay(); + } +}); diff --git a/dataedit/static/peer_review/peer_review.js b/dataedit/static/peer_review/peer_review.js index 47d753aba..98705cf11 100644 --- a/dataedit/static/peer_review/peer_review.js +++ b/dataedit/static/peer_review/peer_review.js @@ -9,12 +9,109 @@ import {renderSummaryPageFields, updateSubmitButtonColor, updateTabProgressIndic import {selectNextField} from "./navigation.js"; import {isEmptyValue, sendJson, getCookie} from "./utilities.js"; import {getFieldState, updateClientStateDict} from "./state_current_review.js"; + function getFieldElByKey(fieldKey) { + return document.getElementById(`field_${fieldKey}`); +} +function getFieldJqByKey(fieldKey) { + const el = getFieldElByKey(fieldKey); + return el ? $(el) : null; +} + +function normalizeFieldKey(fieldKey) { + return String(fieldKey) + .split('.') + .filter(part => part !== '' && !/^\d+$/.test(part)) // убрать индексы + .join('.'); +} + +function getTextFromEl(el, selectors) { + if (!el) return ''; + for (const sel of selectors) { + const found = el.querySelector(sel); + const txt = found?.textContent?.replace(/\s+/g, ' ')?.trim(); + if (txt) return txt; + } + return ''; +} + +function getFallbackTitle(fieldKey) { + const fieldEl = getFieldElByKey(fieldKey); + return getTextFromEl(fieldEl, ['.field__label', '.label', 'label']) || fieldKey; +} + +function getFallbackDescription(fieldKey) { + const fieldEl = getFieldElByKey(fieldKey); + + const attr = + fieldEl?.getAttribute('data-description') || + fieldEl?.getAttribute('data-help') || + fieldEl?.getAttribute('title') || + fieldEl?.dataset?.description || + fieldEl?.dataset?.help; + + if (attr && String(attr).trim()) return String(attr).trim(); + + const helpText = getTextFromEl(fieldEl, [ + '.help-text', + '.helptext', + '.field__help', + '.field__description', + '.description', + '[data-role="description"]', + ]); + if (helpText) return helpText; + + // best-effort: если у вас есть schema на window + const schema = + window.schema_dict || window.schema || window.json_schema || window.ontology_schema; + + if (schema) { + const parts = normalizeFieldKey(fieldKey).split('.'); + let node = schema; + + for (const part of parts) { + if (!node) break; + + if (node.properties && node.properties[part]) { + node = node.properties[part]; + continue; + } + + if (node.items) { + node = node.items; + if (node.properties && node.properties[part]) { + node = node.properties[part]; + continue; + } + } + + if (node[part]) { + node = node[part]; + continue; + } + + node = null; + } + + const desc = node?.description || node?.help || node?.comment; + if (desc && String(desc).trim()) return String(desc).trim(); + } + return ''; +} +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} window.selectedField = window.selectedField ?? null; export let selectedFieldValue=null; export function setSelectedField(fieldKey) { - selectedField = fieldKey; + window.selectedField = fieldKey; } export function setselectedFieldValue(fieldValue) { selectedFieldValue = fieldValue; @@ -139,6 +236,7 @@ export function peerReview(config, checkState = false) { selectNextField(); renderSummaryPageFields(); updateTabProgressIndicatorClasses(); + updatePercentageDisplay(); if (checkState && typeof window.state_dict !== 'undefined') { check_if_review_finished(); @@ -183,18 +281,37 @@ export function updateFieldDescription(cleanedFieldKey, fieldValue) { const fieldDescriptionsElement = document.getElementById("field-descriptions"); const selectedName = document.querySelector("#review-field-name"); - selectedName.textContent = cleanedFieldKey + " " + fieldValue; + // Prefer the real selected key (can include indices/special chars), fallback to cleaned. + const rawKey = window.selectedField || cleanedFieldKey; + const normalizedKey = normalizeFieldKey(rawKey); - if (fieldDescriptionsData[cleanedFieldKey]) { - const fieldInfo = fieldDescriptionsData[cleanedFieldKey]; + // Try description dictionary first (by several key forms) + const fieldInfo = + (fieldDescriptionsData && (fieldDescriptionsData[rawKey] || fieldDescriptionsData[cleanedFieldKey] || fieldDescriptionsData[normalizedKey])) + ? (fieldDescriptionsData[rawKey] || fieldDescriptionsData[cleanedFieldKey] || fieldDescriptionsData[normalizedKey]) + : null; + + // Build header/title text + const titleText = fieldInfo?.title || getFallbackTitle(rawKey) || cleanedFieldKey; + selectedName.textContent = `${titleText}${fieldValue ? ' — ' + fieldValue : ''}`; + + if (fieldInfo) { let fieldInfoText = '
'; if (fieldInfo.title) { fieldInfoText += `

${fieldInfo.title}

`; + } else { + fieldInfoText += `

${escapeHtml(titleText)}

`; } + if (fieldInfo.description) { fieldInfoText += `
Description:
${fieldInfo.description}
`; + } else { + const fallback = getFallbackDescription(rawKey) || getFallbackDescription(cleanedFieldKey); + fieldInfoText += `
Description:
${escapeHtml(fallback || 'No description available for this field.')}
`; + if (!fallback) console.warn('No description for field:', rawKey); } + if (fieldInfo.example) { fieldInfoText += `
Example:
${fieldInfo.example}
`; } @@ -202,14 +319,26 @@ export function updateFieldDescription(cleanedFieldKey, fieldValue) { fieldInfoText += `
Badge:
${fieldInfo.badge}
`; } - fieldInfoText += `
Does it comply with the required ${fieldInfo.title} description convention?
`; + const requiredTitle = fieldInfo.title || titleText; + fieldInfoText += `
Does it comply with the required ${escapeHtml(requiredTitle)} description convention?
`; fieldDescriptionsElement.innerHTML = fieldInfoText; - } else { - fieldDescriptionsElement.textContent = "No description found"; + return; } + + // No dictionary entry: render fallback description instead of a confusing placeholder. + const fallbackDesc = getFallbackDescription(rawKey) || getFallbackDescription(cleanedFieldKey); + + let html = '
'; + html += `

${escapeHtml(titleText)}

`; + html += `
Description:
${escapeHtml(fallbackDesc || 'No description available for this field.')}
`; + html += '
'; + + fieldDescriptionsElement.innerHTML = html; + if (!fallbackDesc) console.warn('No description for field:', rawKey); } export function highlightSelectedField(fieldKey, highlightColor = '#F6F9FB') { + if (!fieldKey) return; const reviewItem = document.querySelectorAll('.review__item'); const selectedDivId = 'field_' + fieldKey; const selectedDiv = document.getElementById(selectedDivId); @@ -275,7 +404,11 @@ export function selectField(fieldList, field) { export function selectState(state, shouldUpdateClient = false) { selectedState = state; - updateClientStateDict(selectedField, state); + + const selectedKey = window.selectedField; + if (selectedKey) { + updateClientStateDict(selectedKey, state); + } if (shouldUpdateClient) { check_if_review_finished(); diff --git a/dataedit/static/peer_review/summary.js b/dataedit/static/peer_review/summary.js index c52bc3360..6ce4b18a3 100644 --- a/dataedit/static/peer_review/summary.js +++ b/dataedit/static/peer_review/summary.js @@ -12,15 +12,15 @@ export function renderSummaryPageFields() { const emptyFields = []; const processedFields = new Set(); + if (window.state_dict && Object.keys(window.state_dict).length > 0) { const fields = document.querySelectorAll('.field'); for (let field of fields) { - let field_id = field.id.slice(6); + const field_id = field.id.slice(6); const fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); const fieldState = getFieldState(field_id); const fieldCategory = field.getAttribute('data-category'); - const fieldSuggestion = field.querySelector('.suggestion.suggestion--highlight')?.textContent.trim() || ""; - + const fieldSuggestion = field.querySelector('.suggestion.suggestion--highlight')?.textContent.trim() || ""; // remove the numbers and replace the dots with spaces let fieldName = field_id.replace(/\./g, ' '); @@ -32,21 +32,24 @@ export function renderSummaryPageFields() { const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`; if (isEmptyValue(fieldValue)) { - emptyFields.push({ fieldName, fieldValue, fieldCategory: "emptyFields", fieldSuggestion }); + emptyFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); } else if (fieldState === 'ok') { - acceptedFields.push({ fieldName, fieldValue, fieldCategory }); + acceptedFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); processedFields.add(uniqueFieldIdentifier); } } } for (const review of current_review.reviews) { - const field_id = `field_${review.key}`; - const fieldSelector = `#${CSS.escape(field_id)}`; - const fieldValue = $(fieldSelector).find('.value').text().replace(/\s+/g, ' ').trim(); + const fieldDomId = `field_${review.key}`; + const fieldEl = document.getElementById(fieldDomId); + const fieldValue = fieldEl + ? $(fieldEl).find('.value').text().replace(/\s+/g, ' ').trim() + : ""; const fieldState = review.fieldReview.state; const fieldCategory = review.category; - const fieldSuggestion = review.fieldReview.reviewerSuggestion + const fieldSuggestion = review.fieldReview.reviewerSuggestion || ""; + let fieldName = review.key.replace(/\./g, ' '); if (fieldCategory !== "general") { @@ -60,13 +63,13 @@ export function renderSummaryPageFields() { } if (isEmptyValue(fieldValue)) { - emptyFields.push({ fieldName, fieldValue, fieldCategory: "emptyFields" }); + emptyFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); } else if (fieldState === 'ok') { - acceptedFields.push({ fieldName, fieldValue, fieldCategory }); + acceptedFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); } else if (fieldState === 'suggestion') { suggestingFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); } else if (fieldState === 'rejected') { - rejectedFields.push({ fieldName, fieldValue, fieldCategory }); + rejectedFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); } processedFields.add(uniqueFieldIdentifier); @@ -89,7 +92,6 @@ export function renderSummaryPageFields() { const fieldCategory = field.getAttribute('data-category'); const fieldSuggestion = field.querySelector('.suggestion.suggestion--highlight')?.textContent.trim() || ""; - let fieldName = field_id.replace(/\./g, ' '); if (fieldCategory !== "general") { @@ -98,87 +100,273 @@ export function renderSummaryPageFields() { const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`; - if (!found && fieldState !== 'ok' && !isEmptyValue(fieldValue) && !processedFields.has(uniqueFieldIdentifier)) { + if ( + !found && + fieldState !== 'ok' && + !isEmptyValue(fieldValue) && + !processedFields.has(uniqueFieldIdentifier) + ) { missingFields.push({ fieldName, fieldValue, fieldCategory, fieldSuggestion }); processedFields.add(uniqueFieldIdentifier); } } } + const allData = []; + allData.push(...missingFields.map((item) => ({ ...item, fieldStatus: 'Missing' }))); + allData.push(...acceptedFields.map((item) => ({ ...item, fieldStatus: 'Accepted' }))); + allData.push(...suggestingFields.map((item) => ({ ...item, fieldStatus: 'Suggested' }))); + allData.push(...rejectedFields.map((item) => ({ ...item, fieldStatus: 'Rejected' }))); + allData.push(...emptyFields.map((item) => ({ ...item, fieldStatus: 'Empty' }))); + const categoriesMap = {}; - // Functions for displaying a table with results on a page - const summaryContainer = document.getElementById("summary"); - - function clearSummaryTable() { - while (summaryContainer.firstChild) { - summaryContainer.firstChild.remove(); - } + function addFieldToCategory(category, field) { + if (!categoriesMap[category]) categoriesMap[category] = []; + categoriesMap[category].push(field); } - function generateTable(data) { - let table = document.createElement('table'); - table.className = 'table review-summary'; - - let thead = document.createElement('thead'); - let header = document.createElement('tr'); - header.innerHTML = 'StatusField CategoryField NameField ValueField Suggestion'; - thead.appendChild(header); - table.appendChild(thead); - - let tbody = document.createElement('tbody'); - - data.forEach((item) => { - let row = document.createElement('tr'); + allData.forEach(item => { + const category = item.fieldCategory || 'general'; + addFieldToCategory(category, item); + }); - let th = document.createElement('th'); - th.scope = "row"; - th.className = "status"; - if (item.fieldStatus === "Missing") { - th.className = "status missing"; + const summaryContainer = document.getElementById("summary"); + summaryContainer.innerHTML = ''; + + const tabsNav = document.createElement('ul'); + tabsNav.className = 'nav nav-tabs'; + + const tabsContent = document.createElement('div'); + tabsContent.className = 'tab-content'; + + let firstTab = true; + + for (const category in categoriesMap) { + const tabId = `tab-${category}`; + + const navItem = document.createElement('li'); + navItem.className = 'nav-item'; + navItem.innerHTML = ` + + `; + tabsNav.appendChild(navItem); + + const tabPane = document.createElement('div'); + tabPane.className = `tab-pane fade${firstTab ? ' show active' : ''}`; + tabPane.id = tabId; + + const fieldsForCategory = categoriesMap[category]; + const singleFields = []; + const groupedFields = {}; + + fieldsForCategory.forEach(field => { + const words = field.fieldName.split(' '); + if (words.length === 1) { + singleFields.push(field); + } else { + const prefix = words[0]; + const rest = words.slice(1); + const indices = rest.filter(word => !isNaN(word)); + const nameWithoutIndices = rest.filter(word => isNaN(word)).join(' '); + + if (!groupedFields[prefix]) groupedFields[prefix] = { indexed: {}, noIndex: [] }; + + if (indices.length > 0) { + const indexKey = indices.map(num => (parseInt(num, 10) + 1)).join('.'); + if (!groupedFields[prefix].indexed[indexKey]) groupedFields[prefix].indexed[indexKey] = []; + groupedFields[prefix].indexed[indexKey].push({ ...field, fieldName: nameWithoutIndices }); + } else { + groupedFields[prefix].noIndex.push({ ...field, fieldName: nameWithoutIndices }); + } } - th.textContent = item.fieldStatus; - row.appendChild(th); - - let tdFieldCategory = document.createElement('td'); - tdFieldCategory.textContent = item.fieldCategory; - row.appendChild(tdFieldCategory); - - let tdFieldId = document.createElement('td'); - tdFieldId.textContent = item.fieldName; - row.appendChild(tdFieldId); - - let tdFieldValue = document.createElement('td'); - tdFieldValue.textContent = item.fieldValue; - row.appendChild(tdFieldValue); + }); - let tdFieldSuggestion = document.createElement('td'); - tdFieldSuggestion.textContent = item.fieldSuggestion; - row.appendChild(tdFieldSuggestion); + if (singleFields.length > 0) { + const table = document.createElement('table'); + table.className = 'table review-summary'; + table.innerHTML = ` + + + Status + Field Name + Field Value + Field Suggestion + + + + ${singleFields.map(f => ` + + ${f.fieldStatus} + ${f.fieldName} + ${f.fieldValue} + ${f.fieldSuggestion || ''} + + `).join('')} + + `; + tabPane.appendChild(table); + } - tbody.appendChild(row); - }); + if (Object.keys(groupedFields).length > 0) { + const accordionContainer = document.createElement('div'); + accordionContainer.className = 'accordion'; + accordionContainer.id = `accordion-${category}`; + + let accordionIndex = 0; + for (const prefix in groupedFields) { + const accordionItem = document.createElement('div'); + accordionItem.className = 'accordion-item'; + const headingId = `heading-${category}-${accordionIndex}`; + const collapseId = `collapse-${category}-${accordionIndex}`; + + const { noIndex, indexed } = groupedFields[prefix]; + + let innerHTML = ''; + + if (noIndex.length > 0) { + innerHTML += ` + + + + + + + + + + + ${noIndex.map(f => ` + + + + + + + `).join('')} + +
StatusField NameField ValueField Suggestion
${f.fieldStatus}${f.fieldName}${f.fieldValue}${f.fieldSuggestion || ''}
+ `; + } + + if (Object.keys(indexed).length > 0) { + const subAccordionId = `subAccordion-${category}-${accordionIndex}`; + innerHTML += `
`; + + Object.entries(indexed).forEach(([idx, idxFields], idxAccordionIndex) => { + const idxHeadingId = `idxHeading-${category}-${accordionIndex}-${idxAccordionIndex}`; + const idxCollapseId = `idxCollapse-${category}-${accordionIndex}-${idxAccordionIndex}`; + + const tabLabel = ['source', 'license'].includes(category) ? 'fields' : `${prefix} ${idx}`; + + innerHTML += ` +
+

+ +

+
+
+ + + + + + + + + + + ${idxFields.map(f => ` + + + + + + + `).join('')} + +
StatusField NameField ValueField Suggestion
${f.fieldStatus}${f.fieldName}${f.fieldValue}${f.fieldSuggestion || ''}
+
+
+
+ `; + }); + + innerHTML += `
`; + } + + accordionItem.innerHTML = ` +

+ +

+
+
+ ${innerHTML} +
+
+ `; + + accordionContainer.appendChild(accordionItem); + accordionIndex++; + } - table.appendChild(tbody); + tabPane.appendChild(accordionContainer); + } - return table; + tabsContent.appendChild(tabPane); + firstTab = false; } - function updateSummaryTable() { - clearSummaryTable(); - let allData = []; - allData.push(...missingFields.map((item) => ({ ...item, fieldStatus: 'Missing' }))); - allData.push(...acceptedFields.map((item) => ({ ...item, fieldStatus: 'Accepted' }))); - allData.push(...suggestingFields.map((item) => ({ ...item, fieldStatus: 'Suggested' }))); - allData.push(...rejectedFields.map((item) => ({ ...item, fieldStatus: 'Rejected' }))); - allData.push(...emptyFields.map((item) => ({ ...item, fieldStatus: 'Empty' }))); - - let table = generateTable(allData); - summaryContainer.appendChild(table); - } + const viewsNavItem = document.createElement('li'); + viewsNavItem.className = 'nav-item'; + viewsNavItem.innerHTML = ` + + `; + tabsNav.appendChild(viewsNavItem); + + const viewsPane = document.createElement('div'); + viewsPane.className = 'tab-pane fade'; + viewsPane.id = 'tab-views'; + + viewsPane.innerHTML = ` + + + + + + + + + + + + ${allData.map(f => ` + + + + + + + + `).join('')} + +
StatusCategoryField NameField ValueField Suggestion
${f.fieldStatus}${f.fieldCategory}${f.fieldName}${f.fieldValue}${f.fieldSuggestion || ''}
+ `; + + tabsContent.appendChild(viewsPane); + summaryContainer.appendChild(tabsNav); + summaryContainer.appendChild(tabsContent); - updateSummaryTable(); updateTabProgressIndicatorClasses(); + updatePercentageDisplay(); } export function updateSubmitButtonColor() { diff --git a/dataedit/templates/dataedit/opr_contributor.html b/dataedit/templates/dataedit/opr_contributor.html index e0e08ffd1..d89e1b6b4 100644 --- a/dataedit/templates/dataedit/opr_contributor.html +++ b/dataedit/templates/dataedit/opr_contributor.html @@ -13,6 +13,8 @@ {% block main %}
+ +

@@ -47,6 +49,15 @@

+
+
+ + + + +
0%
+
+
-
+
- {% for item in meta.general %} -
-

- {% 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.additional_comment }} - - {% endif %} -

-
- {% endfor %} + {% if meta.grouped_meta %} + {# flat general #} + {% for item in meta.grouped_meta.general.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + + {# grouped general #} +
+ {% for group_key, group in meta.grouped_meta.general.grouped.items %} +
+

+ +

+
+
+ {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + + {% if group.grouped %} +
+ {% for sub_key, sub_items in group.grouped.items %} +
+

+ +

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

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} +
+
+
+ {% endfor %} +
+ {% endif %} +
+
+
+ {% endfor %} +
+ {% else %} + {# legacy structure fallback (robust) #} + {% if meta.general.flat %} + {% for item in meta.general.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} +
+ {% for group_key, group in meta.general.grouped.items %} +
+

+ +

+
+
+ {% if group.flat %} + {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% elif group.0 %} + {# true legacy list case: group is a list #} + {% for item in group %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} +
+
+
+ {% endfor %} +
+ {% else %} + {% for item in meta.general %} +
+

+ {{ item.label|default:item.field }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} + {% endif %}
+ + {# -------- Spatial -------- #}

Spatial

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

- {% 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.additional_comment }} - {% endif %} + {% if meta.grouped_meta %} +

+ {% with spatial_grouped=meta.grouped_meta.spatial.grouped %} + {% if spatial_grouped %} + {% for group_key, group in spatial_grouped.items %} +
+

+ +

+
+
+ {# flat fields of the outer group (e.g., extent.* that are not nested) #} + {% if group.flat %} + {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }}

+ {% endfor %} + {% endif %} + + {# inner accordion for subgroups (e.g., BoundingBox) #} + {% if group.grouped %} +
+ {% for sub_key, sub_items in group.grouped.items %} +
+

+ +

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

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} +
+
+
{% endfor %} +
+ {% elif group.0 %} + {# legacy: group is a plain list #} + {% for item in group %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} +
+
+
+ {% endfor %} + {% endif %} + {% endwith %} +
+{% else %} + {# Legacy fallbacks for Spatial #} + {% if meta.spatial.grouped %} +
+ {% for group_key, group in meta.spatial.grouped.items %} +
+

+ +

+
+
+ {% if group.flat %} + {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% elif group.grouped %} + {% for sub_key, sub_items in group.grouped.items %} + {% if sub_items.flat %} + {# sub_items is a dict like { flat:[...], grouped:{...} } #} + {% for item in sub_items.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% elif sub_items.0 %} + {# legacy: sub_items is a simple list #} + {% for item in sub_items %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} + {% endfor %} + {% else %} + {# true legacy list case #} + {% for item in group %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} +
+
+
+ {% endfor %} +
+ {% endif %} + + {% if meta.spatial.flat %} + {% for item in meta.spatial.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} + {% endif %} + + {# -------- Temporal -------- #}

Temporal

- {% for item in meta.temporal %} -
-

- {% 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 %} + {% if meta.grouped_meta %} +

+ {% for group_key, group in meta.grouped_meta.temporal.grouped.items %} +
+

+ +

+
+
+ {% if group.flat %} + {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }}

+ {% endfor %} + {% endif %} + + {% if group.grouped %} +
+ {% for sub_key, sub_items in group.grouped.items %} +
+

+ +

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

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} +
+
+
{% endfor %}
-
-
-
- {% for item in meta.source %} -
-

- {% 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 %} + {% elif group.0 %} + {% for item in group %} +

+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }}

{% endfor %} + {% endif %} +
+
+
+ {% endfor %} +
+{% else %} + {# Legacy fallbacks for Temporal #} + {% if meta.temporal.grouped %} +
+ {% for group_key, group in meta.temporal.grouped.items %} +
+

+ +

+
+
+ {% if group.flat %} + {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% elif group.grouped %} + {% for sub_key, sub_items in group.grouped.items %} + {% if sub_items.flat %} + {% for item in sub_items.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% elif sub_items.0 %} + {% for item in sub_items %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} + {% endfor %} + {% else %} + {# true legacy list case #} + {% for item in group %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} +
+
+
+ {% endfor %} +
+ {% endif %} + + {% if meta.temporal.flat %} + {% for item in meta.temporal.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} + {% endif %} +
-
+
- {% for item in meta.license %} -
-

- {% 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 %} -

-
- {% endfor %} + {% if meta.grouped_meta %} + {% for item in meta.grouped_meta.source.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + +
+ {% for group_key, group in meta.grouped_meta.source.grouped.items %} +
+

+ +

+
+
+ {# flat items of this Source N #} + {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + + {# nested sublists (e.g., Licenses 1, Documents 2) #} + {% if group.grouped %} +
+ {% for sub_key, sub_items in group.grouped.items %} +
+

+ +

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

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} +
+
+
+ {% endfor %} +
+ {% endif %} +
+
+
+ {% endfor %} +
+ {% else %} + {# robust legacy fallback for Source: handle dict {flat, grouped} and true legacy list #} + {% if meta.source.grouped %} +
+ {% for group_key, group in meta.source.grouped.items %} +
+

+ +

+
+
+ {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + + {% if group.grouped %} +
+ {% for sub_key, sub_items in group.grouped.items %} +
+

+ +

+
+
+ {% if sub_items.flat %} + {% for item in sub_items.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% elif sub_items.0 %} + {% for item in sub_items %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} +
+
+
+ {% endfor %} +
+ {% endif %} +
+
+
+ {% endfor %} +
+ {% endif %} + + {% if meta.source.flat %} + {% for item in meta.source.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} + + {# true legacy list fallback #} +{% if meta.source.0 %} + {% for item in meta.source %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} + {% endif %}
+
+
+ {% if meta.grouped_meta %} + {% for item in meta.grouped_meta.license.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + +
+ {% for group_key, group in meta.grouped_meta.license.grouped.items %} +
+

+ +

+
+
+ {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% if group.grouped %} +
+ {% for sub_key, sub_items in group.grouped.items %} +
+

+ +

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

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} +
+
+
+ {% endfor %} +
+ {% endif %} +
+
+
+ {% endfor %} +
+ {% else %} + {# robust legacy fallback for License: handle dict {flat, grouped} and true legacy list #} + {% if meta.license.grouped %} +
+ {% for group_key, group in meta.license.grouped.items %} +
+

+ +

+
+
+ {% for item in group.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + + {% if group.grouped %} +
+ {% for sub_key, sub_items in group.grouped.items %} +
+

+ +

+
+
+ {% if sub_items.flat %} + {% for item in sub_items.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% elif sub_items.0 %} + {# legacy: sub_items is a simple list of items #} + {% for item in sub_items %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} +
+
+
+ {% endfor %} +
+ {% endif %} +
+
+
+ {% endfor %} +
+ {% endif %} + {% if meta.license.flat %} + {% for item in meta.license.flat %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} + + {# true legacy list fallback #} +{% if meta.license.0 %} + {% for item in meta.license %} +
+

+ {{ item.display_field|default:item.field|default:"(undefined)" }} + {{ item.newValue|default:item.value }} + {{ item.reviewer_suggestion }} + {{ item.suggestion_comment }}
+ {{ item.additional_comment }} +

+
+ {% endfor %} + {% endif %} + {% endif %} +
+
@@ -249,6 +1018,7 @@ {{ field_descriptions_json }}
+
-
-
- -
+
+
+ +
-
-
- - -
-
- - -
- -
-
+
+
+
+ + +
+
+ + +
+ +
+ +
-
+
-
Peer reviewed by jh-RLI on 2022-09-19
-
0 sucessful reviews
+
Dataset uploaded by bmlancien on 2022-09-19
+
0 review
+ + +
+
+ + + + +
0%
+
+
@@ -339,10 +1176,12 @@ {% compress js %} - - - + + + -{# #} {% endcompress %} {% endblock after-body-bottom-js %} diff --git a/dataedit/templates/dataedit/opr_review.html b/dataedit/templates/dataedit/opr_review.html index 9fda48afe..9c2e5032e 100644 --- a/dataedit/templates/dataedit/opr_review.html +++ b/dataedit/templates/dataedit/opr_review.html @@ -17,6 +17,7 @@ {% block main %}
+

@@ -51,6 +52,15 @@

+
+
+ + + + +
0%
+
+
+ + +
+
+ + + + +
0%
+
+
{% endblock main %} {% block after-body-bottom-js %} diff --git a/dataedit/views.py b/dataedit/views.py index 920d30637..320dc1ce4 100644 --- a/dataedit/views.py +++ b/dataedit/views.py @@ -2017,79 +2017,192 @@ def parse_keys(self, val, old=""): def sort_in_category(self, schema, table, oemetadata): """ - Group flattened OEMetadata v2 fields into thematic buckets and attach - placeholders required by the review UI. - - Each entry has six keys: - { - "field": "", - "label": ".'>", - "value": "", - "newValue": "", - "reviewer_suggestion": "", - "suggestion_comment": "" - } + Groups OEMetadata v2 fields by top categories and + creates a two-level grouping of lists + (accordion-within-an-accordion) for general, + source, license, and spatial/temporal. + + Category Exit: + {"flat": [ { field, value, label, display_field, newValue, + reviewer_suggestion, suggestion_comment, + ... } ], + "grouped": { "": { "flat":[...], "grouped":{ "":[...] + } }, ... } + } """ import re from collections import defaultdict + def _plus_one_if_digit(txt: str) -> str: + return str(int(txt) + 1) if str(txt).isdigit() else txt + flattened = self.parse_keys(oemetadata) flattened = [ - item for item in flattened if item["field"].startswith("resources.") + x for x in flattened if str(x.get("field", "")).startswith("resources.") ] - bucket_map = { - "spatial": "spatial", - "temporal": "temporal", - "sources": "source", - "licenses": "license", - } - - def make_label(dot_path: str) -> str: - # remove leading resources.. - trimmed = re.sub(r"^resources\.[0-9]+\.", "", dot_path) - parts = trimmed.split(".") - out = [] - for p in parts: - if p in {"@id", "@type"}: - out.append(p) - else: - out.append(p.replace("_", " ")) - if out: - out[0] = out[0][:1].upper() + out[0][1:] - return " ".join(out) - - tmp = defaultdict(list) - + base_items = [] for item in flattened: - raw_key = item["field"] - parts = raw_key.split(".") - - if parts[0] == "resources" and len(parts) >= 3: - root = parts[2] + raw = item["field"] + parts = raw.split(".") + if len(parts) >= 3 and parts[0] == "resources" and parts[1].isdigit(): + trimmed = ".".join(parts[2:]) else: - root = parts[0] + trimmed = raw - bucket = bucket_map.get(root, "general") + lbl_parts = [p.replace("_", " ") for p in trimmed.split(".")] + if lbl_parts: + lbl_parts[0] = lbl_parts[0][:1].upper() + lbl_parts[0][1:] + label = " ".join(lbl_parts) - tmp[bucket].append( + base_items.append( { - "field": raw_key, - "label": make_label(raw_key), - "value": item["value"], + "field": trimmed, + "label": label, + "value": item.get("value", ""), "newValue": "", "reviewer_suggestion": "", "suggestion_comment": "", + "additional_comment": item.get("additional_comment", ""), } ) - return { - "general": tmp["general"], - "spatial": tmp["spatial"], - "temporal": tmp["temporal"], - "source": tmp["source"], - "license": tmp["license"], - } + main_categories = defaultdict(list) + for itm in base_items: + root = itm["field"].split(".")[0] if "." in itm["field"] else itm["field"] + cat = { + "spatial": "spatial", + "temporal": "temporal", + "sources": "source", + "licenses": "license", + }.get(root, "general") + main_categories[cat].append(itm) + + def extract_index(prefix: str) -> int: + m = re.search(r"(?:\.|\s)([0-9]+)$", prefix or "") + return int(m.group(1)) if m else -1 + + def group_index_only(items): + """First index occurrence: name.0.* → 'Name 1'; + otherwise, group by the first token.""" + result = {"flat": [], "grouped": defaultdict(list)} + for itm in items: + field = itm["field"] + m = re.match(r"^([^.]+)\.([0-9]+)(?:\.(.*))?$", field) + if m: + list_name, idx, tail = ( + m.group(1), + int(m.group(2)), + m.group(3) or "value", + ) + disp_prefix = f"{list_name.capitalize()} {idx + 1}" + enriched = dict(itm) + enriched["display_field"] = tail + enriched["display_prefix"] = disp_prefix + enriched["display_index"] = str(idx + 1) + result["grouped"][disp_prefix].append(enriched) + elif "." in field: + group_key = field.split(".")[0] + enriched = dict(itm) + enriched["display_field"] = ".".join(field.split(".")[1:]) + enriched["display_prefix"] = group_key + enriched.pop("display_index", None) + result["grouped"][group_key].append(enriched) + else: + enriched = dict(itm) + enriched["display_field"] = field + enriched.pop("display_index", None) + result["flat"].append(enriched) + result["grouped"] = dict( + sorted(result["grouped"].items(), key=lambda kv: extract_index(kv[0])) + ) + return result + + def nest_sublist_groups(items_for_one_parent): + from collections import defaultdict + + grouped_map = defaultdict(lambda: {"flat": [], "grouped": {}}) + flat = [] + + for itm in items_for_one_parent: + field = itm["field"] + m = re.match(r"^([^.]+)\.([0-9]+)(?:\.(.*))?$", field) + if m: + head, idx, tail = m.group(1), int(m.group(2)), m.group(3) + e = dict(itm) + e["display_field"] = tail if (tail and tail.strip()) else str(idx) + e["display_prefix"] = head + e.pop("display_index", None) + grouped_map[head.capitalize()]["flat"].append(e) + else: + e = dict(itm) + trimmed = ".".join(field.split(".")[1:]) if "." in field else field + e["display_field"] = _plus_one_if_digit(trimmed) + flat.append(e) + + grouped = dict(sorted(grouped_map.items(), key=lambda kv: kv[0])) + return {"flat": flat, "grouped": grouped} + + def _strip_cat_prefix(items, cat_name): + """spatial.extent.name → extent.name; temporal.period.start → + period.start""" + out = [] + for it in items: + f = it["field"] + if f.startswith(cat_name + "."): + trimmed = f[len(cat_name) + 1 :] + e = dict(it) + e["field"] = trimmed + out.append(e) + else: + out.append(it) + return out + + def _group_spatiotemporal(items, cat_name): + """Level 1: by the first token AFTER + 'spatial.'/'temporal.' + Level 2: as usual – separate the '..*' + lists into nested sections. + """ + + stripped = _strip_cat_prefix(items, cat_name) + + first = group_index_only(stripped) + + nested_grouped = {} + for gkey, gitems in first["grouped"].items(): + nested_grouped[gkey.capitalize()] = nest_sublist_groups(gitems) + + return {"flat": first["flat"], "grouped": nested_grouped} + + grouped_meta = {} + for cat, items in main_categories.items(): + if cat == "spatial": + grouped = _group_spatiotemporal(items, "spatial") + elif cat == "temporal": + grouped = _group_spatiotemporal(items, "temporal") + elif cat == "source": + first = group_index_only(items) + nested_grouped = { + k: nest_sublist_groups(v) for k, v in first["grouped"].items() + } + grouped = {"flat": first["flat"], "grouped": nested_grouped} + elif cat == "license": + first = group_index_only(items) + nested_grouped = { + k: nest_sublist_groups(v) for k, v in first["grouped"].items() + } + grouped = {"flat": first["flat"], "grouped": nested_grouped} + else: + # general (как было у вас) + grouped = group_index_only(items) + + grouped_meta[cat] = {"flat": grouped["flat"], "grouped": grouped["grouped"]} + + for k in ("general", "spatial", "temporal", "source", "license"): + grouped_meta.setdefault(k, {"flat": [], "grouped": {}}) + + return grouped_meta def get_all_field_descriptions(self, json_schema, prefix=""): """