diff --git a/dataedit/helper.py b/dataedit/helper.py index e0659c036..969cb92cf 100644 --- a/dataedit/helper.py +++ b/dataedit/helper.py @@ -49,9 +49,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 = {} @@ -88,18 +88,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: @@ -108,20 +97,9 @@ 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'. - - Args: - metadata (dict): The original metadata dictionary to update. - review_data (dict): The review data containing the new values - for various keys. + """Recursively updates metadata with new values from review_data. - 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: list | dict | None, keys: list[str]): @@ -189,18 +167,7 @@ def delete_nested_field(data: list | dict | None, keys: list[str]): 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(): @@ -213,73 +180,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] = {} + + def iter_field_items(node): + """Yield all leaf field-item dicts inside nested list/dict structures. - for review in review_data: + 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..ac81e28d3 100644 --- a/dataedit/static/peer_review/main.js +++ b/dataedit/static/peer_review/main.js @@ -3,20 +3,40 @@ import * as common from "./peer_review.js"; import { selectState } from './peer_review.js'; -window.selectState = selectState; +import { selectNextField, selectPreviousField } from './navigation.js'; +import { setGetFieldState } from './state_current_review.js'; -import { selectNextField } from './navigation.js' -window.selectNextField = selectNextField; +// Static imports avoid the "Failed to fetch" dynamic import errors +import { initReviewer } from './opr_reviewer.js'; +import { initContributor } from './opr_contributor.js'; -import { selectPreviousField } from './navigation.js' +// Expose functions to global window scope for HTML onclick events +window.selectState = selectState; +window.selectNextField = selectNextField; window.selectPreviousField = selectPreviousField; -import { setGetFieldState } from './state_current_review.js'; -import './opr_reviewer.js'; +// Initialize the state getter setGetFieldState((fieldKey) => { return window.state_dict?.[fieldKey] ?? null; }); + document.addEventListener('DOMContentLoaded', function () { - common.initCurrentReview(config); - common.peerReview(config, true); + // Initialize common logic + // 'config' is defined in the HTML template + if (typeof config !== 'undefined') { + common.initCurrentReview(config); + common.peerReview(config, true); + } + + // Initialize role-specific logic based on the HTML marker + const marker = document.getElementById('opr-page-marker'); + const oprPage = marker?.dataset?.oprPage; + + if (oprPage === 'reviewer') { + initReviewer(); + } else if (oprPage === 'contributor') { + initContributor(); + } else { + console.warn('OPR page marker not found or invalid; skipping role-specific initialization'); + } }); \ No newline at end of file diff --git a/dataedit/static/peer_review/navigation.js b/dataedit/static/peer_review/navigation.js index 9dc7dac99..d2a0174d2 100644 --- a/dataedit/static/peer_review/navigation.js +++ b/dataedit/static/peer_review/navigation.js @@ -1,16 +1,47 @@ // SPDX-FileCopyrightText: 2025 Reiner Lemoine Institut // SPDX-License-Identifier: AGPL-3.0-or-later -import {getCategoryToTabIdMapping, makeFieldList, selectField} from "./peer_review.js"; + +import { getCategoryToTabIdMapping, makeFieldList, selectField } from "./peer_review.js"; + +export function updateTabProgress() { + const allFields = document.querySelectorAll('.review__item'); + const total = allFields.length; + let completed = 0; + + allFields.forEach(field => { + // Check for any of the completed states (ok, rejected, suggestion) + if (field.classList.contains('field-ok') || + field.classList.contains('field-rejected') || + field.classList.contains('field-suggestion')) { + completed++; + } + }); + + const percentage = total === 0 ? 0 : Math.round((completed / total) * 100); + + // Update the circular progress bar + const circle = document.getElementById('okProgressCircle'); + const text = document.getElementById('okPercentageText'); + + if (circle && text) { + // 326.72 is 2*PI*r where r=52 (from your SVG) + const circumference = 326.72; + const offset = circumference - (percentage / 100) * circumference; + circle.style.strokeDashoffset = offset; + text.textContent = `${percentage}%`; + } +} + +// Alias to fix the ReferenceError in summary.js +export const updatePercentageDisplay = updateTabProgress; export function switchCategoryTab(category) { - const currentTab = document.querySelector('.tab-pane.active'); // Get the currently active tab + const currentTab = document.querySelector('.tab-pane.active'); const tabIdForCategory = getCategoryToTabIdMapping()[category]; - console.log("tabID", tabIdForCategory); - if (currentTab.getAttribute('id') !== tabIdForCategory) { - // The clicked field does not belong to the current tab, switch to the next tab + + if (currentTab && currentTab.getAttribute('id') !== tabIdForCategory) { const targetTab = document.getElementById(tabIdForCategory); if (targetTab) { - // The target tab exists, click the tab link to switch to it targetTab.click(); } } @@ -22,9 +53,6 @@ export function selectNextField() { selectField(fieldList, next); } -/** - * Selects the HTML field element previous to the current one and clicks it - */ export function selectPreviousField() { var fieldList = makeFieldList(); var prev = fieldList.indexOf('field_' + window.selectedField) - 1; diff --git a/dataedit/static/peer_review/opr_contributor.js b/dataedit/static/peer_review/opr_contributor.js index 75b8e57b2..ce9f13646 100644 --- a/dataedit/static/peer_review/opr_contributor.js +++ b/dataedit/static/peer_review/opr_contributor.js @@ -1,16 +1,6 @@ -// SPDX-FileCopyrightText: 2025 Bryan Lancien © 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 Jonas Huber © Reiner Lemoine Institut -// SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut -// SPDX-FileCopyrightText: 2025 Stephan Uller © Reiner Lemoine Institut -// SPDX-FileCopyrightText: 2025 user © Reiner Lemoine Institut -// +// SPDX-FileCopyrightText: 2025 Reiner Lemoine Institut // SPDX-License-Identifier: AGPL-3.0-or-later - -import * as common from './peer_review.js'; import { hideReviewerOptions, setSelectedField, @@ -22,814 +12,80 @@ import { selectedCategory, setSelectedCategory, checkReviewComplete, - showToast, updateFieldDescription, - highlightSelectedField, initializeEventBindings, + highlightSelectedField, + initializeEventBindings, + selectState, + savePeerReview } from './peer_review.js'; -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; -// OK Field View Change -$('#button').bind('click', hideReviewerOptions); +import { + selectNextField, + switchCategoryTab, + updatePercentageDisplay +} from "./navigation.js"; -$('#ok-button').bind('click', saveEntrances); -// Suggestion Field View Change -$('#suggestion-button').bind('click', showReviewerOptions); -$('#suggestion-button').bind('click', updateSubmitButtonColor); -// Reject Field View Change -$('#rejected-button').bind('click', showReviewerOptions); -$('#rejected-button').bind('click', updateSubmitButtonColor); -// Clear Input fields when new tab is selected -// nav items are selected via their class -$('.nav-link').click(clearInputFields); -// field items selector +import { + renderSummaryPageFields, + updateTabProgressIndicatorClasses +} from "./summary.js"; -/** - * Returns name from cookies - * @param {string} name Key to look up in cookie - * @returns {value} Cookie value - */ -function getCookie(name) { - var cookieValue = null; - if (document.cookie && document.cookie !== "") { - var cookies = document.cookie.split(";"); - for (var i = 0; i < cookies.length; i++) { - var cookie = $.trim(cookies[i]); - if (cookie.substring(0, name.length + 1) === name + "=") { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; -} +import { + isEmptyValue +} from "./utilities.js"; -/** - * Get CSRF Token - * @returns {string} CSRF Token - */ -function getCsrfToken() { - var token1 = getCookie("csrftoken"); - return token1; -} +// --- Local Helpers --- -/** - * Sends JSON to backend url - * @param {string} method Get or post request - * @param {string} url URL to send JSON to - * @param {json} data Data to send to backend - * @param {function} success Success function - * @param {function} error Error function - * @returns {value} AJAX function return - */ -function sendJson(method, url, data, success, error) { - var token = getCsrfToken(); - return $.ajax({ - url: url, - headers: {"X-CSRFToken": token}, - data_type: "json", - cache: false, - contentType: "application/json; charset=utf-8", - processData: false, - data: data, - type: method, - success: success, - error: error, - }); +function updateFieldColor(fieldKey, state) { + // Escaping dots for jQuery selector if needed + const safeId = '#field_' + fieldKey.replace(/\./g, "\\."); + $(safeId).removeClass('field-ok field-suggestion field-rejected'); + $(safeId).addClass(`field-${state}`); } -/** - * Reads error message from response - * @param {json} response Get or post request - * @returns {string} Response error message - */ -function getErrorMsg(response) { - try { - var response_msg = ( - 'Upload failed: ' + JSON.parse(response.responseJSON).error - ); - } catch (e) { - var response_msg = response.responseText; - } - return response_msg; -} +// --- Main Initialization --- -/** - * Configurates peer review - * @param {json} config Configuration JSON from Django backend. - */ -function peerReview(config) { - /* - TODO: Show loading icon if peer review page is loaded - */ +export function initContributor() { + // Contributors typically use the same save mechanism for now + initializeEventBindings(saveEntrancesForContributor); + + // Delegated event listener for field clicks + document.addEventListener('click', function(event) { + const field = event.target.closest('.field'); + if (!field) return; - // (function init() { - // $('#peer_review-loading').removeClass('d-none'); - // config.form = $('#peer_review-form'); - // })(); - selectNextField(); + const fieldKey = field.dataset.fieldkey; + const fieldValue = field.dataset.fieldvalue; + const category = field.dataset.category; + + if (fieldKey && category !== undefined) { + click_field(fieldKey, fieldValue, category); + } + }); + + // Initial renders renderSummaryPageFields(); updateTabProgressIndicatorClasses(); updatePercentageDisplay(); } -/** - * Save peer review to backend - */ -function savePeerReview() { - $('#peer_review-save').removeClass('d-none'); - json = JSON.stringify({reviewType: 'save', reviewData: current_review}); - sendJson("POST", config.url_peer_review, json).then(function() { - window.location = config.url_table; - }).catch(function(err) { - // TODO evaluate error, show user message - $('#peer_review-save').addClass('d-none'); - alert(getErrorMsg(err)); - }); -} - function click_field(fieldKey, fieldValue, category) { - const cleanedFieldKey = fieldKey.replace(/\.\d+/g, ''); - + switchCategoryTab(category); - setSelectedField(fieldKey); - setselectedFieldValue(fieldValue); setSelectedCategory(category); - + updateFieldDescription(cleanedFieldKey, fieldValue); highlightSelectedField(fieldKey); - - const fieldState = getFieldState(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; - }); - } else { - ["ok-button", "rejected-button", "suggestion-button"].forEach(btn => { - document.getElementById(btn).disabled = false; - }); - } - + clearInputFields(); hideReviewerOptions(); } -function clearInputFields() { - document.getElementById("valuearea").value = ""; - document.getElementById("commentarea").value = ""; -} - -/** - * Switch to the category tab if needed - */ -function switchCategoryTab(category) { - const currentTab = document.querySelector('.tab-pane.active'); // Get the currently active tab - const tabIdForCategory = getCategoryToTabIdMapping()[category]; - if (currentTab.getAttribute('id') !== tabIdForCategory) { - // The clicked field does not belong to the current tab, switch to the next tab - const targetTab = document.getElementById(tabIdForCategory); - if (targetTab) { - // The target tab exists, click the tab link to switch to it - targetTab.click(); - } - } -} - -/** - * Function to provide the mapping of category to the correct tab ID - */ -function getCategoryToTabIdMapping() { - // Define the mapping of category to tab ID - const mapping = { - 'general': 'general-tab', - 'spatial': 'spatiotemporal-tab', - 'temporal': 'spatiotemporal-tab', - 'source': 'source-tab', - 'license': 'license-tab', - }; - return mapping; -} - - click_field(fieldKey, fieldValue, category); - }); - }); -}); -/** - * Saves selected state - * @param fieldKey - */ - -export function getFieldStateForContributor(fieldKey) { - // This function gets the state of a field - return state_dict[fieldKey]; - - function selectState(state) { // eslint-disable-line no-unused-vars - selectedState = state; - updateClientStateDict(fieldKey = selectedField, state = state); -} - -function updateClientStateDict(fieldKey, state) { - state_dict = state_dict ?? {}; - if (fieldKey in state_dict) { - // console.log(`Der Schlüssel '${fieldKey}' ist vorhanden.`); - state_dict[fieldKey] = state; - } else { - // console.log(`Der Schlüssel '${fieldKey}' ist nicht vorhanden.`); - state_dict[fieldKey] = state; - } -} - - - - - - -/** - * Renders fields on the Summary page, sorted by review state - */ -/** - * Displays fields based on selected category - */ - -function renderSummaryPageFields() { - const categoriesMap = {}; - - function addFieldToCategory(category, field) { - if (!categoriesMap[category]) categoriesMap[category] = []; - categoriesMap[category].push(field); - - // Removed incorrect use of 'continue' and refactored loop variable declaration - const category_fields = category.querySelectorAll(".field"); - for (const 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'); - let fieldName = field_id.replace(/\./g, ' '); - 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 }); - 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, ' '); - if (fieldCategory !== "general") { - fieldName = fieldName.split(' ').slice(1).join(' '); - } - - let fieldStatus = isEmptyValue(fieldValue) ? 'Empty' : - fieldState === 'ok' ? 'Accepted' : - fieldState === 'rejected' ? 'Rejected' : 'Missing'; - - addFieldToCategory(fieldCategory, { fieldName, fieldValue, fieldStatus }); - }); - - 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 fields = categoriesMap[category]; - const singleFields = []; - const groupedFields = {}; - - fields.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 }); - } - - 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('')} - `; - tabPane.appendChild(table); - } - - 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 Value
${f.fieldStatus}${f.fieldName}${f.fieldValue}
`; - } - - 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 Value
${f.fieldStatus}${f.fieldName}${f.fieldValue}
-
-
-
`; - }); - - innerHTML += `
`; - } - - tabsContent.appendChild(tabPane); - firstTab = false; - } - 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'; - - const allFields = Object.entries(categoriesMap).flatMap(([category, fields]) => - fields.map(f => ({...f, category})) - ); - - viewsPane.innerHTML = - - - - - ${allFields.map(f => - - - - - - ).join('')} - -
StatusCategoryField NameField Value
${f.fieldStatus}${f.category}${f.fieldName}${f.fieldValue}
; - - 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 - * @returns {string} HTML list of fields - */ -function createFieldList(fields) { - return ` -
    - ${fields.map((field) => `
  • ${field.fieldCategory}: ${field.fieldValue}
  • `).join('')} -
- `; -} - -// // Function to show the error toast -// function showErrorToast(liveToast) { -// liveToast.show(); -// } - -function showToast(title, message, type) { - var toast = document.getElementById('liveToast'); - var toastTitle = document.getElementById('toastTitle'); - var toastBody = document.getElementById('toastBody'); - - // Update the toast's header and body based on the type - if (type === 'error') { - toast.classList.remove('bg-success'); - toast.classList.add('bg-danger'); - } else if (type === 'success') { - toast.classList.remove('bg-danger'); - toast.classList.add('bg-success'); - } - - // Set the title and body text - toastTitle.textContent = title; - toastBody.textContent = message; - - var bsToast = new bootstrap.Toast(toast); - bsToast.show(); -} - -setGetFieldState(getFieldStateForContributor); - function saveEntrancesForContributor() { - - 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'); - showToast("Error", "The value suggestion text field is required to save the field review!", "error"); - return; // Stop execution if validation fails - } else { - valuearea.setCustomValidity(''); - } - - 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); - if (fieldElement) { - var valueElement = fieldElement.querySelector('.value'); - if (valueElement) { - valueElement.innerText = initialReviewerSuggestions[selectedField]; - } - } - } - - - if (Object.keys(current_review["reviews"]).length === 0 && - current_review["reviews"].constructor === Object) { - current_review["reviews"] = []; - } - - if (selectedField) { - var reviewFound = false; - - for (let i = 0; i < current_review["reviews"].length; i++) { - if (current_review["reviews"][i]["key"] === selectedField) { - reviewFound = true; - // console.log("review" + current_review.reviews["reviews"][i]["fieldReview"]) //undefined "reviews" - console.log("review" + current_review["reviews"][i]["fieldReview"]) //undefined "reviews" - if (!Array.isArray(current_review["reviews"][i]["fieldReview"])) { - current_review["reviews"][i]["fieldReview"] = [current_review["reviews"][i]["fieldReview"]]; - } - var element = document.querySelector('[aria-selected="true"]'); - var category = element.getAttribute("data-bs-target"); - current_review["reviews"][i]["fieldReview"].push({ - "timestamp": Date.now(), - "user": "oep_contributor", // TODO put actual username - "role": "contributor", - "contributorValue": selectedFieldValue, - "newValue": selectedState === "ok" ? initialReviewerSuggestions[selectedField] : "", - "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 suggestionElement = fieldElement.querySelector('.suggestion--highlight'); - var commentElement = fieldElement.querySelector('.suggestion--comment'); - // var additionalCommentElement = fieldElement.querySelector('.suggestion--additional-comment'); - suggestionElement.innerText = document.getElementById("valuearea").value; - commentElement.innerText = document.getElementById("commentarea").value; - // additionalCommentElement.innerText = document.getElementById("comments").value; - break; - } - } - - if (!reviewFound) { - var element = document.querySelector('[aria-selected="true"]'); - var category = element.getAttribute("data-bs-target"); - current_review["reviews"].push({ - "category": selectedCategory, - "key": selectedField, - "fieldReview": [ - { - "timestamp": Date.now(), - "user": "oep_contributor", // TODO put actual username - "role": "contributor", - "contributorValue": selectedFieldValue, - "newValue": selectedState === "ok" ? initialReviewerSuggestions[selectedField] : "", - "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 suggestionElement = fieldElement.querySelector('.suggestion--highlight'); - var commentElement = fieldElement.querySelector('.suggestion--comment'); - var additionalCommentElement = fieldElement.querySelector('.suggestion--additional-comment'); // For new comment - - 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 = ""; - updateFieldColor(); - checkReviewComplete(); - selectNextField(); - renderSummaryPageFields(); - updateTabProgressIndicatorClasses(); - updatePercentageDisplay() ; -} -initializeEventBindings(saveEntrancesForContributor); -}}} - -/** - * - * Checks if all fields are reviewed and activates submit button if ready - */ -function checkReviewComplete() { - const fields = document.querySelectorAll('.field'); - for (let field of fields) { - let fieldName = field.id.slice(6); - const fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); - const fieldState = getFieldState(fieldName); - let reviewed = current_review["reviews"].find((review) => review.key === fieldName); - - if (!reviewed && fieldState !== 'ok' && !isEmptyValue(fieldValue)) { - $('#submit_summary').addClass('disabled'); - return; - } - } - $('#submit_summary').removeClass('disabled'); - showToast("Success", "You have reviewed all fields and can submit the review to get feedback!", 'success'); -} - - - - -/** - * Shows reviewer Comment and Suggestion Input options - */ -function showReviewerOptions() { - $("#reviewer_remarks").removeClass('d-none'); -} - -/** - * Hides reviewer Comment and Suggestion Input options - */ -function hideReviewerOptions() { - $("#reviewer_remarks").addClass('d-none'); -} - -/** - * Colors Field based on Reviewer input - */ -function updateFieldColor() { - // Color ok/suggestion/rejected - field_id = `#field_${selectedField}`.replaceAll(".", "\\."); - $(field_id).removeClass('field-ok'); - $(field_id).removeClass('field-suggestion'); - $(field_id).removeClass('field-rejected'); - $(field_id).addClass(`field-${selectedState}`); -} - -/** - * Colors Field based on Reviewer input - */ -function updateSubmitButtonColor() { - // Color Save comment / new value - $(submitButton).removeClass('btn-warning'); - $(submitButton).removeClass('btn-danger'); - if (selectedState == "suggestion") { - $(submitButton).addClass('btn-warning'); - } else { - $(submitButton).addClass('btn-danger'); - } -} - - -function updateTabProgressIndicatorClasses() { - const tabNames = ['general', 'spatiotemporal', 'source', 'license']; - - for (let i = 0; i < tabNames.length; i++) { - let tabName = tabNames[i]; - let tab = document.getElementById(tabName + '-tab'); - if (!tab) continue; - - let fieldsInTab = Array.from(document.querySelectorAll('#' + tabName + ' .field')); - - let allOk = fieldsInTab.every((field) => { - const fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); - return isEmptyValue(fieldValue) || field.classList.contains('field-ok'); - }); - - if (allOk) { - tab.classList.add('status--done'); - } else { - tab.classList.add('status'); - } - } -} - -function updateTabClasses() { - const tabNames = ['general', 'spatiotemporal', 'source', 'license']; - for (let i = 0; i < tabNames.length; i++) { - let tabName = tabNames[i]; - let tab = document.getElementById(tabName + '-tab'); - if (!tab) continue; - - let fields = Array.from(document.querySelectorAll('#' + tabName + ' .field')); - - let allOk = true; - for (let j = 0; j < fields.length; j++) { - let fieldState = getFieldState(fields[j].id.replace('field_', '')); - if (fieldState !== 'ok') { - allOk = false; - break; - } - } - if (allOk) { - tab.classList.add('status'); - tab.classList.add('status--done'); - } else { - tab.classList.add('status'); - } - } -} -window.addEventListener('DOMContentLoaded', function() { - updateTabClasses(); - updatePercentageDisplay() ; -}); - -function calculateOkPercentage(stateDict) { - let totalCount = 0; - let okCount = 0; - - for (let key in stateDict) { - let fieldValue = $(document.getElementById(`field_${key}`)).find('.value').text().replace(/\s+/g, ' ').trim(); - if (!isEmptyValue(fieldValue)) { - totalCount++; - if (stateDict[key] === "ok") { - okCount++; - } - } - } - - return totalCount === 0 ? 0 : (okCount / totalCount) * 100; -} - -function updatePercentageDisplay() { - const percentage = calculateOkPercentage(state_dict); - document.getElementById("percentageDisplay").textContent = percentage.toFixed(2); -} - - -/** - * Hide and show revier controles once the user clicks the summary tab - */ -console.log('Script is running...'); - -document.addEventListener('DOMContentLoaded', function() { - // console.log('DOM fully loaded and parsed'); - - const summaryTab = document.getElementById('summary-tab'); - const otherTabs = [ - document.getElementById('general-tab'), - document.getElementById('spatiotemporal-tab'), - document.getElementById('source-tab'), - document.getElementById('license-tab'), - ]; - const reviewContent = document.querySelector(".review__content"); - - console.log('Summary Tab:', summaryTab); - console.log('Other Tabs:', otherTabs); - console.log('Review Content:', reviewContent); - - if (summaryTab && reviewContent) { - summaryTab.addEventListener('click', function() { - toggleReviewControls(false); - reviewContent.classList.toggle("tab-pane--100"); - }); - } else { - console.error('Summary tab or review content not found'); - } - - otherTabs.forEach(function(tab, index) { - if (tab) { - tab.addEventListener('click', function() { - toggleReviewControls(true); - reviewContent.classList.remove("tab-pane--100"); - }); - } else { - console.error('Tab at index ' + index + ' not found'); - } - }); - - function toggleReviewControls(show) { - const reviewControls = document.querySelector('.review__controls'); - console.log('Review Controls:', reviewControls); - if (reviewControls) { - reviewControls.style.display = show ? '' : 'none'; - } - } -}); - - -peerReview(config); + // Basic logic for contributor saving actions + checkReviewComplete(); + selectNextField(); +} \ No newline at end of file diff --git a/dataedit/static/peer_review/opr_reviewer.js b/dataedit/static/peer_review/opr_reviewer.js index 2154bad00..3c144ce69 100644 --- a/dataedit/static/peer_review/opr_reviewer.js +++ b/dataedit/static/peer_review/opr_reviewer.js @@ -1,117 +1,104 @@ -// SPDX-FileCopyrightText: 2025 Bryan Lancien © Reiner Lemoine Institut -// SPDX-FileCopyrightText: 2025 Daryna Barabanova © 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 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 Stephan Uller © Reiner Lemoine Institut -// SPDX-FileCopyrightText: 2025 user © Reiner Lemoine Institut -// +// SPDX-FileCopyrightText: 2025 Reiner Lemoine Institut // SPDX-License-Identifier: AGPL-3.0-or-later -// this raises more errors as transition from script to module -// makes it more complicated to use onclick in html elements -// import { updateClientStateDict } from './frontend/state.js' -import * as common from './peer_review.js'; import { - hideReviewerOptions, - setSelectedField, - setselectedFieldValue, - clearInputFields, - selectedState, - selectedFieldValue, - current_review, - selectedCategory, - setSelectedCategory, - checkReviewComplete, - showToast, - highlightSelectedField, updateFieldDescription, initializeEventBindings, makeFieldList - + hideReviewerOptions, + showReviewerOptions, + showReviewerCommentsOptions, + hideReviewerCommentsOptions, // Now available + setSelectedField, + setselectedFieldValue, + clearInputFields, + selectedState, + selectedFieldValue, + current_review, + selectedCategory, + setSelectedCategory, + checkReviewComplete, + showToast, + highlightSelectedField, + updateFieldDescription, + initializeEventBindings, + getErrorMsg, + sendJson } from './peer_review.js'; -window.selectState = common.selectState; - - -import {check_if_review_finished, checkFieldStates } from './opr_reviewer_logic.js'; -import {getFieldState, setGetFieldState} from "./state_current_review.js"; -import {selectNextField, switchCategoryTab} from "./navigation.js"; -import {renderSummaryPageFields, updateTabProgressIndicatorClasses} from "./summary.js"; -import {isEmptyValue, updateFieldColor} from "./utilities.js"; +import { check_if_review_finished } from './opr_reviewer_logic.js'; +import { getFieldState, setGetFieldState, updateClientStateDict } from "./state_current_review.js"; +import { switchCategoryTab, selectNextField, updatePercentageDisplay } from "./navigation.js"; +import { renderSummaryPageFields, updateTabProgressIndicatorClasses } from "./summary.js"; +import {isEmptyValue, isEffectivelyEmpty} from "./utilities.js";window.clientSideReviewFinished = window.clientSideReviewFinished ?? false; +let initialReviewerSuggestions = {}; window.clientSideReviewFinished = window.clientSideReviewFinished ?? false; +export function initReviewer() { + initializeEventBindings(saveEntrancesForReviewer); + + $('#peer_review-delete').off('click').on('click', deletePeerReview); + + // Toggle Logic for Reviewer Buttons + $('#ok-button').on('click', () => { + hideReviewerOptions(); + hideReviewerCommentsOptions(); + }); + $('#suggestion-button').on('click', () => { + showReviewerOptions(); + hideReviewerCommentsOptions(); + }); + $('#rejected-button').on('click', () => { + hideReviewerOptions(); + showReviewerCommentsOptions(); + }); -// Delete review -$('#peer_review-delete').bind('click', deletePeerReview); -// OK Field View Change -$('#ok-button').bind('click', hideReviewerOptions); -// Suggestion Field View Change -$('#suggestion-button').bind('click', hideReviewerCommentOptions); -// Reject Field View Change -$('#rejected-button').bind('click', hideReviewerOptions); + document.addEventListener('click', function (event) { + const field = event.target.closest('.field'); + if (!field) return; -/** - * Configurates peer review - * @param {json} config Configuration JSON from Django backend. - */ -function peerReview(config) { - /* - TODO: Show loading icon if peer review page is loaded - */ + const fieldKey = field.dataset.fieldkey; + const fieldValue = field.dataset.fieldvalue; + const category = field.dataset.category; - // (function init() { - // $('#peer_review-loading').removeClass('d-none'); - // config.form = $('#peer_review-form'); - // })(); + if (fieldKey && category !== undefined) { + click_field(fieldKey, fieldValue, category); + } + }); + document.querySelectorAll(".suggestion--highlight").forEach(function (suggestion) { + var field = suggestion.id.split("_")[1]; + if(field) initialReviewerSuggestions[field] = suggestion.innerText; + }); - selectNextField(); renderSummaryPageFields(); updateTabProgressIndicatorClasses(); updatePercentageDisplay(); - if (state_dict) { + + if (typeof window.state_dict !== 'undefined') { check_if_review_finished(); - -function deletePeerReview() { - // confirm - if (!confirm("Are you sure?")) { - return; } +} - const json = JSON.stringify({ reviewType: 'delete', reviewData: current_review, review_id: current_review.review_id || config.review_id}); +function deletePeerReview() { + if (!confirm("Are you sure?")) return; + const json = JSON.stringify({ + reviewType: 'delete', + reviewData: current_review, + review_id: current_review.review_id || config.review_id + }); $('#peer_review-delete').addClass('d-none'); - sendJson("POST", config.url_peer_review, json) - .then(function() { - window.location = config.url_table; - }) - .catch(function(err) { + .then(() => window.location = config.url_table) + .catch((err) => { $('#peer_review-delete').removeClass('d-none'); alert(getErrorMsg(err)); }); } -/** - * Finish peer review and save to backend - */ - -/** - * Identifies field name and value sets selected stlye and refreshes - * reviewer box (side panel) infos. - * @param value - */ - -var fieldEvaluations = {}; // Object for tracking evaluated fields - function click_field(fieldKey, fieldValue, category) { - const isEmpty = isEmptyValue(fieldValue); + const isEmpty = isEffectivelyEmpty(fieldKey, fieldValue); const cleanedFieldKey = fieldKey.replace(/\.\d+/g, ''); switchCategoryTab(category); setSelectedField(fieldKey); - setselectedFieldValue(fieldValue); setSelectedCategory(category); @@ -119,456 +106,141 @@ function click_field(fieldKey, fieldValue, category) { highlightSelectedField(fieldKey); const fieldState = getFieldState(fieldKey); - const fieldWasEvaluated = fieldEvaluations[fieldKey]; - - if (fieldState) { - if (fieldState === 'ok' && !fieldWasEvaluated) { - ["ok-button", "rejected-button", "suggestion-button"].forEach(btn => { - const buttonEl = document.getElementById(btn); - if (buttonEl) { - buttonEl.disabled = true; - } -}); + + // Enable/Disable buttons based on state + // 2. Logic to Enable/Disable buttons + // If it's empty, buttons MUST be disabled, regardless of previous state (unless you want to allow un-reviewing, but generally empty = no action) + const buttons = ["ok-button", "rejected-button", "suggestion-button"]; + + buttons.forEach(btn => { + const el = document.getElementById(btn); + if (isEmpty) { + el.disabled = true; // Force disable if empty + } else { + // If not empty, disable if no state is selected yet? + // Actually, in the original code, buttons were enabled if a state existed. + // But usually, you want buttons enabled so you CAN select a state. + // Let's assume buttons should be enabled if the field has content. + el.disabled = false; + } + }); - } else if (['suggestion', 'rejected'].includes(fieldState) || fieldWasEvaluated) { - ["ok-button", "rejected-button", "suggestion-button"].forEach(btn => { - document.getElementById(btn).disabled = false; - }); + // 3. Handle empty field messages + // We need to escape the selector for jQuery/querySelector because keys can have dots + const safeFieldKey = CSS.escape(fieldKey); // Native JS escape + // Or manually if you prefer: fieldKey.replace(/(:|\.|\[|\]|,|=|@)/g, "\\$1"); + + + // Handle empty field messages + 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}`); + const labelEl = fieldElementForMsg.querySelector('.key'); + const valueEl = fieldElementForMsg.querySelector('.value'); + + 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); + } + if(labelEl) labelEl.style.color = '#6c757d'; + if(valueEl) valueEl.style.color = '#6c757d'; + } else { + if (explanationElement) explanationElement.remove(); + if(labelEl) labelEl.style.color = ''; + if(valueEl) valueEl.style.color = ''; } - } else { - ["ok-button", "rejected-button", "suggestion-button"].forEach(btn => { - const buttonEl = document.getElementById(btn); - if (buttonEl) { - buttonEl.disabled = isEmpty; } -}); - -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); - } -} - - - document.getElementById("ok-button").addEventListener('click', () => { - fieldEvaluations[fieldKey] = 'ok'; - }); - document.getElementById("rejected-button").addEventListener('click', () => { - fieldEvaluations[fieldKey] = 'rejected'; - }); - document.getElementById("suggestion-button").addEventListener('click', () => { - fieldEvaluations[fieldKey] = 'suggestion'; - }); + // Reset UI state for new selection clearInputFields(); hideReviewerOptions(); - hideReviewerCommentOptions(); -} + hideReviewerCommentsOptions(); } -document.addEventListener('DOMContentLoaded', function() { - document.addEventListener('click', function (event) { - const field = event.target.closest('.field'); - if (!field) return; - - const fieldKey = field.dataset.fieldkey; - const fieldValue = field.dataset.fieldvalue; - const category = field.dataset.category; - - if (fieldKey && category !== undefined) { - click_field(fieldKey, fieldValue, category); - } - } - 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 Value'; - thead.appendChild(header); - table.appendChild(thead); - - let tbody = document.createElement('tbody'); - - data.forEach((item) => { - let row = document.createElement('tr'); - - let th = document.createElement('th'); - th.scope = "row"; - th.className = "status"; - if (item.fieldStatus === "Missing") { - th.className = "status missing"; - } - 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.field_id; - row.appendChild(tdFieldId); - - let tdFieldValue = document.createElement('td'); - tdFieldValue.textContent = item.fieldValue; - row.appendChild(tdFieldValue); - - tbody.appendChild(row); - }); - - table.appendChild(tbody); - - return table; - } - - - 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); - } - - updateSummaryTable(); - updateTabProgressIndicatorClasses(); - updatePercentageDisplay(); +function updateFieldColor(fieldKey, state) { + const safeId = '#field_' + fieldKey.replace(/\./g, "\\."); + $(safeId).removeClass('field-ok field-suggestion field-rejected'); + $(safeId).addClass(`field-${state}`); } - }); -}); -window.click_field = click_field; -/** - * Saves field review to current review list - */ function saveEntrancesForReviewer() { if (selectedState === "rejected") { const comments = document.getElementById('comments'); - if (comments.value.trim() === '') { - comments.setCustomValidity('Comment is required'); - showToast("Error", "The comment text field is required to save the field review!", "error"); + showToast("Error", "Comment is required for rejection!", "error"); return; - } else { - comments.setCustomValidity(''); } - - valuearea.reportValidity(); - } - // If the field state is neither "ok" nor "rejected", user input should be checked for suggestions - if (selectedState !== "ok" && selectedState !== "rejected") { + } else if (selectedState === "suggestion") { const valuearea = document.getElementById('valuearea'); - if (valuearea.value.trim() === '') { - valuearea.setCustomValidity('Value suggestion is required'); - showToast("Error", "The value suggestion text field is required to save the field review!", "error"); + showToast("Error", "Value suggestion is required!", "error"); return; - } else { - valuearea.setCustomValidity(''); - } - - valuearea.reportValidity(); - } else if (selectedState === "ok") { - var fieldElement = document.getElementById("field_" + selectedField); - if (fieldElement) { - var valueElement = fieldElement.querySelector('.value'); - if (valueElement) { - // Check if the suggested value was present before the page loaded - if (initialReviewerSuggestions[selectedField] && initialReviewerSuggestions[selectedField].trim() !== '') { - // If the proposed value was previous, then we overwrite the original value with this proposal. - valueElement.innerText = initialReviewerSuggestions[selectedField]; - } else { - // Otherwise, set the original value - valueElement.innerText = selectedFieldValue; - } - } - - document.getElementById('valuearea').value = ''; - document.getElementById('commentarea').value = ''; - - var suggestionElement = fieldElement.querySelector('.suggestion--highlight'); - if (suggestionElement) { - suggestionElement.innerText = ''; // Clearing the proposed value - } - - if (initialReviewerSuggestions[selectedField]) { - initialReviewerSuggestions[selectedField] = ''; // Resetting a previously saved proposal - } } } - if (selectedField) { - var fieldExists = false; + if (selectedState === "ok") { + clearInputFields(); + } - current_review["reviews"].forEach(function(review, idx) { - if (review["key"] === selectedField) { + if (window.selectedField) { + const currentKey = window.selectedField; + let fieldExists = false; + + const reviewObj = { + "timestamp": Date.now(), + "user": "oep_reviewer", + "role": "reviewer", + "contributorValue": selectedFieldValue, + "newValue": (selectedState === "suggestion") ? document.getElementById("valuearea").value : "", + "comment": document.getElementById("commentarea").value, + "additionalComment": document.getElementById("comments").value, + "reviewerSuggestion": (selectedState === "suggestion") ? document.getElementById("valuearea").value : "", + "state": selectedState, + }; + + current_review.reviews.forEach(function(review, idx) { + if (review["key"] === currentKey) { fieldExists = true; - - if (selectedState === "ok" || selectedState === "rejected") { - Object.assign(current_review["reviews"][idx], { - "category": selectedCategory, - "key": selectedField, - "fieldReview": { - "timestamp": Date.now(), - "user": "oep_reviewer", - "role": "reviewer", - "contributorValue": selectedFieldValue, - // If there was a suggested value before loading, save it as the new value - "newValue": initialReviewerSuggestions[selectedField] ? initialReviewerSuggestions[selectedField] : "", - "comment": document.getElementById("commentarea").value, - "additionalComment": document.getElementById("comments").value, - "reviewerSuggestion": "", - "state": selectedState, - }, - }); - } else if (selectedState === "suggest" ){ - Object.assign(current_review["reviews"][idx], { - "category": selectedCategory, - "key": selectedField, - "fieldReview": { - "timestamp": Date.now(), - "user": "oep_reviewer", - "role": "reviewer", - "contributorValue": selectedFieldValue, - "newValue": document.getElementById("valuearea").value, - "comment": document.getElementById("commentarea").value, - "additionalComment": document.getElementById("comments").value, - "reviewerSuggestion": document.getElementById("valuearea").value, - "state": selectedState, - }, - }); - - var fieldElement = document.getElementById("field_" + selectedField); - if (fieldElement) { - var suggestionElement = fieldElement.querySelector('.suggestion--highlight'); - var additionalCommentElement = fieldElement.querySelector('.suggestion--additional-comment'); - if (suggestionElement) { - suggestionElement.innerText = document.getElementById("valuearea").value; - } if (additionalCommentElement) { - additionalCommentElement.innerText = document.getElementById("comments").value; - } - } - } + Object.assign(current_review["reviews"][idx], { + "category": selectedCategory, + "fieldReview": reviewObj + }); } }); if (!fieldExists) { - current_review["reviews"].push({ + current_review.reviews.push({ "category": selectedCategory, - "key": selectedField, - "fieldReview": { - "timestamp": Date.now(), - "user": "oep_reviewer", - "role": "reviewer", - "contributorValue": selectedFieldValue, - "newValue": selectedState === "ok" ? (initialReviewerSuggestions[selectedField] || "") : document.getElementById("valuearea").value, - "comment": document.getElementById("commentarea").value, - "additionalComment": document.getElementById("comments").value, - "reviewerSuggestion": selectedState === "ok" ? "" : document.getElementById("valuearea").value, - "state": selectedState, - }, + "key": currentKey, + "fieldReview": reviewObj }); + } - var fieldElement = document.getElementById("field_" + selectedField); - if (fieldElement) { - var suggestionElement = fieldElement.querySelector('.suggestion--highlight'); - var additionalCommentElement = fieldElement.querySelector('.suggestion--additional-comment'); - if (suggestionElement) { - suggestionElement.innerText = document.getElementById("valuearea").value; - } if (additionalCommentElement) { - additionalCommentElement.innerText = document.getElementById("comments").value; - } - } + updateFieldColor(currentKey, selectedState); + updateClientStateDict(currentKey, selectedState); + + // Update DOM suggestions immediately + const fieldElement = document.getElementById("field_" + currentKey); + if (fieldElement) { + const suggEl = fieldElement.querySelector('.suggestion--highlight'); + if (suggEl) suggEl.innerText = reviewObj.reviewerSuggestion; + + const commEl = fieldElement.querySelector('.suggestion--additional-comment'); + if (commEl) commEl.innerText = reviewObj.additionalComment; } } - updateFieldColor(); - if (selectedState === "ok" ) { - document.getElementById("valuearea").value = ""; - document.getElementById("commentarea").value = ""; - } document.getElementById("comments").value = ""; checkReviewComplete(); - selectNextField(); renderSummaryPageFields(); updateTabProgressIndicatorClasses(); + updatePercentageDisplay(); + + selectNextField(); check_if_review_finished(); -} - -initializeEventBindings(saveEntrancesForReviewer); - - - renderSummaryPageFields(); - updateTabProgressIndicatorClasses(); - updatePercentageDisplay(); -} - -function getFieldState(fieldKey) { - if (state_dict && state_dict[fieldKey] !== undefined) { - return state_dict[fieldKey]; - } else { - // I dont like that this shows as a error in the console - // console.log(`Cannot get state for fieldKey "${fieldKey}" because it is not found in stateDict or stateDict itself is null.`); - return null; - } -} - -/** - * Checks if all fields are reviewed and activates submit button if ready - */ -/** - * Returns a list of all fields and their values. - * @returns {Array} List of objects with field names and values. - */ - - - -export function getFieldStateForReviewer(fieldKey) { - if (window.state_dict && window.state_dict[fieldKey] !== undefined) { - return window.state_dict[fieldKey]; - } else { - // I don't like that this shows as an error in the console.log(Cannot get state for fieldKey "${fieldKey}" - // because it is not found in stateDict or stateDict itself is null.); - return null; - } -} - -setGetFieldState(getFieldStateForReviewer); - -/** - * Checks if all fields are accepted and activates award badge div to finish the review. - * Also deactivates the submitbutton. - */ - -function hideReviewerCommentOptions() { - $("#reviewer_comments").addClass('d-none'); -} - -/** - * Shows reviewer Comment and Suggestion Input options - */ -function showReviewerOptions() { - $("#reviewer_remarks").removeClass('d-none'); -} - -/** - * Hides reviewer Comment and Suggestion Input options - */ -function hideReviewerOptions() { - $("#reviewer_remarks").addClass('d-none'); -} - -/** - * Colors Field based on Reviewer input - */ -function updateFieldColor() { - // Color ok/suggestion/rejected - field_id = `#field_${selectedField}`.replaceAll(".", "\\."); - // console.log(field_id) - $(field_id).removeClass('field-ok'); - $(field_id).removeClass('field-suggestion'); - $(field_id).removeClass('field-rejected'); - $(field_id).addClass(`field-${selectedState}`); -} - -/** - * Colors Field based on Reviewer input - */ -function updateSubmitButtonColor() { - // Color Save comment / new value - $(submitButton).removeClass('btn-warning'); - $(submitButton).removeClass('btn-danger'); - if (selectedState === "suggestion") { - $(submitButton).addClass('btn-warning'); - } else { - $(submitButton).addClass('btn-danger'); - } -} - -/** - * Hide and show revier controles once the user clicks the summary tab - */ -const summaryTab = document.getElementById('summary-tab'); -const otherTabs = [ - document.getElementById('general-tab'), - document.getElementById('spatiotemporal-tab'), - document.getElementById('source-tab'), - document.getElementById('license-tab'), -]; -const reviewContent = document.querySelector(".review__content"); -function updateTabClasses() { - const tabNames = ['general', 'spatiotemporal', 'source', 'license']; - for (let i = 0; i < tabNames.length; i++) { - let tabName = tabNames[i]; - let tab = document.getElementById(tabName + '-tab'); - if (!tab) continue; - - let fields = Array.from(document.querySelectorAll('#' + tabName + ' .field')); - - let allOkOrEmpty = fields.every(field => { - let fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); - let fieldState = getFieldState(field.id.replace('field_', '')); - return isEmptyValue(fieldValue) || fieldState === 'ok'; - }); - - if (allOkOrEmpty) { - tab.classList.add('status'); - tab.classList.add('status--done'); - } else { - tab.classList.add('status'); - } - } -} - - -window.addEventListener('DOMContentLoaded', function() { - updateTabClasses(); - updatePercentageDisplay() ; - updateTabClasses(); -}); - - - - -function getTotalFieldCount() { - var allFields = makeFieldList(); - return allFields.length; -} - -function calculateOkPercentage(stateDict) { - let totalCount = 0; - let okCount = 0; - - for (let key in stateDict) { - let fieldValue = $(document.getElementById(`field_${key}`)).find('.value').text().replace(/\s+/g, ' ').trim(); - if (!isEmptyValue(fieldValue)) { - totalCount++; - if (stateDict[key] === "ok") { - okCount++; - } - } - } - - let percentage = totalCount === 0 ? 0 : (okCount / totalCount) * 100; - return percentage.toFixed(2); -} - -function updatePercentageDisplay() { - document.getElementById("percentageDisplay").textContent = calculateOkPercentage(window.state_dict); -} +} \ No newline at end of file diff --git a/dataedit/static/peer_review/opr_reviewer_logic.js b/dataedit/static/peer_review/opr_reviewer_logic.js index 892755b03..bde1ec1d0 100644 --- a/dataedit/static/peer_review/opr_reviewer_logic.js +++ b/dataedit/static/peer_review/opr_reviewer_logic.js @@ -1,8 +1,7 @@ // SPDX-FileCopyrightText: 2025 Reiner Lemoine Institut // SPDX-License-Identifier: AGPL-3.0-or-later import {current_review, getAllFieldsAndValues, getErrorMsg, showToast} from "./peer_review.js"; -import {isEmptyValue, sendJson} from "./utilities.js"; -import {getFieldState} from "./state_current_review.js"; +import {isEmptyValue, isEffectivelyEmpty, sendJson} from "./utilities.js";import {getFieldState} from "./state_current_review.js"; export function finishPeerReview() { $('#peer_review-submitting').removeClass('d-none'); @@ -61,15 +60,15 @@ export function check_if_review_finished() { export function checkFieldStates() { const allFields = getAllFieldsAndValues(); - for (const { fieldName, fieldValue } of allFields) { - if (!isEmptyValue(fieldValue)) { + console.log(fieldName, fieldValue) + if (!isEffectivelyEmpty(fieldName, fieldValue)) { const fieldState = getFieldState(fieldName); - if (fieldState !== 'ok' && fieldState !== 'rejected') { + if (fieldState !== 'ok' && fieldState !== 'rejected' && fieldState !== 'suggestion') { return false; } } } return true; -} +} \ No newline at end of file diff --git a/dataedit/static/peer_review/peer_review.js b/dataedit/static/peer_review/peer_review.js index 47d753aba..5b9e11b08 100644 --- a/dataedit/static/peer_review/peer_review.js +++ b/dataedit/static/peer_review/peer_review.js @@ -1,33 +1,77 @@ // SPDX-FileCopyrightText: 2025 Reiner Lemoine Institut // SPDX-License-Identifier: AGPL-3.0-or-later -import { - checkFieldStates, - check_if_review_finished -} from './opr_reviewer_logic.js'; -import {renderSummaryPageFields, updateSubmitButtonColor, updateTabProgressIndicatorClasses} from "./summary.js"; -import {selectNextField} from "./navigation.js"; -import {isEmptyValue, sendJson, getCookie} from "./utilities.js"; -import {getFieldState, updateClientStateDict} from "./state_current_review.js"; +import { check_if_review_finished } from './opr_reviewer_logic.js'; +import { renderSummaryPageFields, updateSubmitButtonColor, updateTabProgressIndicatorClasses } from "./summary.js"; +import { selectNextField, updatePercentageDisplay } from "./navigation.js"; +import {isEmptyValue, isEffectivelyEmpty, sendJson, getCookie, getErrorMsg} from "./utilities.js"; +import { getFieldState, updateClientStateDict } from "./state_current_review.js"; + +// Re-export utilities for other modules +export { getCookie, sendJson, getErrorMsg }; + +// --- DOM Helpers --- +function getFieldElByKey(fieldKey) { + return document.getElementById(`field_${fieldKey}`); +} + +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?.dataset?.description; + + if (attr && String(attr).trim()) return String(attr).trim(); + + return getTextFromEl(fieldEl, ['.help-text', '.description']) || ''; +} + +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// --- State Management --- window.selectedField = window.selectedField ?? null; -export let selectedFieldValue=null; +export let selectedFieldValue = null; +export let selectedState; +export let selectedCategory; +export let current_review; export function setSelectedField(fieldKey) { - selectedField = fieldKey; + window.selectedField = fieldKey; } export function setselectedFieldValue(fieldValue) { selectedFieldValue = fieldValue; } -export let selectedState; -export let selectedCategory; - export function setSelectedCategory(value) { selectedCategory = value; } -export let current_review; - export function initCurrentReview(config) { current_review = { topic: config.topic, @@ -50,95 +94,100 @@ export function initCurrentReview(config) { window.current_review = current_review; } +// --- Event Bindings --- export function initializeEventBindings(saveEntrancesFn) { - // Submit field review - $('#submitButton').bind('click', saveEntrancesFn); - $('#submitCommentButton').bind('click', saveEntrancesFn); - $('#submitButton').bind('click', hideReviewerOptions); - $('#submitCommentButton').bind('click', hideReviewerOptions); - - // Submit review (visible to contributor) - $('#submit_summary').bind('click', submitPeerReview); - // save the current review (not visible to contributor) - $('#peer_review-save').bind('click', savePeerReview); - // Cancel review - $('#peer_review-cancel').bind('click', cancelPeerReview); - $('#ok-button').bind('click', saveEntrancesFn); - - // Suggestion Field View Change - $('#suggestion-button').bind('click', showReviewerOptions); - $('#suggestion-button').bind('click', updateSubmitButtonColor); - - // Reject Field View Change - $('#rejected-button').bind('click', showReviewerCommentsOptions); - $('#rejected-button').bind('click', updateSubmitButtonColor); - - // Clear Input fields when new tab is selected - $('.nav-link').click(clearInputFields); -} + // Save actions + $('#submitButton').off('click').on('click', saveEntrancesFn); + $('#submitCommentButton').off('click').on('click', saveEntrancesFn); + $('#ok-button').off('click').on('click', saveEntrancesFn); + + // Global Review Actions + $('#submit_summary').off('click').on('click', submitPeerReview); + $('#peer_review-save').off('click').on('click', savePeerReview); + $('#peer_review-cancel').off('click').on('click', cancelPeerReview); + + // Button State Toggles (Visual) + $('#suggestion-button').off('click').on('click', () => { + selectState('suggestion'); + updateSubmitButtonColor(); + }); + $('#rejected-button').off('click').on('click', () => { + selectState('rejected'); + updateSubmitButtonColor(); + }); + $('#ok-button').on('click', () => { + selectState('ok'); + updateSubmitButtonColor(); + }); -/** - * Returns name from cookies - * @param {string} name Key to look up in cookie - * @returns {value} Cookie value - */ - -/** - * Get CSRF Token - * @returns {string} CSRF Token - */ -export function getCsrfToken() { - var token1 = getCookie("csrftoken"); - return token1; + $('.nav-link').click(clearInputFields); } -/** - * Sends JSON to backend url - * @param {string} method Get or post request - * @param {string} url URL to send JSON to - * @param {json} data Data to send to backend - * @param {function} success Success function - * @param {function} error Error function - * @returns {value} AJAX function return - */ - +// --- Helper Functions --- export function getAllFieldsAndValues() { const fields = document.querySelectorAll('.field'); const fieldList = []; - fields.forEach(field => { const fieldName = field.id.slice(6); const fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); fieldList.push({ fieldName, fieldValue }); }); - return fieldList; } -/** - * Reads error message from response - * @param {json} response Get or post request - * @returns {string} Response error message - */ -export function getErrorMsg(response) { - try { - var response_msg = ( - 'Upload failed: ' + JSON.parse(response.responseJSON).error - ); - } catch (e) { - console.log(response); - var response_msg = response.responseText; + +export function makeFieldList() { + var fieldElements = []; + $(".field").each(function() { + fieldElements.push(this.id); + }); + return fieldElements; +} + +export function selectField(fieldList, field) { + if (field >= 0 && field < fieldList.length) { + var element = fieldList[field]; + document.getElementById(element).click(); } - return response_msg; } -export function peerReview(config, checkState = false) { - /* - TODO: Show loading icon if peer review page is loaded - */ +export function selectState(state, shouldUpdateClient = false) { + selectedState = state; + const selectedKey = window.selectedField; + if (selectedKey) { + updateClientStateDict(selectedKey, state); + } + if (shouldUpdateClient) { + check_if_review_finished(); + } +} + +export function clearInputFields() { + const v = document.getElementById("valuearea"); + const c = document.getElementById("commentarea"); + if(v) v.value = ""; + if(c) c.value = ""; +} + +// --- UI Toggles --- +export function showReviewerOptions() { + $("#reviewer_remarks").removeClass('d-none'); +} +export function hideReviewerOptions() { + $("#reviewer_remarks").addClass('d-none'); +} +export function showReviewerCommentsOptions() { + $("#reviewer_comments").removeClass('d-none'); +} +export function hideReviewerCommentsOptions() { + $("#reviewer_comments").addClass('d-none'); +} +// --- Core Logic --- +export function peerReview(config, checkState = false) { selectNextField(); renderSummaryPageFields(); updateTabProgressIndicatorClasses(); + updatePercentageDisplay(); if (checkState && typeof window.state_dict !== 'undefined') { check_if_review_finished(); @@ -151,209 +200,107 @@ export function savePeerReview() { sendJson("POST", config.url_peer_review, json).then(function() { window.location = config.url_table; }).catch(function(err) { - // TODO evaluate error, show user message $('#peer_review-save').addClass('d-none'); alert(getErrorMsg(err)); }); } -/** - * Submits peer review to backend - */ export function submitPeerReview() { $('#peer_review-submitting').removeClass('d-none'); let json = JSON.stringify({reviewType: 'submit', reviewData: current_review}); sendJson("POST", config.url_peer_review, json).then(function() { window.location = config.url_table; }).catch(function(err) { - // TODO evaluate error, show user message $('#peer_review-submitting').addClass('d-none'); alert(getErrorMsg(err)); }); } -/** - * Cancels peer review and forwards to cancel url - */ export function cancelPeerReview() { window.location = config.url_table; } +export function checkReviewComplete() { + const fields = getAllFieldsAndValues(); + let allComplete = true; + + for (let field of fields) { + const fieldState = getFieldState(field.fieldName); + const isEmpty = isEffectivelyEmpty(field.fieldName, field.fieldValue); + + if (!isEmpty && fieldState !== 'ok' && fieldState !== 'rejected' && fieldState !== 'suggestion') { + allComplete = false; + break; + } + } + + const submitButton = $('#submit_summary'); + + if (allComplete) { + submitButton.removeClass('disabled'); + if (!window.clientSideReviewFinished) { + showToast("Success", "You have reviewed all fields and can submit the review to get feedback!", 'success'); + } + } else { + submitButton.addClass('disabled'); + } +} + export function updateFieldDescription(cleanedFieldKey, fieldValue) { const fieldDescriptionsElement = document.getElementById("field-descriptions"); const selectedName = document.querySelector("#review-field-name"); - selectedName.textContent = cleanedFieldKey + " " + fieldValue; + const rawKey = window.selectedField || cleanedFieldKey; + const normalizedKey = normalizeFieldKey(rawKey); - if (fieldDescriptionsData[cleanedFieldKey]) { - const fieldInfo = fieldDescriptionsData[cleanedFieldKey]; - let fieldInfoText = '
'; + const fieldInfo = (fieldDescriptionsData && (fieldDescriptionsData[rawKey] || fieldDescriptionsData[cleanedFieldKey] || fieldDescriptionsData[normalizedKey])) || null; - if (fieldInfo.title) { - fieldInfoText += `

${fieldInfo.title}

`; - } - if (fieldInfo.description) { - fieldInfoText += `
Description:
${fieldInfo.description}
`; - } - if (fieldInfo.example) { - fieldInfoText += `
Example:
${fieldInfo.example}
`; - } - if (fieldInfo.badge) { - fieldInfoText += `
Badge:
${fieldInfo.badge}
`; - } + const titleText = fieldInfo?.title || getFallbackTitle(rawKey) || cleanedFieldKey; + selectedName.textContent = `${titleText}${fieldValue ? ' — ' + fieldValue : ''}`; - fieldInfoText += `
Does it comply with the required ${fieldInfo.title} description convention?
`; - fieldDescriptionsElement.innerHTML = fieldInfoText; - } else { - fieldDescriptionsElement.textContent = "No description found"; + let html = '
'; + html += `

${escapeHtml(fieldInfo?.title || titleText)}

`; + + const desc = fieldInfo?.description || getFallbackDescription(rawKey) || 'No description available.'; + html += `
Description:
${escapeHtml(desc)}
`; + + if (fieldInfo?.example) { + html += `
Example:
${fieldInfo.example}
`; } + html += '
'; + + fieldDescriptionsElement.innerHTML = html; } export function highlightSelectedField(fieldKey, highlightColor = '#F6F9FB') { + if (!fieldKey) return; const reviewItem = document.querySelectorAll('.review__item'); - const selectedDivId = 'field_' + fieldKey; - const selectedDiv = document.getElementById(selectedDivId); - - reviewItem.forEach(div => { - div.style.backgroundColor = ''; - }); + const selectedDiv = document.getElementById('field_' + fieldKey); + reviewItem.forEach(div => div.style.backgroundColor = ''); if (selectedDiv && !selectedDiv.classList.contains('field-ok')) { selectedDiv.style.backgroundColor = highlightColor; } } -export function clearInputFields() { - document.getElementById("valuearea").value = ""; - document.getElementById("commentarea").value = ""; -} - -/** - * Switch to the category tab if needed - */ - -/** - * Function to provide the mapping of category to the correct tab ID - */ export function getCategoryToTabIdMapping() { - // Define the mapping of category to tab ID - const mapping = { + return { 'general': 'general-tab', 'spatial': 'spatiotemporal-tab', 'temporal': 'spatiotemporal-tab', 'source': 'source-tab', 'license': 'license-tab', }; - return mapping; -} - -/** - * Creates List of all fields from html elements - */ -export function makeFieldList() { - var fieldElements = []; - $(".field").each(function() { - fieldElements.push(this.id); - }); - // alert(fieldElements[14]); - return fieldElements; -} - -/** - * Selects the HTML field element after the current one and clicks it - */ - -/** - * Clicks a Field after checking it exists - */ -export function selectField(fieldList, field) { - if (field >= 0 && field < fieldList.length) { - var element = fieldList[field]; - document.getElementById(element).click(); - } -} - -export function selectState(state, shouldUpdateClient = false) { - selectedState = state; - updateClientStateDict(selectedField, state); - - if (shouldUpdateClient) { - check_if_review_finished(); - } } - -/** - * Creates an HTML list of fields with their categories - * @param {Array} fields Array of field objects - * @returns {string} HTML list of fields - */ -export function createFieldList(fields) { - return ` -
    - ${fields.map((field) => `
  • ${field.fieldCategory}: ${field.fieldValue}
  • `).join('')} -
- `; -} export function showToast(title, message, type) { var toast = document.getElementById('liveToast'); var toastTitle = document.getElementById('toastTitle'); var toastBody = document.getElementById('toastBody'); - // Update the toast's header and body based on the type - if (type === 'error') { - toast.classList.remove('bg-success'); - toast.classList.add('bg-danger'); - } else if (type === 'success') { - toast.classList.remove('bg-danger'); - toast.classList.add('bg-success'); - } - - // Set the title and body text + toast.className = `toast hide ${type === 'error' ? 'bg-danger' : 'bg-success'}`; toastTitle.textContent = title; toastBody.textContent = message; - var bsToast = new bootstrap.Toast(toast); - bsToast.show(); -} -/** - * Checks if all fields are reviewed and activates submit button if ready - */ -export function checkReviewComplete() { - const fields = getAllFieldsAndValues(); - - for (let field of fields) { - const fieldState = getFieldState(field.fieldName); - - const reviewed = current_review["reviews"].find((review) => review.key === field.fieldName); - - if (!reviewed && fieldState !== 'ok' && fieldState !== 'rejected' && !isEmptyValue(field.fieldValue)) { - $('#submit_summary').addClass('disabled'); - return; - } - } - $('#submit_summary').removeClass('disabled'); - if (!clientSideReviewFinished) { - showToast("Success", "You have reviewed all fields and can submit the review to get feedback!", 'success'); - } -} - -/** - * Shows reviewer Comment and Suggestion Input options - */ -export function showReviewerOptions() { - $("#reviewer_remarks").removeClass('d-none'); -} -/** - * Hides reviewer Comment and Suggestion Input options - */ -export function hideReviewerOptions() { - $("#reviewer_remarks").addClass('d-none'); -} - -export function showReviewerCommentsOptions() { - $("#reviewer_comments").removeClass('d-none'); -} - - - + new bootstrap.Toast(toast).show(); +} \ No newline at end of file diff --git a/dataedit/static/peer_review/summary.js b/dataedit/static/peer_review/summary.js index c52bc3360..315c8349a 100644 --- a/dataedit/static/peer_review/summary.js +++ b/dataedit/static/peer_review/summary.js @@ -2,8 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {current_review, selectedState} from "./peer_review.js"; import {getFieldState} from "./state_current_review.js"; -import {isEmptyValue} from "./utilities.js"; - +import {isEmptyValue, isEffectivelyEmpty, sendJson} from "./utilities.js"; +import { updatePercentageDisplay } from "./navigation.js"; export function renderSummaryPageFields() { const acceptedFields = []; const suggestingFields = []; @@ -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, ' '); @@ -31,22 +31,25 @@ export function renderSummaryPageFields() { const uniqueFieldIdentifier = `${fieldName}-${fieldCategory}`; - if (isEmptyValue(fieldValue)) { - emptyFields.push({ fieldName, fieldValue, fieldCategory: "emptyFields", fieldSuggestion }); + if (isEffectivelyEmpty(field_id, fieldValue)) { + 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") { @@ -59,14 +62,14 @@ export function renderSummaryPageFields() { continue; } - if (isEmptyValue(fieldValue)) { - emptyFields.push({ fieldName, fieldValue, fieldCategory: "emptyFields" }); + if (isEffectivelyEmpty(field_id, fieldValue)) { + 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' && + !isEffectivelyEmpty(field_id, 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() { @@ -196,17 +384,18 @@ export function updateSubmitButtonColor() { export function updateTabProgressIndicatorClasses() { const tabNames = ['general', 'spatiotemporal', 'source', 'license']; - tabNames.forEach(tabName => { const tab = document.getElementById(`${tabName}-tab`); if (!tab) return; const fieldsInTab = Array.from(document.querySelectorAll(`#${tabName} .field`)); - const allReviewed = fieldsInTab.every(field => { + const allReviewed = fieldsInTab.length === 0 || fieldsInTab.every(field => { + const fieldKey = field.id.replace('field_', ''); const fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); - const fieldState = getFieldState(field.id.replace('field_', '')); - return isEmptyValue(fieldValue) || ['ok', 'suggestion', 'rejected'].includes(fieldState); + const fieldState = getFieldState(fieldKey); + const effectivelyEmpty = isEffectivelyEmpty(fieldKey, fieldValue); + return effectivelyEmpty || ['ok', 'suggestion', 'rejected'].includes(fieldState); }); tab.classList.toggle('status--done', allReviewed); @@ -223,10 +412,11 @@ export function updateTabClasses() { let fields = Array.from(document.querySelectorAll('#' + tabName + ' .field')); let allReviewed = fields.every(field => { - let fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); - let fieldState = getFieldState(field.id.replace('field_', '')); - return isEmptyValue(fieldValue) || ['ok', 'suggest', 'rejected'].includes(fieldState); - }); + let fieldValue = $(field).find('.value').text().replace(/\s+/g, ' ').trim(); + let fieldId = field.id.replace('field_', ''); + let fieldState = getFieldState(fieldId); + return isEffectivelyEmpty(fieldId, fieldValue) || ['ok', 'suggestion', 'rejected'].includes(fieldState); +}); if (allReviewed) { tab.classList.add('status'); diff --git a/dataedit/static/peer_review/utilities.js b/dataedit/static/peer_review/utilities.js index ef2d32d9a..7f619cb39 100644 --- a/dataedit/static/peer_review/utilities.js +++ b/dataedit/static/peer_review/utilities.js @@ -1,16 +1,5 @@ // SPDX-FileCopyrightText: 2025 Reiner Lemoine Institut // SPDX-License-Identifier: AGPL-3.0-or-later -import {getCsrfToken, selectedState} from "./peer_review.js"; - -export function updateFieldColor() { - // Color ok/suggestion/rejected - let field_id = `field_${selectedField}`; - let safe_selector = `#${CSS.escape(field_id)}`; - $(safe_selector).removeClass('field-ok'); - $(safe_selector).removeClass('field-suggestion'); - $(safe_selector).removeClass('field-rejected'); - $(safe_selector).addClass(`field-${selectedState}`); -} export function getCookie(name) { var cookieValue = null; @@ -27,23 +16,63 @@ export function getCookie(name) { return cookieValue; } +export function getCsrfToken() { + return getCookie("csrftoken"); +} export function sendJson(method, url, data, success, error) { var token = getCsrfToken(); return $.ajax({ url: url, + type: method, headers: {"X-CSRFToken": token}, - data_type: "json", + dataType: "json", cache: false, contentType: "application/json; charset=utf-8", processData: false, data: data, - type: method, success: success, - error: error, + error: error }); } export function isEmptyValue(value) { - return value === "" || value === "None" || value === "[]"; + if (value === null || value === undefined) return true; + + // Convert to string and trim whitespace + const s = String(value).trim(); + + return ( + s === '' || + s === 'None' || + s === 'null' || + s === '[]' || + s === '{}' + ); +} + +const BOUNDING_BOX_FIELDS = [ + 'extent.boundingBox.0', + 'extent.boundingBox.1', + 'extent.boundingBox.2', + 'extent.boundingBox.3', +]; + +export function isEffectivelyEmpty(fieldKey, fieldValue) { + if (isEmptyValue(fieldValue)) return true; + if (BOUNDING_BOX_FIELDS.includes(fieldKey) && String(fieldValue).trim() === '0') return true; + return false; +} + +export function getErrorMsg(response) { + try { + if (response.responseJSON && response.responseJSON.error) { + return 'Upload failed: ' + response.responseJSON.error; + } + var response_msg = 'Upload failed: ' + JSON.parse(response.responseText).error; + } catch (e) { + console.log(response); + var response_msg = response.responseText || "Unknown error"; + } + return response_msg; } \ No newline at end of file diff --git a/dataedit/templates/dataedit/opr_contributor.html b/dataedit/templates/dataedit/opr_contributor.html index 29f4ad92e..44f1ebb20 100644 --- a/dataedit/templates/dataedit/opr_contributor.html +++ b/dataedit/templates/dataedit/opr_contributor.html @@ -1,13 +1,18 @@ {% extends "dataedit/filter.html" %} +{% load django_vite %} {% load static %} {% load django_bootstrap5 %} {% load compress %} {% block after-head %} + {% vite_asset "dataedit/static/peer_review/main.js" %} {% endblock after-head %} {% block site-header %} {% endblock site-header %} {% block main %}
+

- + +
+
+ + + +
0%
+
+
- -
-
- -
-
- - -
-
- -
-
- +
+
+ +
+
+ + +
+
+ +
+
+ +
@@ -176,143 +190,91 @@
-
- {% 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 %} -

-
- {% endfor %} -
-
+ aria-labelledby="source-tab">
-
- {% 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 %} -
-
+ aria-labelledby="license-tab">
{{ field_descriptions_json }}
-
-
+
+
+
+
+
-
- - +
+
-
- - +
+
- +
-
- - +
+
- +
- Peer reviewed by jh-RLI on - 2022-09-19 + Dataset uploaded by bmlancien on 2022-09-19
- 0 sucessful - reviews + 0 review
@@ -445,45 +364,34 @@
{% endblock main %} {% block after-body-bottom-js %} - - - - {% compress js %} - - - - {# #} - {% endcompress %} -{% endblock after-body-bottom-js %} +{% endblock %} diff --git a/dataedit/templates/dataedit/opr_review.html b/dataedit/templates/dataedit/opr_review.html index b6b54377b..dd37c4ddf 100644 --- a/dataedit/templates/dataedit/opr_review.html +++ b/dataedit/templates/dataedit/opr_review.html @@ -4,15 +4,14 @@ {% load django_bootstrap5 %} {% load compress %} {% block after-head %} - {#{% vite_asset "dataedit/static/peer_review/peer_review.js" %}#} - {# {% vite_asset "dataedit/static/peer_review/peer_review.js" %}#} - {#{% vite_asset "dataedit/static/peer_review/opr_reviewer.js" %}#} {% vite_asset "dataedit/static/peer_review/main.js" %} {% endblock after-head %} {% block site-header %} {% endblock site-header %} {% block main %}
+

- + +
+
+ + + + +
0%
+
+
- -
-
- -
-
- - -
-
- -
-
- -
- {% if review_id is not None and not review_finished %} -
- +
+
+ +
+
+ + +
+
+
- {% endif %} +
+ +
+ {% if review_id is not None and not review_finished %} +
+ +
+ {% endif %} +
@@ -186,29 +200,136 @@
-
-
- -
- - -
-
- - -
-
-
- -
-
- -
-
- -
-
- -
+
+
+
+ +
+ +
+
+ + +
+
+
+ +
+
+
-
-
-
- - -
-
- - -
+
+ id="suggestion-button" + value="suggestion" + onclick="selectState('suggestion');"> + + + + + + Suggest +
-
-
- - -
+
+ id="rejected-button" + value="rejected" + onclick="selectState('rejected');"> + + + + + + Deny +
-
-
-
- Dataset uploaded by bmlancien on - 2022-09-19 +
+
+
+ + +
+
+ + +
+
-
- 0 review +
+
+ + +
+
+
+
+ Dataset uploaded by bmlancien on 2022-09-19 +
+
+ 0 review +
+
-
+ + {% endblock main %} {% block after-body-bottom-js %} - {#{% compress js %}#} - {# #} - {# #} - {# #} - {#{% endcompress %}#} {% endblock after-body-bottom-js %} diff --git a/dataedit/views.py b/dataedit/views.py index a8e190cd7..d93b903d1 100644 --- a/dataedit/views.py +++ b/dataedit/views.py @@ -1127,77 +1127,190 @@ def parse_keys(self, val, old=""): def sort_in_category(self, table: str, 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":{ "":[...] + } }, ... } + } """ + 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=""): """