Add spreadsheet enhancements and default handling
Implemented a toggleable spreadsheet UI component for sample data, added fields such as priority and comments, and improved backend validation. Default values for "directory" are now assigned when missing, with feedback highlighted in green on the front end.
This commit is contained in:
@ -3,6 +3,10 @@ from typing import Any, Optional, List, Dict
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from typing_extensions import Annotated
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpreadsheetModel(BaseModel):
|
||||
@ -98,34 +102,45 @@ class SpreadsheetModel(BaseModel):
|
||||
@field_validator("directory", mode="before")
|
||||
@classmethod
|
||||
def directory_characters(cls, v):
|
||||
if v:
|
||||
v = str(v).strip("/").replace(" ", "_")
|
||||
if re.search("\n", v):
|
||||
raise ValueError(f" '{v}' is not valid. newline character detected.")
|
||||
logger.debug(f"Validating 'directory' field with value: {repr(v)}")
|
||||
|
||||
valid_macros = [
|
||||
"{date}",
|
||||
"{prefix}",
|
||||
"{sgpuck}",
|
||||
"{puck}",
|
||||
"{beamline}",
|
||||
"{sgprefix}",
|
||||
"{sgpriority}",
|
||||
"{sgposition}",
|
||||
"{protein}",
|
||||
"{method}",
|
||||
]
|
||||
pattern = re.compile("|".join(re.escape(macro) for macro in valid_macros))
|
||||
v = pattern.sub("macro", v)
|
||||
|
||||
allowed_chars = "[a-z0-9_.+-]"
|
||||
directory_re = re.compile(
|
||||
f"^(({allowed_chars}*|{allowed_chars}+)*/*)*$", re.IGNORECASE
|
||||
# Assign default value if v is None or empty
|
||||
if not v:
|
||||
default_value = "{sgPuck}/{sgPosition}"
|
||||
logger.warning(
|
||||
f"'directory' field is empty or None. Assigning default value: "
|
||||
f"{default_value}"
|
||||
)
|
||||
return default_value
|
||||
|
||||
v = str(v).strip("/").replace(" ", "_")
|
||||
if "\n" in v:
|
||||
raise ValueError(f"'{v}' is not valid. Newline character detected.")
|
||||
|
||||
# Replace valid macros for consistency
|
||||
valid_macros = [
|
||||
"{date}",
|
||||
"{prefix}",
|
||||
"{sgPuck}",
|
||||
"{sgPosition}",
|
||||
"{beamline}",
|
||||
"{sgPrefix}",
|
||||
"{sgPriority}",
|
||||
"{protein}",
|
||||
"{method}",
|
||||
]
|
||||
pattern = re.compile("|".join(re.escape(macro) for macro in valid_macros))
|
||||
v = pattern.sub("macro", v)
|
||||
|
||||
# Ensure only allowed characters are in the directory value
|
||||
allowed_chars = "[a-z0-9_.+-]"
|
||||
directory_re = re.compile(
|
||||
f"^(({allowed_chars}*|{allowed_chars}+)*/*)*$", re.IGNORECASE
|
||||
)
|
||||
if not directory_re.match(v):
|
||||
raise ValueError(
|
||||
f"'{v}' is not valid. Value must be a valid path or macro."
|
||||
)
|
||||
if not directory_re.match(v):
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid. Value must be a valid path or macro."
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("positioninpuck", mode="before")
|
||||
@ -251,110 +266,109 @@ class SpreadsheetModel(BaseModel):
|
||||
raise ValueError(f" '{v}' is not valid." f"Value must be one of {allowed}.")
|
||||
return v
|
||||
|
||||
@field_validator("spacegroupnumber", mode="before")
|
||||
@classmethod
|
||||
def spacegroupnumber_allowed(cls, v):
|
||||
if v is not None:
|
||||
try:
|
||||
v = int(v)
|
||||
if not (1 <= v <= 230):
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid."
|
||||
f"Value must be an integer between 1 and 230."
|
||||
)
|
||||
except (ValueError, TypeError) as e:
|
||||
@field_validator("spacegroupnumber", mode="before")
|
||||
@classmethod
|
||||
def spacegroupnumber_allowed(cls, v):
|
||||
if v is not None:
|
||||
try:
|
||||
v = int(v)
|
||||
if not (1 <= v <= 230):
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid."
|
||||
f"Value must be an integer between 1 and 230."
|
||||
) from e
|
||||
return v
|
||||
|
||||
@field_validator("cellparameters", mode="before")
|
||||
@classmethod
|
||||
def cellparameters_format(cls, v):
|
||||
if v:
|
||||
values = [float(i) for i in v.split(",")]
|
||||
if len(values) != 6 or any(val <= 0 for val in values):
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid."
|
||||
f"Value must be a set of six positive floats or integers."
|
||||
)
|
||||
return v
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid."
|
||||
f"Value must be an integer between 1 and 230."
|
||||
) from e
|
||||
return v
|
||||
|
||||
@field_validator("rescutkey", "rescutvalue", mode="before")
|
||||
@classmethod
|
||||
def rescutkey_value_pair(cls, values):
|
||||
rescutkey = values.get("rescutkey")
|
||||
rescutvalue = values.get("rescutvalue")
|
||||
if rescutkey and rescutvalue:
|
||||
if rescutkey not in {"is", "cchalf"}:
|
||||
raise ValueError("Rescutkey must be either 'is' or 'cchalf'")
|
||||
if not isinstance(rescutvalue, float) or rescutvalue <= 0:
|
||||
raise ValueError(
|
||||
"Rescutvalue must be a positive float if rescutkey is provided"
|
||||
)
|
||||
return values
|
||||
@field_validator("cellparameters", mode="before")
|
||||
@classmethod
|
||||
def cellparameters_format(cls, v):
|
||||
if v:
|
||||
values = [float(i) for i in v.split(",")]
|
||||
if len(values) != 6 or any(val <= 0 for val in values):
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid."
|
||||
f"Value must be a set of six positive floats or integers."
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("trustedhigh", mode="before")
|
||||
@classmethod
|
||||
def trustedhigh_allowed(cls, v):
|
||||
if v is not None:
|
||||
try:
|
||||
v = float(v)
|
||||
if not (0 <= v <= 2.0):
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid."
|
||||
f"Value must be a float between 0 and 2.0."
|
||||
)
|
||||
except (ValueError, TypeError) as e:
|
||||
# @field_validator("rescutkey", "rescutvalue", mode="before")
|
||||
# @classmethod
|
||||
# def rescutkey_value_pair(cls, values):
|
||||
# rescutkey = values.get("rescutkey")
|
||||
# rescutvalue = values.get("rescutvalue")
|
||||
# if rescutkey and rescutvalue:
|
||||
# if rescutkey not in {"is", "cchalf"}:
|
||||
# raise ValueError("Rescutkey must be either 'is' or 'cchalf'")
|
||||
# if not isinstance(rescutvalue, float) or rescutvalue <= 0:
|
||||
# raise ValueError(
|
||||
# "Rescutvalue must be a positive float if rescutkey is provided"
|
||||
# )
|
||||
# return values
|
||||
|
||||
@field_validator("trustedhigh", mode="before")
|
||||
@classmethod
|
||||
def trustedhigh_allowed(cls, v):
|
||||
if v is not None:
|
||||
try:
|
||||
v = float(v)
|
||||
if not (0 <= v <= 2.0):
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid."
|
||||
f"Value must be a float between 0 and 2.0."
|
||||
) from e
|
||||
return v
|
||||
)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid." f"Value must be a float between 0 and 2.0."
|
||||
) from e
|
||||
return v
|
||||
|
||||
@field_validator("chiphiangles", mode="before")
|
||||
@classmethod
|
||||
def chiphiangles_allowed(cls, v):
|
||||
if v is not None:
|
||||
try:
|
||||
v = float(v)
|
||||
if not (0 <= v <= 30):
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid."
|
||||
f"Value must be a float between 0 and 30."
|
||||
)
|
||||
except (ValueError, TypeError) as e:
|
||||
@field_validator("chiphiangles", mode="before")
|
||||
@classmethod
|
||||
def chiphiangles_allowed(cls, v):
|
||||
if v is not None:
|
||||
try:
|
||||
v = float(v)
|
||||
if not (0 <= v <= 30):
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid. Value must be a float between 0 and 30."
|
||||
) from e
|
||||
return v
|
||||
f" '{v}' is not valid."
|
||||
f"Value must be a float between 0 and 30."
|
||||
)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid. Value must be a float between 0 and 30."
|
||||
) from e
|
||||
return v
|
||||
|
||||
@field_validator("dose", mode="before")
|
||||
@classmethod
|
||||
def dose_positive(cls, v):
|
||||
if v is not 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:
|
||||
@field_validator("dose", mode="before")
|
||||
@classmethod
|
||||
def dose_positive(cls, v):
|
||||
if v is not None:
|
||||
try:
|
||||
v = float(v)
|
||||
if v <= 0:
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid. Value must be a positive float."
|
||||
) from e
|
||||
return v
|
||||
)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ValueError(
|
||||
f" '{v}' is not valid. Value must be a positive float."
|
||||
) from e
|
||||
return v
|
||||
|
||||
class TELLModel(SpreadsheetModel):
|
||||
input_order: int
|
||||
samplemountcount: int = 0
|
||||
samplestatus: str = "not present"
|
||||
puckaddress: str = "---"
|
||||
username: str
|
||||
puck_number: int
|
||||
prefix: Optional[str]
|
||||
folder: Optional[str]
|
||||
# class TELLModel(SpreadsheetModel):
|
||||
# input_order: int
|
||||
# samplemountcount: int = 0
|
||||
# samplestatus: str = "not present"
|
||||
# puckaddress: str = "---"
|
||||
# username: str
|
||||
# puck_number: int
|
||||
# prefix: Optional[str]
|
||||
# folder: Optional[str]
|
||||
|
||||
|
||||
class SpreadsheetResponse(BaseModel):
|
||||
|
Reference in New Issue
Block a user