Skip to content
1 change: 1 addition & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2622,6 +2622,7 @@ SPDX-FileCopyrightText = [
"2024 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut",
"2024 Eike Broda <https://github.com/ebroda>",
"2025 Daryna Barabanova <https://github.com/Darynarli> © Reiner Lemoine Institut",
"2026 Vismaya Jochem <https://github.com/vismayajochem> © Reiner Lemoine Institut",
]

[[annotations]]
Expand Down
79 changes: 75 additions & 4 deletions dataedit/metadata/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
"""
SPDX-FileCopyrightText: 2025 Pierre Francois <https://github.com/Bachibouzouk> © Reiner Lemoine Institut
SPDX-FileCopyrightText: 2025 Pierre Francois <https://github.com/Bachibouzouk> © Reiner Lemoine Institut
SPDX-FileCopyrightText: 2025 Christian Winger <https://github.com/wingechr> © Öko-Institut e.V.
SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
SPDX-FileCopyrightText: 2025 Martin Glauer <https://github.com/MGlauer> © Otto-von-Guericke-Universität Magdeburg
SPDX-FileCopyrightText: 2025 Martin Glauer <https://github.com/MGlauer> © Otto-von-Guericke-Universität Magdeburg
SPDX-FileCopyrightText: 2025 Christian Winger <https://github.com/wingechr> © Öko-Institut e.V.
SPDX-FileCopyrightText: 2025 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
SPDX-FileCopyrightText: 2025 shara <https://github.com/SharanyaMohan-30> © Otto-von-Guericke-Universität Magdeburg
SPDX-FileCopyrightText: 2025 user <https://github.com/Darynarli> © Reiner Lemoine Institut
SPDX-FileCopyrightText: 2026 Vismaya Jochem <https://github.com/vismayajochem> © Reiner Lemoine Institut
SPDX-License-Identifier: AGPL-3.0-or-later
""" # noqa: 501

Expand Down Expand Up @@ -95,3 +92,77 @@ def load_metadata_from_db(table: str) -> dict:
validate_metadata(metadata, check_license=False)

return metadata


def has_valid_filled_metadata(metadata: dict) -> bool:
"""
Check if metadata has been meaningfully filled beyond the default fields
that are automatically populated by load_metadata_from_db.

The default fields set by load_metadata_from_db are:
- "name": table name
- "resources": [{"name": table name}]
- "metaMetadata": from OEMETADATA_V20_TEMPLATE

Returns False if metadata is empty or only contains these default fields.
Returns True if at least one additional field has a meaningful value.

Args:
metadata: The OEMetadata dictionary

Returns:
bool: True if metadata has been filled beyond defaults, False otherwise
"""
if not metadata:
return False

# Keys that are considered "default" / auto-filled and should be ignored
DEFAULT_ONLY_KEYS = {"name", "metaMetadata"}

def check_value(val):
"""Check if a value is considered non-empty."""
if val is None:
return False
if isinstance(val, str):
return val.strip() not in ("", "None", "null")
if isinstance(val, (list, dict)):
return len(val) > 0
return True

def is_default_resources(resources):
"""
Check if resources only contains the default minimal entry
set by load_metadata_from_db: [{"name": <table_name>}]
"""
if not isinstance(resources, list):
return False
if len(resources) != 1:
# More than one resource means user has added content
return False

resource = resources[0]
if not isinstance(resource, dict):
return False

# Default resource only has "name" key
return set(resource.keys()) == {"name"}

def traverse(obj):
"""Recursively traverse the metadata structure."""
if isinstance(obj, dict):
for key, value in obj.items():
if key in DEFAULT_ONLY_KEYS:
continue
if key == "resources":
# Only skip if it matches the default minimal resources structure
if is_default_resources(value):
continue
if check_value(value) or traverse(value):
return True
elif isinstance(obj, list):
for item in obj:
if check_value(item) or traverse(item):
return True
return False

return traverse(metadata)
10 changes: 9 additions & 1 deletion dataedit/static/peer_review/main.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// SPDX-FileCopyrightText: 2025 Reiner Lemoine Institut
// SPDX-FileCopyrightText: 2026 Vismaya Jochem <https://github.com/vismayajochem> © Reiner Lemoine Institut
// SPDX-FileCopyrightText: 2025 Daryna Barabanova <https://github.com/Darynarli> © Reiner Lemoine Institut
// SPDX-FileCopyrightText: 2026 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

import * as common from "./peer_review.js";
import { selectState } from './peer_review.js';
import { selectNextField, selectPreviousField } from './navigation.js';
import { selectNextField, selectPreviousField, selectFirstReviewableField } from './navigation.js';
import { setGetFieldState } from './state_current_review.js';

// Static imports avoid the "Failed to fetch" dynamic import errors
Expand Down Expand Up @@ -36,6 +37,13 @@ document.addEventListener('DOMContentLoaded', function () {

if (oprPage === 'reviewer') {
initReviewer();

// Auto-select first reviewable field after all initialization is complete
// Use a longer timeout to ensure all accordions, tabs, and state are ready
setTimeout(() => {
selectFirstReviewableField();
}, 600);

} else if (oprPage === 'contributor') {
initContributor();
} else {
Expand Down
124 changes: 118 additions & 6 deletions dataedit/static/peer_review/navigation.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// SPDX-FileCopyrightText: 2025 Reiner Lemoine Institut
// SPDX-FileCopyrightText: 2026 Vismaya Jochem <https://github.com/vismayajochem> © Reiner Lemoine Institut
// SPDX-FileCopyrightText: 2025 Daryna Barabanova <https://github.com/Darynarli> © Reiner Lemoine Institut
// SPDX-FileCopyrightText: 2026 Jonas Huber <https://github.com/jh-RLI> © Reiner Lemoine Institut
// SPDX-License-Identifier: AGPL-3.0-or-later

import { getCategoryToTabIdMapping, makeFieldList, selectField } from "./peer_review.js";
import { isEmptyValue, isEffectivelyEmpty, sendJson } from "./utilities.js";

export function updateTabProgress() {
const allFields = document.querySelectorAll('.review__item');
Expand Down Expand Up @@ -50,13 +52,123 @@ export function switchCategoryTab(category) {
}

export function selectNextField() {
var fieldList = makeFieldList();
var next = fieldList.indexOf('field_' + window.selectedField) + 1;
selectField(fieldList, next);
const fieldList = makeFieldList();
const currentIndex = fieldList.indexOf('field_' + window.selectedField);

for (let i = currentIndex + 1; i < fieldList.length; i++) {
const fieldElement = document.getElementById(fieldList[i]);
if (!fieldElement) continue;

const fieldKey = fieldElement.dataset.fieldkey;
const fieldValue = fieldElement.dataset.fieldvalue;

if (!isEffectivelyEmpty(fieldKey, fieldValue)) {
selectField(fieldList, i);
return;
}
}
}

export function selectPreviousField() {
var fieldList = makeFieldList();
var prev = fieldList.indexOf('field_' + window.selectedField) - 1;
selectField(fieldList, prev);
const fieldList = makeFieldList();
const currentIndex = fieldList.indexOf('field_' + window.selectedField);

for (let i = currentIndex - 1; i >= 0; i--) {
const fieldElement = document.getElementById(fieldList[i]);
if (!fieldElement) continue;

const fieldKey = fieldElement.dataset.fieldkey;
const fieldValue = fieldElement.dataset.fieldvalue;

if (!isEffectivelyEmpty(fieldKey, fieldValue)) {
selectField(fieldList, i);
return;
}
}
}
/**
* Selects the first field that needs review (not empty and not yet reviewed)
*/
export function selectFirstReviewableField() {
const allFields = document.querySelectorAll('.review__item.field');

for (let field of allFields) {
const fieldKey = field.getAttribute('data-fieldkey');
const fieldValue = field.getAttribute('data-fieldvalue');

// Skip if no fieldKey
if (!fieldKey || fieldKey === '') continue;

// Check if field is effectively empty
if (isEffectivelyEmpty(fieldKey, fieldValue)) continue;

// Check if field is already reviewed
const currentState = window.state_dict?.[fieldKey];
const isReviewed = currentState && ['ok', 'suggestion', 'rejected'].includes(currentState);

// Select if NOT empty AND NOT reviewed
if (!isReviewed) {
// Get the category from the field
const category = field.getAttribute('data-category');

// Check if field is in a collapsed accordion
const accordionCollapse = field.closest('.accordion-collapse');

if (accordionCollapse && !accordionCollapse.classList.contains('show')) {
// Find and click the accordion button to expand it
const accordionButton = document.querySelector(
`[data-bs-target="#${accordionCollapse.id}"]`
);

if (accordionButton) {
// Wait for accordion to expand, then select field
accordionCollapse.addEventListener('shown.bs.collapse', () => {
selectFieldAfterTabSwitch(fieldKey, fieldValue, category);
}, { once: true });

accordionButton.click();
return; // Exit early, will be called after accordion opens
}
}

// Check if we need to switch tabs
const tabPane = field.closest('.tab-pane');
if (tabPane && !tabPane.classList.contains('active')) {
const tabId = tabPane.id;
const tabButton = document.querySelector(`[data-bs-target="#${tabId}"]`);

if (tabButton) {
// Wait for tab transition
setTimeout(() => {
selectFieldAfterTabSwitch(fieldKey, fieldValue, category);
}, 300);

tabButton.click();
return;
}
}

// Field is visible, select it directly
selectFieldAfterTabSwitch(fieldKey, fieldValue, category);
return; // Stop after selecting first field
}
}

console.log('No reviewable fields found (all are either empty or already reviewed)');
}

/**
* Helper function to select field after tab/accordion animation
*/
function selectFieldAfterTabSwitch(fieldKey, fieldValue, category) {
// Trigger the field click programmatically
const fieldElement = document.querySelector(`.field[data-fieldkey="${fieldKey}"]`);

if (fieldElement) {
// Scroll to field
fieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' });

// Trigger click event (this will call your click_field function)
fieldElement.click();
}
}
Loading
Loading