Add column type mapping and enhance validation

Introduced a backend mapping for column expected types, improving validation and error handling. Updated UI to highlight default and corrected values, with additional detailed validation for data collection parameters.
This commit is contained in:
GotthardG 2025-01-07 16:07:13 +01:00
parent 92306fcfa6
commit 35369fd13c
4 changed files with 61 additions and 55 deletions

View File

@ -137,11 +137,12 @@ async def validate_cell(data: dict):
logger.info(f"Validating cell row {row_num}, column {col_name}, value {value}")
# Get the full data for the row
# Retrieve the full data for the row
current_row_data = row_storage.get_row(row_num)
if not current_row_data:
logger.error(f"No data found for row {row_num}")
# Explicitly return a 404 error if the row is missing
raise HTTPException(status_code=404, detail=f"No data found for row {row_num}")
try:
@ -152,33 +153,30 @@ async def validate_cell(data: dict):
cleaned_value = importer._clean_value(value, expected_type)
current_row_data[col_name] = cleaned_value # Update raw data
# If the column belongs to the nested `data_collection_parameters`
# Nested parameter handling for `DataCollectionParameters`
if col_name in DataCollectionParameters.model_fields:
# Ensure current_nested is a Pydantic model
nested_data = current_row_data.get("data_collection_parameters")
if isinstance(
nested_data, dict
): # If it's a dict, convert it to a Pydantic model
if isinstance(nested_data, dict):
# Convert dict to Pydantic model
current_nested = DataCollectionParameters(**nested_data)
elif isinstance(
nested_data, DataCollectionParameters
): # Already a valid model
elif isinstance(nested_data, DataCollectionParameters):
# Already a valid model
current_nested = nested_data
else: # If it's None or anything else, create a new instance
else:
current_nested = DataCollectionParameters()
# Convert the model to a dictionary, update the specific field, and
# re-create the Pydantic model
# Update the nested model's field and reapply validation
nested_params = current_nested.model_dump()
nested_params[col_name] = cleaned_value # Update the nested field
nested_params[col_name] = cleaned_value
current_row_data["data_collection_parameters"] = DataCollectionParameters(
**nested_params
)
return {"is_valid": True, "message": "", "corrected_value": cleaned_value}
except ValidationError as e:
# Handle and log errors
# Handle validation errors
logger.error(f"Validation error details: {e.errors()}")
column_error = next(
(err for err in e.errors() if err.get("loc")[0] == col_name), None
@ -188,7 +186,21 @@ async def validate_cell(data: dict):
f"Validation failed for row {row_num}, column {col_name}. Error: {message}"
)
return {"is_valid": False, "message": message}
except ValueError as e:
# Handle expected typecasting or value errors specifically
error_message = str(e)
logger.warning(
f"Failed to validate value '{value}' for row "
f"{row_num}, column {col_name}: {error_message}"
)
raise HTTPException(
status_code=400,
detail=f"Validation failed for row "
f"{row_num}, column {col_name}: {error_message}",
)
except Exception as e:
# Log unexpected issues
# Log unexpected issues and re-raise HTTP 500
logger.error(f"Unexpected error during validation: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error validating cell: {str(e)}")

View File

@ -163,24 +163,19 @@ class DataCollectionParameters(BaseModel):
) from e
return v
@field_validator("oscillation", "targetresolution", mode="before")
@field_validator("oscillation", mode="before")
@classmethod
def positive_float_validator(cls, v):
logger.debug(f"Running positive_float_validator for value: {v}")
if v is not None:
try:
v = float(v)
if v <= 0:
logger.error(f"Validation failed: '{v}' is not greater than 0.")
raise ValueError(
f"'{v}' is not valid. Value must be a positive float."
)
except (ValueError, TypeError) as e:
logger.error(f"Validation failed: '{v}' caused error {str(e)}")
raise ValueError(
f"'{v}' is not valid. Value must be a positive float."
) from e
logger.debug(f"Validation succeeded for value: {v}")
if v is None:
return None
try:
v = float(v)
if v <= 0:
raise ValueError(f"'{v}' is not valid. Value must be a positive float.")
except (ValueError, TypeError) as e:
raise ValueError(
f"'{v}' is not valid. Value must be a positive float."
) from e
return v
@field_validator("exposure", mode="before")

View File

@ -62,30 +62,22 @@ class SampleSpreadsheetImporter:
return column_type_mapping.get(column_name, str)
def _clean_value(self, value, expected_type=None):
"""Clean value by converting it to the expected type and handle edge cases."""
if value is None:
return None
if expected_type == str:
# Ensure value is converted to string and stripped of whitespace
return str(value).strip()
if expected_type in [float, int]:
try:
return expected_type(value)
except (ValueError, TypeError):
# If conversion fails, return None
return None
if isinstance(value, str):
try:
# Handle numeric strings
if "." in value:
return float(value)
else:
return int(value)
except ValueError:
pass
# In case of failure, return the stripped string
return value.strip()
# If no expected type or value type match, return the original value
except (ValueError, TypeError) as e:
logger.error(
f"Failed to cast value '{value}' to {expected_type}. Error: {e}"
)
raise ValueError(
f"Invalid value: '{value}'. Expected type: {expected_type}."
)
# Fallback for unhandled types
logger.warning(f"Unhandled type for value: '{value}'. Returning as-is.")
return value
def import_spreadsheet(self, file):

View File

@ -122,23 +122,22 @@ const SpreadsheetTable = ({
if (response && response.is_valid !== undefined) {
if (response.is_valid) {
// Handle validation success (remove error)
// If valid, update the value (and use corrected_value if returned)
const correctedValue = response.corrected_value ?? newValue;
currentRow.data[colIndex] = correctedValue;
updatedRawData[rowIndex] = currentRow;
setRawData(updatedRawData); // Update table data
setRawData(updatedRawData);
// Remove error associated with this cell
// Remove the error and mark as non-editable
const updatedErrors = localErrors.filter(
(error) => !(error.row === currentRow.row_num && error.cell === colIndex)
);
setLocalErrors(updatedErrors);
setLocalErrors(updatedErrors); // Update error list
// Update non-editable state
setNonEditableCells((prev) => new Set([...prev, `${rowIndex}-${colIndex}`]));
} else {
// Handle validation failure (add error)
// If not valid, don't add to nonEditableCells and update the error list
const errorMessage = response.message || "Invalid value.";
const newError = {
row: currentRow.row_num,
@ -147,10 +146,18 @@ const SpreadsheetTable = ({
};
const updatedErrors = [
...localErrors.filter((error) => !(error.row === newError.row && error.cell === newError.cell)), // Avoid duplicates
...localErrors.filter(
(error) => !(error.row === newError.row && error.cell === newError.cell)
),
newError,
];
setLocalErrors(updatedErrors);
setNonEditableCells((prev) => {
const updatedSet = new Set(prev);
updatedSet.delete(`${rowIndex}-${colIndex}`); // Ensure it stays editable
return updatedSet;
});
}
} else {
console.error("Unexpected response from backend:", response);