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:
@ -137,11 +137,12 @@ async def validate_cell(data: dict):
|
|||||||
|
|
||||||
logger.info(f"Validating cell row {row_num}, column {col_name}, value {value}")
|
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)
|
current_row_data = row_storage.get_row(row_num)
|
||||||
|
|
||||||
if not current_row_data:
|
if not current_row_data:
|
||||||
logger.error(f"No data found for row {row_num}")
|
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}")
|
raise HTTPException(status_code=404, detail=f"No data found for row {row_num}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -152,33 +153,30 @@ async def validate_cell(data: dict):
|
|||||||
cleaned_value = importer._clean_value(value, expected_type)
|
cleaned_value = importer._clean_value(value, expected_type)
|
||||||
current_row_data[col_name] = cleaned_value # Update raw data
|
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:
|
if col_name in DataCollectionParameters.model_fields:
|
||||||
# Ensure current_nested is a Pydantic model
|
|
||||||
nested_data = current_row_data.get("data_collection_parameters")
|
nested_data = current_row_data.get("data_collection_parameters")
|
||||||
|
|
||||||
if isinstance(
|
if isinstance(nested_data, dict):
|
||||||
nested_data, dict
|
# Convert dict to Pydantic model
|
||||||
): # If it's a dict, convert it to a Pydantic model
|
|
||||||
current_nested = DataCollectionParameters(**nested_data)
|
current_nested = DataCollectionParameters(**nested_data)
|
||||||
elif isinstance(
|
elif isinstance(nested_data, DataCollectionParameters):
|
||||||
nested_data, DataCollectionParameters
|
# Already a valid model
|
||||||
): # Already a valid model
|
|
||||||
current_nested = nested_data
|
current_nested = nested_data
|
||||||
else: # If it's None or anything else, create a new instance
|
else:
|
||||||
current_nested = DataCollectionParameters()
|
current_nested = DataCollectionParameters()
|
||||||
|
|
||||||
# Convert the model to a dictionary, update the specific field, and
|
# Update the nested model's field and reapply validation
|
||||||
# re-create the Pydantic model
|
|
||||||
nested_params = current_nested.model_dump()
|
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(
|
current_row_data["data_collection_parameters"] = DataCollectionParameters(
|
||||||
**nested_params
|
**nested_params
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"is_valid": True, "message": "", "corrected_value": cleaned_value}
|
return {"is_valid": True, "message": "", "corrected_value": cleaned_value}
|
||||||
|
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
# Handle and log errors
|
# Handle validation errors
|
||||||
logger.error(f"Validation error details: {e.errors()}")
|
logger.error(f"Validation error details: {e.errors()}")
|
||||||
column_error = next(
|
column_error = next(
|
||||||
(err for err in e.errors() if err.get("loc")[0] == col_name), None
|
(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}"
|
f"Validation failed for row {row_num}, column {col_name}. Error: {message}"
|
||||||
)
|
)
|
||||||
return {"is_valid": False, "message": 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:
|
except Exception as e:
|
||||||
# Log unexpected issues
|
# Log unexpected issues and re-raise HTTP 500
|
||||||
logger.error(f"Unexpected error during validation: {str(e)}")
|
logger.error(f"Unexpected error during validation: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail=f"Error validating cell: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error validating cell: {str(e)}")
|
||||||
|
@ -163,24 +163,19 @@ class DataCollectionParameters(BaseModel):
|
|||||||
) from e
|
) from e
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator("oscillation", "targetresolution", mode="before")
|
@field_validator("oscillation", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def positive_float_validator(cls, v):
|
def positive_float_validator(cls, v):
|
||||||
logger.debug(f"Running positive_float_validator for value: {v}")
|
if v is None:
|
||||||
if v is not None:
|
return None
|
||||||
try:
|
try:
|
||||||
v = float(v)
|
v = float(v)
|
||||||
if v <= 0:
|
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.")
|
||||||
raise ValueError(
|
|
||||||
f"'{v}' is not valid. Value must be a positive float."
|
|
||||||
)
|
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
logger.error(f"Validation failed: '{v}' caused error {str(e)}")
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"'{v}' is not valid. Value must be a positive float."
|
f"'{v}' is not valid. Value must be a positive float."
|
||||||
) from e
|
) from e
|
||||||
logger.debug(f"Validation succeeded for value: {v}")
|
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator("exposure", mode="before")
|
@field_validator("exposure", mode="before")
|
||||||
|
@ -62,30 +62,22 @@ class SampleSpreadsheetImporter:
|
|||||||
return column_type_mapping.get(column_name, str)
|
return column_type_mapping.get(column_name, str)
|
||||||
|
|
||||||
def _clean_value(self, value, expected_type=None):
|
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:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
if expected_type == str:
|
if expected_type == str:
|
||||||
# Ensure value is converted to string and stripped of whitespace
|
|
||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
if expected_type in [float, int]:
|
if expected_type in [float, int]:
|
||||||
try:
|
try:
|
||||||
return expected_type(value)
|
return expected_type(value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError) as e:
|
||||||
# If conversion fails, return None
|
logger.error(
|
||||||
return None
|
f"Failed to cast value '{value}' to {expected_type}. Error: {e}"
|
||||||
if isinstance(value, str):
|
)
|
||||||
try:
|
raise ValueError(
|
||||||
# Handle numeric strings
|
f"Invalid value: '{value}'. Expected type: {expected_type}."
|
||||||
if "." in value:
|
)
|
||||||
return float(value)
|
# Fallback for unhandled types
|
||||||
else:
|
logger.warning(f"Unhandled type for value: '{value}'. Returning as-is.")
|
||||||
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
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def import_spreadsheet(self, file):
|
def import_spreadsheet(self, file):
|
||||||
|
@ -122,23 +122,22 @@ const SpreadsheetTable = ({
|
|||||||
|
|
||||||
if (response && response.is_valid !== undefined) {
|
if (response && response.is_valid !== undefined) {
|
||||||
if (response.is_valid) {
|
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;
|
const correctedValue = response.corrected_value ?? newValue;
|
||||||
currentRow.data[colIndex] = correctedValue;
|
currentRow.data[colIndex] = correctedValue;
|
||||||
updatedRawData[rowIndex] = currentRow;
|
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(
|
const updatedErrors = localErrors.filter(
|
||||||
(error) => !(error.row === currentRow.row_num && error.cell === colIndex)
|
(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}`]));
|
setNonEditableCells((prev) => new Set([...prev, `${rowIndex}-${colIndex}`]));
|
||||||
} else {
|
} 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 errorMessage = response.message || "Invalid value.";
|
||||||
const newError = {
|
const newError = {
|
||||||
row: currentRow.row_num,
|
row: currentRow.row_num,
|
||||||
@ -147,10 +146,18 @@ const SpreadsheetTable = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updatedErrors = [
|
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,
|
newError,
|
||||||
];
|
];
|
||||||
setLocalErrors(updatedErrors);
|
setLocalErrors(updatedErrors);
|
||||||
|
|
||||||
|
setNonEditableCells((prev) => {
|
||||||
|
const updatedSet = new Set(prev);
|
||||||
|
updatedSet.delete(`${rowIndex}-${colIndex}`); // Ensure it stays editable
|
||||||
|
return updatedSet;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Unexpected response from backend:", response);
|
console.error("Unexpected response from backend:", response);
|
||||||
|
Reference in New Issue
Block a user