Compare commits
12 Commits
recover-co
...
main
Author | SHA1 | Date | |
---|---|---|---|
d4e7c0833b | |||
4a9ac53dd8 | |||
719dc3ce2e | |||
3f4c27ee5b | |||
fb6745fd31 | |||
8c4ae8dd88 | |||
0b58ac5d27 | |||
6055bcad17 | |||
c56b07718e | |||
487e0fc720 | |||
871b455d5e | |||
b15cfdfa99 |
4
app.py
4
app.py
@ -1,6 +1,6 @@
|
|||||||
# Validate configuration before starting the app
|
# Validate configuration before starting the app
|
||||||
from functions.validate_config import validate_config
|
from functions.validate_config import validate_config
|
||||||
from config import item_attributes
|
from config import item_attributes, sql_conf
|
||||||
|
|
||||||
config_status = validate_config(item_attributes)
|
config_status = validate_config(item_attributes)
|
||||||
if config_status != "Ok":
|
if config_status != "Ok":
|
||||||
@ -23,7 +23,7 @@ from routes.confirm_save import confirm_save_bp
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = 'your_secret_key' # Required for flashing messages and session
|
app.secret_key = 'your_secret_key' # Required for flashing messages and session
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://assetadmin:1234@localhost/asset_test_db'
|
app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql://{sql_conf.SQL_USER}:{sql_conf.SQL_PASSWORD}@{sql_conf.SQL_HOST}/{sql_conf.SQL_DB}'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
|
40
config.py
40
config.py
@ -1,40 +1,48 @@
|
|||||||
from definitions.attribute import Attribute
|
from definitions.attribute import textAttribute, intAttribute, dateAttribute, selectAttribute
|
||||||
|
|
||||||
item_attributes = {
|
# MySQL information
|
||||||
"assettag": Attribute(
|
class sql_conf:
|
||||||
|
SQL_USER = "assetadmin"
|
||||||
|
SQL_PASSWORD = "1234"
|
||||||
|
SQL_HOST = "localhost"
|
||||||
|
SQL_DB = "asset_test_db"
|
||||||
|
SQL_TABLE = "asset_test"
|
||||||
|
|
||||||
|
item_attributes = [
|
||||||
|
textAttribute(
|
||||||
|
attrib_name="assettag",
|
||||||
display_name="Asset Tag",
|
display_name="Asset Tag",
|
||||||
html_input_type="text",
|
|
||||||
required=True,
|
required=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
primary=True,
|
primary=True,
|
||||||
regex=r"^[A-Z0-9]+$", # Only uppercase letters and numbers
|
regex=r"^[A-Z0-9]+$", # Only uppercase letters and numbers
|
||||||
default_val=1000000
|
default_val="1000000"
|
||||||
),
|
),
|
||||||
"hostname": Attribute(
|
textAttribute(
|
||||||
|
attrib_name="hostname",
|
||||||
display_name="Host Name",
|
display_name="Host Name",
|
||||||
html_input_type="text",
|
|
||||||
required=True,
|
required=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
regex=r"^[a-z0-9._-]+$" # Lowercase letters, numbers, dots, underscores, hyphens
|
regex=r"^[a-z0-9._-]+$" # Lowercase letters, numbers, dots, underscores, hyphens
|
||||||
),
|
),
|
||||||
"warrantyfrom": Attribute(
|
dateAttribute(
|
||||||
|
attrib_name="warrantyfrom",
|
||||||
display_name="Warranty From",
|
display_name="Warranty From",
|
||||||
html_input_type="date",
|
|
||||||
default_val="2020-03-09",
|
default_val="2020-03-09",
|
||||||
required=True
|
required=True
|
||||||
),
|
),
|
||||||
"status": Attribute(
|
selectAttribute(
|
||||||
|
attrib_name="status",
|
||||||
display_name="Status",
|
display_name="Status",
|
||||||
html_input_type="select",
|
|
||||||
required=True,
|
required=True,
|
||||||
options=["Active", "Inactive"], # Allowed values
|
options=["Active", "Inactive"], # Allowed values
|
||||||
default_val="Active"
|
default_val="Active"
|
||||||
),
|
),
|
||||||
"staffnum": Attribute(
|
intAttribute(
|
||||||
|
attrib_name="staffnum",
|
||||||
display_name="Staff No.",
|
display_name="Staff No.",
|
||||||
html_input_type="number",
|
|
||||||
required=True,
|
required=True,
|
||||||
min=100000, # 6 digits
|
min_val=100000, # 6 digits
|
||||||
max=99999999, # 8 digits
|
max_val=99999999, # 8 digits
|
||||||
)
|
)
|
||||||
}
|
]
|
@ -1,16 +1,205 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
class Attribute:
|
class Attribute:
|
||||||
def __init__(self, display_name, html_input_type="text", required=False, unique=False, primary=False, regex=None, min=None, max=None, options=None, default_val="", auto_increment=False, index=False, comment="", compareto=None):
|
"""Base class for all attribute types."""
|
||||||
self.display_name = display_name # Input label or table column header.
|
def __init__(
|
||||||
self.html_input_type = html_input_type # HTML form input type. Determines MySQL data type.
|
self,
|
||||||
self.required = required # HTML form input "required" attribute and MySQL "Not Null" constraint
|
attrib_name: str,
|
||||||
self.unique = unique # MySQL "unique" constraint
|
display_name: str,
|
||||||
self.primary = primary # MySQL "primary key" constraint
|
html_input_type: str,
|
||||||
self.regex = regex # Regex for value validation
|
required: bool = False,
|
||||||
self.min = min # HTML form input "min" attribute. Sets minimum value.
|
unique: bool = False,
|
||||||
self.max = max # HTML form input "max" attribute. Sets maximum value.
|
primary: bool = False,
|
||||||
self.options = options # List of options for "select" inputs
|
default_val: Optional[str] = None,
|
||||||
self.default_val = default_val # HTML form input "value" attribute. Sets default value.
|
compareto: Optional[List[Tuple[str, str]]] = None,
|
||||||
self.auto_increment = auto_increment # bool: MySQL autoincrement
|
):
|
||||||
self.index = index # bool: MySQL index
|
self.attrib_name = attrib_name
|
||||||
self.comment = comment # Description text
|
self.display_name = display_name
|
||||||
self.compareto = compareto # Compare to another attribute of the item for validation: ["comparison", "referenceattrib"]
|
self.html_input_type = html_input_type
|
||||||
|
self.required = required
|
||||||
|
self.unique = unique
|
||||||
|
self.primary = primary
|
||||||
|
self.default_val = default_val
|
||||||
|
self.compareto = compareto
|
||||||
|
|
||||||
|
def validate(self) -> Optional[str]:
|
||||||
|
"""Validate common attributes. Returns an error message if invalid, otherwise None."""
|
||||||
|
if not self.attrib_name:
|
||||||
|
return "Missing attribute name."
|
||||||
|
if not self.display_name:
|
||||||
|
return f"Missing display name for attribute '{self.attrib_name}'."
|
||||||
|
if not self.html_input_type:
|
||||||
|
return f"Missing input type for attribute '{self.attrib_name}'."
|
||||||
|
if self.html_input_type not in ["text", "number", "date", "select"]:
|
||||||
|
return f"Invalid input type for attribute '{self.attrib_name}'."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class textAttribute(Attribute):
|
||||||
|
"""Class for text attributes."""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
attrib_name: str,
|
||||||
|
display_name: str,
|
||||||
|
regex: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(attrib_name, display_name, html_input_type="text", **kwargs)
|
||||||
|
self.regex = regex
|
||||||
|
|
||||||
|
def validate(self) -> Optional[str]:
|
||||||
|
"""Validate text-specific attributes."""
|
||||||
|
error = super().validate()
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
if self.default_val is not None and self.regex is not None:
|
||||||
|
if not re.match(self.regex, str(self.default_val)):
|
||||||
|
return f"default_val does not match the regex pattern for attribute '{self.attrib_name}'."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class intAttribute(Attribute):
|
||||||
|
"""Class for integer attributes."""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
attrib_name: str,
|
||||||
|
display_name: str,
|
||||||
|
min_val: Optional[int] = None,
|
||||||
|
max_val: Optional[int] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(attrib_name, display_name, html_input_type="number", **kwargs)
|
||||||
|
self.min_val = min_val
|
||||||
|
self.max_val = max_val
|
||||||
|
|
||||||
|
def validate(self) -> Optional[str]:
|
||||||
|
"""Validate integer-specific attributes."""
|
||||||
|
error = super().validate()
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
if self.min_val is not None and not isinstance(self.min_val, int):
|
||||||
|
return f"min_val must be an integer for attribute '{self.attrib_name}'."
|
||||||
|
if self.max_val is not None and not isinstance(self.max_val, int):
|
||||||
|
return f"max_val must be an integer for attribute '{self.attrib_name}'."
|
||||||
|
if self.min_val is not None and self.max_val is not None and self.min_val > self.max_val:
|
||||||
|
return f"min_val cannot be greater than max_val for attribute '{self.attrib_name}'."
|
||||||
|
if self.default_val is not None and not isinstance(self.default_val, int):
|
||||||
|
return f"default_val must be an integer for attribute '{self.attrib_name}'."
|
||||||
|
if self.default_val is not None:
|
||||||
|
if self.min_val is not None and self.default_val < self.min_val:
|
||||||
|
return f"default_val cannot be less than min_val for attribute '{self.attrib_name}'."
|
||||||
|
if self.max_val is not None and self.default_val > self.max_val:
|
||||||
|
return f"default_val cannot be greater than max_val for attribute '{self.attrib_name}'."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class floatAttribute(Attribute):
|
||||||
|
"""Class for float attributes."""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
attrib_name: str,
|
||||||
|
display_name: str,
|
||||||
|
min_val: Optional[float] = None,
|
||||||
|
max_val: Optional[float] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(attrib_name, display_name, html_input_type="number", **kwargs)
|
||||||
|
self.min_val = min_val
|
||||||
|
self.max_val = max_val
|
||||||
|
|
||||||
|
def validate(self) -> Optional[str]:
|
||||||
|
"""Validate float-specific attributes."""
|
||||||
|
error = super().validate()
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
if self.min_val is not None and not isinstance(self.min_val, (int, float)):
|
||||||
|
return f"min_val must be a number for attribute '{self.attrib_name}'."
|
||||||
|
if self.max_val is not None and not isinstance(self.max_val, (int, float)):
|
||||||
|
return f"max_val must be a number for attribute '{self.attrib_name}'."
|
||||||
|
if self.min_val is not None and self.max_val is not None and self.min_val > self.max_val:
|
||||||
|
return f"min_val cannot be greater than max_val for attribute '{self.attrib_name}'."
|
||||||
|
if self.default_val is not None and not isinstance(self.default_val, (int, float)):
|
||||||
|
return f"default_val must be a number for attribute '{self.attrib_name}'."
|
||||||
|
if self.default_val is not None:
|
||||||
|
if self.min_val is not None and self.default_val < self.min_val:
|
||||||
|
return f"default_val cannot be less than min_val for attribute '{self.attrib_name}'."
|
||||||
|
if self.max_val is not None and self.default_val > self.max_val:
|
||||||
|
return f"default_val cannot be greater than max_val for attribute '{self.attrib_name}'."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class dateAttribute(Attribute):
|
||||||
|
"""Class for date attributes."""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
attrib_name: str,
|
||||||
|
display_name: str,
|
||||||
|
min_val: Optional[str] = None,
|
||||||
|
max_val: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(attrib_name, display_name, html_input_type="date", **kwargs)
|
||||||
|
self.min_val = min_val
|
||||||
|
self.max_val = max_val
|
||||||
|
|
||||||
|
def _is_date(self, value: str) -> bool:
|
||||||
|
"""Check if a value is a valid date in YYYY-MM-DD format."""
|
||||||
|
try:
|
||||||
|
datetime.strptime(value, "%Y-%m-%d")
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def validate(self) -> Optional[str]:
|
||||||
|
"""Validate date-specific attributes."""
|
||||||
|
error = super().validate()
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
if self.min_val is not None and not self._is_date(self.min_val):
|
||||||
|
return f"min_val must be a valid date (YYYY-MM-DD) for attribute '{self.attrib_name}'."
|
||||||
|
if self.max_val is not None and not self._is_date(self.max_val):
|
||||||
|
return f"max_val must be a valid date (YYYY-MM-DD) for attribute '{self.attrib_name}'."
|
||||||
|
if self.min_val is not None and self.max_val is not None:
|
||||||
|
min_date = datetime.strptime(self.min_val, "%Y-%m-%d")
|
||||||
|
max_date = datetime.strptime(self.max_val, "%Y-%m-%d")
|
||||||
|
if max_date < min_date:
|
||||||
|
return f"max_val cannot be earlier than min_val for attribute '{self.attrib_name}'."
|
||||||
|
if self.default_val is not None and not self._is_date(self.default_val):
|
||||||
|
return f"default_val must be a valid date (YYYY-MM-DD) for attribute '{self.attrib_name}'."
|
||||||
|
if self.default_val is not None:
|
||||||
|
default_date = datetime.strptime(self.default_val, "%Y-%m-%d")
|
||||||
|
if self.min_val is not None:
|
||||||
|
min_date = datetime.strptime(self.min_val, "%Y-%m-%d")
|
||||||
|
if default_date < min_date:
|
||||||
|
return f"default_val cannot be earlier than min_val for attribute '{self.attrib_name}'."
|
||||||
|
if self.max_val is not None:
|
||||||
|
max_date = datetime.strptime(self.max_val, "%Y-%m-%d")
|
||||||
|
if default_date > max_date:
|
||||||
|
return f"default_val cannot be later than max_val for attribute '{self.attrib_name}'."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class selectAttribute(Attribute):
|
||||||
|
"""Class for select attributes."""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
attrib_name: str,
|
||||||
|
display_name: str,
|
||||||
|
options: List[str],
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(attrib_name, display_name, html_input_type="select", **kwargs)
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
def validate(self) -> Optional[str]:
|
||||||
|
"""Validate select-specific attributes."""
|
||||||
|
error = super().validate()
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
if not self.options:
|
||||||
|
return f"options cannot be empty for attribute '{self.attrib_name}'."
|
||||||
|
if self.default_val is not None and self.default_val not in self.options:
|
||||||
|
return f"default_val must be one of the options for attribute '{self.attrib_name}'."
|
||||||
|
return None
|
@ -1,39 +1,43 @@
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from config import item_attributes # Import the configuration
|
|
||||||
from sqlalchemy import Enum, Integer, String, Date, Column
|
from sqlalchemy import Enum, Integer, String, Date, Column
|
||||||
|
from config import item_attributes, sql_conf
|
||||||
|
from definitions.attribute import textAttribute, intAttribute, floatAttribute, dateAttribute, selectAttribute
|
||||||
|
|
||||||
|
# Initialize SQLAlchemy
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
# Dynamically create the Asset model
|
|
||||||
def create_asset_model():
|
def create_asset_model():
|
||||||
"""
|
"""
|
||||||
Dynamically creates the Asset model based on the configuration in item_attributes.
|
Dynamically creates the Asset model based on the configuration in item_attributes.
|
||||||
"""
|
"""
|
||||||
|
# Define the table name and basic methods
|
||||||
attrs = {
|
attrs = {
|
||||||
'__tablename__': 'asset_test', # Table name
|
'__tablename__': sql_conf.SQL_TABLE, # Table name from config
|
||||||
'__init__': lambda self, **kwargs: self.__dict__.update(kwargs), # Constructor
|
'__init__': lambda self, **kwargs: self.__dict__.update(kwargs), # Constructor
|
||||||
'__repr__': lambda self: f"<Asset {getattr(self, next(attrib for attrib, config in item_attributes.items() if config.primary))}>" # Representation
|
'__repr__': lambda self: f"<Asset {getattr(self, next(attrib.attrib_name for attrib in item_attributes if attrib.primary))}>" # Representation
|
||||||
}
|
}
|
||||||
|
|
||||||
for attrib, config in item_attributes.items():
|
# Define columns based on item_attributes
|
||||||
# Determine the column type based on the configuration
|
for attrib in item_attributes:
|
||||||
if config.html_input_type == "text":
|
# Determine the column type
|
||||||
|
if isinstance(attrib, textAttribute):
|
||||||
column_type = String(50)
|
column_type = String(50)
|
||||||
elif config.html_input_type == "date":
|
elif isinstance(attrib, dateAttribute):
|
||||||
column_type = Date
|
column_type = Date
|
||||||
elif config.html_input_type == "select":
|
elif isinstance(attrib, selectAttribute):
|
||||||
column_type = Enum(*config.options)
|
column_type = Enum(*attrib.options)
|
||||||
elif config.html_input_type == "number":
|
elif isinstance(attrib, (intAttribute, floatAttribute)):
|
||||||
column_type = Integer
|
column_type = Integer if isinstance(attrib, intAttribute) else Float
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported attribute type: {type(attrib)}")
|
||||||
|
|
||||||
# Define the column with additional properties
|
# Define the column with additional properties
|
||||||
attrs[attrib] = Column(
|
attrs[attrib.attrib_name] = Column(
|
||||||
column_type,
|
column_type,
|
||||||
primary_key=config.primary,
|
primary_key=attrib.primary,
|
||||||
unique=config.unique,
|
unique=attrib.unique,
|
||||||
nullable=not config.required,
|
nullable=not attrib.required,
|
||||||
default=config.default_val,
|
default=attrib.default_val
|
||||||
comment=config.comment
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the Asset class dynamically
|
# Create the Asset class dynamically
|
||||||
|
@ -3,20 +3,34 @@ from io import TextIOWrapper
|
|||||||
from config import item_attributes
|
from config import item_attributes
|
||||||
|
|
||||||
def get_csv_data(file):
|
def get_csv_data(file):
|
||||||
|
"""Extract and validate CSV data."""
|
||||||
csv_file = TextIOWrapper(file, encoding='utf-8')
|
csv_file = TextIOWrapper(file, encoding='utf-8')
|
||||||
|
|
||||||
|
# Check delimiter
|
||||||
|
sample = csv_file.read(1024)
|
||||||
|
csv_file.seek(0)
|
||||||
|
if '|' not in sample:
|
||||||
|
raise ValueError("CSV file must use '|' as the delimiter.")
|
||||||
|
|
||||||
reader = csv.DictReader(csv_file, delimiter='|')
|
reader = csv.DictReader(csv_file, delimiter='|')
|
||||||
|
|
||||||
# Validate CSV headers based on config
|
# Validate CSV headers based on config
|
||||||
required_headers = set(item_attributes.keys()) # Get required headers as per configuration
|
required_headers = {attrib.display_name for attrib in item_attributes} # Use display names
|
||||||
csv_headers = set(reader.fieldnames) # Get headers present in the csv file
|
csv_headers = set(reader.fieldnames) # Get headers present in the CSV file
|
||||||
|
|
||||||
# Check if the required headers exist
|
# Check if the required headers exist
|
||||||
if not required_headers.issubset(reader.fieldnames):
|
if not required_headers.issubset(csv_headers):
|
||||||
raise ValueError(f"CSV file must include headers: {', '.join(required_headers)}.")
|
raise ValueError(f"CSV file must include headers: {', '.join(required_headers)}.")
|
||||||
|
|
||||||
# Check for invalid headers
|
# Check for invalid headers
|
||||||
if extra_headers := csv_headers - required_headers:
|
if extra_headers := csv_headers - required_headers:
|
||||||
raise ValueError(f"Unexpected headers found: {', '.join(extra_headers)}")
|
raise ValueError(f"Unexpected headers found: {', '.join(extra_headers)}")
|
||||||
|
|
||||||
|
# Map display names to attribute names
|
||||||
|
display_to_attrib = {attrib.display_name: attrib.attrib_name for attrib in item_attributes}
|
||||||
|
|
||||||
# Extract data dynamically based on config
|
# Extract data dynamically based on config
|
||||||
return [{attrib: row[attrib] for attrib in item_attributes} for row in reader]
|
return [
|
||||||
|
{display_to_attrib[header]: row[header] for header in required_headers}
|
||||||
|
for row in reader
|
||||||
|
]
|
@ -1,106 +1,32 @@
|
|||||||
import re
|
from typing import List
|
||||||
from datetime import datetime
|
from definitions.attribute import Attribute
|
||||||
|
|
||||||
def _validate_number(attrib, attrib_name):
|
def validate_config(item_attributes: List[Attribute]) -> str:
|
||||||
"""Validate number-specific attributes."""
|
"""
|
||||||
if attrib.min and not _is_int(attrib.min):
|
Validate the configuration file to ensure all attributes are properly defined.
|
||||||
return False
|
Returns "Ok" if everything is valid, otherwise returns an error message.
|
||||||
if attrib.max and not _is_int(attrib.max):
|
"""
|
||||||
return False
|
# Check for duplicate attribute names
|
||||||
if attrib.min and attrib.max and int(attrib.max) < int(attrib.min):
|
attrib_names = [attrib.attrib_name for attrib in item_attributes]
|
||||||
return False
|
if len(attrib_names) != len(set(attrib_names)):
|
||||||
if attrib.default_val and not _is_int(attrib.default_val):
|
return "Duplicate attribute names are not allowed."
|
||||||
return False
|
|
||||||
if attrib.default_val:
|
|
||||||
if attrib.min and int(attrib.default_val) < int(attrib.min):
|
|
||||||
return False
|
|
||||||
if attrib.max and int(attrib.default_val) > int(attrib.max):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _validate_date(attrib, attrib_name):
|
# Validate each attribute
|
||||||
"""Validate date-specific attributes."""
|
for attrib in item_attributes:
|
||||||
if attrib.min and not _is_date(attrib.min):
|
error = attrib.validate()
|
||||||
return False
|
if error:
|
||||||
if attrib.max and not _is_date(attrib.max):
|
return error
|
||||||
return False
|
|
||||||
if attrib.min and attrib.max and datetime.strptime(attrib.max, "%Y-%m-%d") < datetime.strptime(attrib.min, "%Y-%m-%d"):
|
|
||||||
return False
|
|
||||||
if attrib.default_val and not _is_date(attrib.default_val):
|
|
||||||
return False
|
|
||||||
if attrib.default_val:
|
|
||||||
if attrib.min and datetime.strptime(attrib.default_val, "%Y-%m-%d") < datetime.strptime(attrib.min, "%Y-%m-%d"):
|
|
||||||
return False
|
|
||||||
if attrib.max and datetime.strptime(attrib.default_val, "%Y-%m-%d") > datetime.strptime(attrib.max, "%Y-%m-%d"):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _validate_text(attrib, attrib_name):
|
# Validate comparison (if applicable)
|
||||||
"""Validate text-specific attributes."""
|
|
||||||
if attrib.min or attrib.max or attrib.auto_increment or attrib.options:
|
|
||||||
return False
|
|
||||||
if attrib.default_val and attrib.regex and not re.match(attrib.regex, str(attrib.default_val)):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _validate_select(attrib, attrib_name):
|
|
||||||
"""Validate select-specific attributes."""
|
|
||||||
if not attrib.options:
|
|
||||||
return False
|
|
||||||
if attrib.default_val and attrib.default_val not in attrib.options:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _is_int(value):
|
|
||||||
"""Check if a value is a valid integer."""
|
|
||||||
try:
|
|
||||||
int(value)
|
|
||||||
return True
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _is_date(value):
|
|
||||||
"""Check if a value is a valid date in YYYY-MM-DD format."""
|
|
||||||
try:
|
|
||||||
datetime.strptime(value, "%Y-%m-%d")
|
|
||||||
return True
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Validate the configuration file to ensure all attributes are properly defined.
|
|
||||||
def validate_config(item_attributes):
|
|
||||||
for attrib_name, attrib in item_attributes.items():
|
|
||||||
# Validate display_name and html_input_type
|
|
||||||
if not attrib.display_name:
|
|
||||||
return f"Missing display name for attribute '{attrib_name}'."
|
|
||||||
if not attrib.html_input_type:
|
|
||||||
return f"Missing input type for attribute '{attrib_name}'."
|
|
||||||
if attrib.html_input_type not in ["text", "number", "date", "select"]:
|
|
||||||
return f"Invalid input type for attribute '{attrib_name}'."
|
|
||||||
|
|
||||||
# Validate min, max, and default values based on input type
|
|
||||||
if attrib.html_input_type == "number":
|
|
||||||
if not _validate_number(attrib, attrib_name):
|
|
||||||
return f"Invalid number configuration for attribute '{attrib_name}'."
|
|
||||||
elif attrib.html_input_type == "date":
|
|
||||||
if not _validate_date(attrib, attrib_name):
|
|
||||||
return f"Invalid date configuration for attribute '{attrib_name}'."
|
|
||||||
elif attrib.html_input_type == "text":
|
|
||||||
if not _validate_text(attrib, attrib_name):
|
|
||||||
return f"Invalid text configuration for attribute '{attrib_name}'."
|
|
||||||
elif attrib.html_input_type == "select":
|
|
||||||
if not _validate_select(attrib, attrib_name):
|
|
||||||
return f"Invalid select configuration for attribute '{attrib_name}'."
|
|
||||||
|
|
||||||
# Validate min and max values
|
|
||||||
if (attrib.min or attrib.max) and attrib.html_input_type not in ['number', 'date']:
|
|
||||||
return f"'{attrib_name}' must be of type 'number' or 'date' to have min or max values."
|
|
||||||
|
|
||||||
# Validate comparison
|
|
||||||
if attrib.compareto:
|
if attrib.compareto:
|
||||||
if attrib.compareto[1] not in item_attributes:
|
if not isinstance(attrib.compareto, list) or not all(
|
||||||
return f"Invalid reference attribute '{attrib.compareto[1]}' for comparison in attribute '{attrib_name}'."
|
isinstance(pair, tuple) and len(pair) == 2 for pair in attrib.compareto
|
||||||
if attrib.compareto[0] not in ["lt", "gt", "le", "ge", "eq"]:
|
):
|
||||||
return f"Invalid comparison operator for attribute '{attrib_name}'. Valid operators are: lt, gt, le, ge, eq."
|
return f"Invalid comparison format for attribute '{attrib.attrib_name}'. Expected a list of tuples."
|
||||||
# Return "Ok" if everything is valid, otherwise return an error message.
|
for cmp, ref_attr in attrib.compareto:
|
||||||
|
if cmp not in ["lt", "gt", "le", "ge", "eq"]:
|
||||||
|
return f"Invalid comparison operator '{cmp}' for attribute '{attrib.attrib_name}'."
|
||||||
|
if ref_attr not in attrib_names:
|
||||||
|
return f"Invalid reference attribute '{ref_attr}' for comparison in attribute '{attrib.attrib_name}'."
|
||||||
|
|
||||||
return "Ok"
|
return "Ok"
|
@ -1,43 +1,103 @@
|
|||||||
import re
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from definitions.attribute import *
|
||||||
|
from config import item_attributes
|
||||||
|
|
||||||
# Validate the input value based on the attribute's constraints
|
def _is_int(value: Any) -> bool:
|
||||||
def validate_value(attrib, input_val):
|
"""Check if a value is a valid integer (including string representations)."""
|
||||||
# Check if the value is required
|
if isinstance(value, int):
|
||||||
if attrib.required and not input_val:
|
return True
|
||||||
return f"{attrib.display_name} is required."
|
if isinstance(value, str):
|
||||||
|
|
||||||
# Validate based on input type
|
|
||||||
if attrib.html_input_type == "number":
|
|
||||||
try:
|
try:
|
||||||
input_val = int(input_val)
|
int(value)
|
||||||
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return f"{attrib.display_name} must be a valid number."
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
if attrib.min is not None and input_val < attrib.min:
|
def _is_float(value: Any) -> bool:
|
||||||
return f"{attrib.display_name} must be at least {attrib.min}."
|
"""Check if a value is a valid float (including string representations)."""
|
||||||
if attrib.max is not None and input_val > attrib.max:
|
if isinstance(value, (int, float)):
|
||||||
return f"{attrib.display_name} must be at most {attrib.max}."
|
return True
|
||||||
elif attrib.html_input_type == "date":
|
if isinstance(value, str):
|
||||||
try:
|
try:
|
||||||
input_val = datetime.strptime(input_val, "%Y-%m-%d") # Validate date format
|
float(value)
|
||||||
if attrib.min is not None and input_val < datetime.strptime(attrib.min, "%Y-%m-%d"):
|
return True
|
||||||
return f"{attrib.display_name} must be on or after {attrib.min}."
|
except ValueError:
|
||||||
if attrib.max is not None and input_val > datetime.strptime(attrib.max, "%Y-%m-%d"):
|
return False
|
||||||
return f"{attrib.display_name} must be on or before {attrib.max}."
|
return False
|
||||||
except:
|
|
||||||
return f"{attrib.display_name} must be a valid date in YYYY-MM-DD format."
|
|
||||||
elif attrib.html_input_type == "select":
|
|
||||||
if input_val not in attrib.options:
|
|
||||||
return f"{attrib.display_name} must be one of {attrib.options}."
|
|
||||||
elif attrib.html_input_type == "text":
|
|
||||||
if attrib.regex and not re.match(attrib.regex, input_val):
|
|
||||||
return f"{attrib.display_name} is invalid. Allowed pattern: {attrib.regex}."
|
|
||||||
|
|
||||||
# Validate comparison
|
def _is_date(value: Any) -> bool:
|
||||||
#if attrib.compareto:
|
"""Check if a value is a valid date in YYYY-MM-DD format."""
|
||||||
# compare attrib value
|
if not isinstance(value, str):
|
||||||
#return ""
|
return False
|
||||||
|
try:
|
||||||
|
datetime.strptime(value, "%Y-%m-%d")
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
# If all checks pass, return "Ok"
|
def validate_values(form_data: Dict[str, Any]) -> Optional[str]:
|
||||||
return "Ok"
|
"""
|
||||||
|
Validate form data against the configuration in item_attributes.
|
||||||
|
Returns an error message if invalid, otherwise None.
|
||||||
|
"""
|
||||||
|
# Check if all required fields are present
|
||||||
|
for attrib in item_attributes:
|
||||||
|
if attrib.required and attrib.attrib_name not in form_data:
|
||||||
|
return f"Missing required field: {attrib.display_name}."
|
||||||
|
|
||||||
|
# Check for unexpected fields
|
||||||
|
submitted_fields = set(form_data.keys())
|
||||||
|
expected_fields = {attrib.attrib_name for attrib in item_attributes}
|
||||||
|
if unexpected_fields := submitted_fields - expected_fields:
|
||||||
|
return f"Unexpected fields submitted: {', '.join(unexpected_fields)}."
|
||||||
|
|
||||||
|
# Validate each field
|
||||||
|
for attrib in item_attributes:
|
||||||
|
if attrib.attrib_name not in form_data:
|
||||||
|
continue # Skip optional fields that are not submitted
|
||||||
|
|
||||||
|
value = form_data[attrib.attrib_name]
|
||||||
|
|
||||||
|
# Validate based on attribute type
|
||||||
|
if isinstance(attrib, textAttribute):
|
||||||
|
if attrib.regex is not None and not re.match(attrib.regex, str(value)):
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must match pattern: {attrib.regex}."
|
||||||
|
|
||||||
|
elif isinstance(attrib, intAttribute):
|
||||||
|
if not _is_int(value):
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be an integer."
|
||||||
|
value_int = int(value) # Convert to integer for range checks
|
||||||
|
if attrib.min_val is not None and value_int < attrib.min_val:
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be at least {attrib.min_val}."
|
||||||
|
if attrib.max_val is not None and value_int > attrib.max_val:
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be at most {attrib.max_val}."
|
||||||
|
|
||||||
|
elif isinstance(attrib, floatAttribute):
|
||||||
|
if not _is_float(value):
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be a number."
|
||||||
|
value_float = float(value) # Convert to float for range checks
|
||||||
|
if attrib.min_val is not None and value_float < attrib.min_val:
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be at least {attrib.min_val}."
|
||||||
|
if attrib.max_val is not None and value_float > attrib.max_val:
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be at most {attrib.max_val}."
|
||||||
|
|
||||||
|
elif isinstance(attrib, dateAttribute):
|
||||||
|
if not _is_date(value):
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be a valid date (YYYY-MM-DD)."
|
||||||
|
if attrib.min_val is not None:
|
||||||
|
min_date = datetime.strptime(attrib.min_val, "%Y-%m-%d")
|
||||||
|
if datetime.strptime(value, "%Y-%m-%d") < min_date:
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be on or after {attrib.min_val}."
|
||||||
|
if attrib.max_val is not None:
|
||||||
|
max_date = datetime.strptime(attrib.max_val, "%Y-%m-%d")
|
||||||
|
if datetime.strptime(value, "%Y-%m-%d") > max_date:
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be on or before {attrib.max_val}."
|
||||||
|
|
||||||
|
elif isinstance(attrib, selectAttribute):
|
||||||
|
if value not in attrib.options:
|
||||||
|
return f"Invalid value for {attrib.display_name}. Must be one of: {', '.join(attrib.options)}."
|
||||||
|
|
||||||
|
return None # No errors
|
@ -1,7 +1,7 @@
|
|||||||
from flask import Blueprint, redirect, session, request, jsonify
|
from flask import Blueprint, redirect, session, request, jsonify
|
||||||
from definitions.models import Asset, db
|
from definitions.models import Asset, db
|
||||||
import json
|
import json
|
||||||
from config import item_attributes # Import the configuration
|
from config import item_attributes
|
||||||
|
|
||||||
confirm_save_bp = Blueprint('confirm_save', __name__)
|
confirm_save_bp = Blueprint('confirm_save', __name__)
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ def confirm_save():
|
|||||||
for asset_data in edited_assets:
|
for asset_data in edited_assets:
|
||||||
# Dynamically create the Asset object using item_attributes
|
# Dynamically create the Asset object using item_attributes
|
||||||
asset = Asset(**{
|
asset = Asset(**{
|
||||||
attrib: asset_data[attrib]
|
attrib.attrib_name: asset_data[attrib.attrib_name]
|
||||||
for attrib in item_attributes
|
for attrib in item_attributes
|
||||||
})
|
})
|
||||||
db.session.add(asset)
|
db.session.add(asset)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from flask import Blueprint, request, render_template, redirect
|
from flask import Blueprint, request, render_template, redirect, flash
|
||||||
from definitions.models import Asset, db
|
from definitions.models import Asset, db
|
||||||
from config import item_attributes
|
from config import item_attributes
|
||||||
from sqlalchemy import exc # Import exc for database exceptions
|
from sqlalchemy import exc # Import exc for database exceptions
|
||||||
|
from functions.validate_values import validate_values # Form validation
|
||||||
|
|
||||||
addasset_bp = Blueprint('addasset', __name__)
|
addasset_bp = Blueprint('addasset', __name__)
|
||||||
|
|
||||||
@ -14,20 +15,35 @@ def create():
|
|||||||
# Process submitted form
|
# Process submitted form
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Get data from form
|
# Get data from form
|
||||||
form_data = {attrib: request.form[attrib] for attrib in item_attributes}
|
form_data = {attrib.attrib_name: request.form.get(attrib.attrib_name) for attrib in item_attributes}
|
||||||
try:
|
|
||||||
primary_attr = next((attrib_name for attrib_name, attrib in item_attributes.items() if attrib.primary), None)
|
|
||||||
except ValueError:
|
|
||||||
return render_template('create.html', item_attributes=item_attributes, exc=primary_attr)
|
|
||||||
|
|
||||||
|
# Form validation
|
||||||
|
error = validate_values(form_data)
|
||||||
|
if error:
|
||||||
|
return render_template('create.html', item_attributes=item_attributes, error=error)
|
||||||
|
|
||||||
|
# Create the Asset object
|
||||||
item = Asset(**form_data)
|
item = Asset(**form_data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.session.add(item)
|
db.session.add(item)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except exc.IntegrityError:
|
except exc.IntegrityError as e:
|
||||||
return render_template('create.html', item_attributes=item_attributes, exc='integrity')
|
# Handle duplicate primary key or unique constraint errors
|
||||||
except exc.StatementError:
|
primary_attrib = next((attrib for attrib in item_attributes if attrib.primary), None)
|
||||||
return render_template('create.html', item_attributes=item_attributes, exc='status')
|
if primary_attrib:
|
||||||
|
error = f"An entry with {primary_attrib.display_name} '{form_data[primary_attrib.attrib_name]}' already exists."
|
||||||
|
else:
|
||||||
|
error = "An entry with the same primary key already exists."
|
||||||
|
return render_template('create.html', item_attributes=item_attributes, error=error)
|
||||||
|
except exc.StatementError as e:
|
||||||
|
# Handle other database errors
|
||||||
|
error = f"Database error: {str(e)}"
|
||||||
|
return render_template('create.html', item_attributes=item_attributes, error=error)
|
||||||
|
except Exception as e:
|
||||||
|
# Handle unexpected errors
|
||||||
|
error = f"An unexpected error occurred: {str(e)}"
|
||||||
|
return render_template('create.html', item_attributes=item_attributes, error=error)
|
||||||
|
|
||||||
|
# Redirect to /viewall on success
|
||||||
return redirect('/viewall')
|
return redirect('/viewall')
|
@ -8,7 +8,7 @@ delete_bp = Blueprint('deleteasset', __name__)
|
|||||||
def delete(primary_value):
|
def delete(primary_value):
|
||||||
# Identify the primary attribute
|
# Identify the primary attribute
|
||||||
primary_attrib = next(
|
primary_attrib = next(
|
||||||
(attrib for attrib, config in item_attributes.items() if config.primary),
|
(attrib for attrib in item_attributes if attrib.primary),
|
||||||
None
|
None
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ def delete(primary_value):
|
|||||||
return "Primary attribute not defined in configuration."
|
return "Primary attribute not defined in configuration."
|
||||||
|
|
||||||
# Fetch the item using the primary attribute
|
# Fetch the item using the primary attribute
|
||||||
item = Asset.query.filter_by(**{primary_attrib: primary_value}).first()
|
item = Asset.query.filter_by(**{primary_attrib.attrib_name: primary_value}).first()
|
||||||
if not item:
|
if not item:
|
||||||
abort(404) # Item not found
|
abort(404) # Item not found
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from flask import Blueprint, Response
|
from flask import Blueprint, Response
|
||||||
from definitions.models import Asset
|
from definitions.models import Asset
|
||||||
|
from config import item_attributes
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
|
||||||
@ -14,12 +15,13 @@ def export_csv():
|
|||||||
# Fetch all records from the database
|
# Fetch all records from the database
|
||||||
records = Asset.query.all()
|
records = Asset.query.all()
|
||||||
|
|
||||||
# Write headers
|
# Write headers (use display names from item_attributes)
|
||||||
writer.writerow([column.name for column in Asset.__mapper__.columns])
|
headers = [attrib.display_name for attrib in item_attributes]
|
||||||
|
writer.writerow(headers)
|
||||||
|
|
||||||
# Write data rows
|
# Write data rows
|
||||||
for record in records:
|
for record in records:
|
||||||
writer.writerow([getattr(record, column.name) for column in Asset.__mapper__.columns])
|
writer.writerow([getattr(record, attrib.attrib_name) for attrib in item_attributes])
|
||||||
|
|
||||||
# Prepare the response
|
# Prepare the response
|
||||||
response = Response(
|
response = Response(
|
||||||
|
@ -2,53 +2,53 @@ from flask import Blueprint, request, render_template, redirect
|
|||||||
from definitions.models import Asset, db
|
from definitions.models import Asset, db
|
||||||
from config import item_attributes
|
from config import item_attributes
|
||||||
from sqlalchemy import exc # Import exc for database exceptions
|
from sqlalchemy import exc # Import exc for database exceptions
|
||||||
|
from functions.validate_values import validate_values # Form validation
|
||||||
|
|
||||||
update_bp = Blueprint('editasset', __name__)
|
update_bp = Blueprint('editasset', __name__)
|
||||||
|
|
||||||
@update_bp.route('/update/<string:primary_value>/', methods=['GET', 'POST'])
|
@update_bp.route('/update/<string:primary_value>/', methods=['GET', 'POST'])
|
||||||
def update(primary_value):
|
def update(primary_value):
|
||||||
# Identify the primary attribute
|
# Identify the primary attribute
|
||||||
primary_attrib = next(
|
primary_attrib = next((attrib for attrib in item_attributes if attrib.primary), None)
|
||||||
(attrib for attrib, config in item_attributes.items() if config.primary),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
|
|
||||||
if not primary_attrib:
|
if not primary_attrib:
|
||||||
return "Primary attribute not defined in configuration."
|
return "Primary attribute not defined in configuration."
|
||||||
|
|
||||||
# Fetch the item using the primary attribute
|
# Fetch the item using the primary attribute
|
||||||
item = Asset.query.filter_by(**{primary_attrib: primary_value}).first()
|
item = Asset.query.filter_by(**{primary_attrib.attrib_name: primary_value}).first()
|
||||||
if not item:
|
if not item:
|
||||||
return f"{item_attributes[primary_attrib].display_name} '{primary_value}' not found." # Move this to a template
|
return f"{primary_attrib.display_name} '{primary_value}' not found."
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
|
# Render the update form with the current item data
|
||||||
return render_template('update.html', item=item, item_attributes=item_attributes)
|
return render_template('update.html', item=item, item_attributes=item_attributes)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Dynamically get form data using item_attributes
|
# Get data from form
|
||||||
form_data = {attrib: request.form[attrib] for attrib in item_attributes}
|
form_data = {attrib.attrib_name: request.form.get(attrib.attrib_name) for attrib in item_attributes}
|
||||||
|
|
||||||
# Validate status (if it's an option field)
|
# Form validation
|
||||||
if 'status' in item_attributes and item_attributes['status'].html_input_type == "select":
|
error = validate_values(form_data)
|
||||||
if form_data['status'] not in item_attributes['status'].options:
|
if error:
|
||||||
return render_template('update.html', item=item, exc='status', item_attributes=item_attributes)
|
return render_template('update.html', item=item, item_attributes=item_attributes, error=error)
|
||||||
|
|
||||||
# Convert staffnum to int (if it's a number field)
|
|
||||||
if 'staffnum' in item_attributes and item_attributes['staffnum'].html_input_type == "number":
|
|
||||||
try:
|
|
||||||
form_data['staffnum'] = int(form_data['staffnum'])
|
|
||||||
except ValueError:
|
|
||||||
return render_template('update.html', item=item, exc='staffnum', item_attributes=item_attributes)
|
|
||||||
|
|
||||||
# Update the item with the new data
|
# Update the item with the new data
|
||||||
for attrib, value in form_data.items():
|
for attrib in item_attributes:
|
||||||
setattr(item, attrib, value)
|
setattr(item, attrib.attrib_name, form_data[attrib.attrib_name])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except exc.IntegrityError:
|
except exc.IntegrityError:
|
||||||
return render_template('update.html', item=item, exc='integrity', item_attributes=item_attributes)
|
# Handle duplicate primary key or unique constraint errors
|
||||||
except (exc.StatementError, exc.InvalidRequestError):
|
error = f"An entry with {primary_attrib.display_name} '{form_data[primary_attrib.attrib_name]}' already exists."
|
||||||
return render_template('update.html', item=item, exc='status', item_attributes=item_attributes)
|
return render_template('update.html', item=item, item_attributes=item_attributes, error=error)
|
||||||
|
except exc.StatementError as e:
|
||||||
|
# Handle other database errors
|
||||||
|
error = f"Database error: {str(e)}"
|
||||||
|
return render_template('update.html', item=item, item_attributes=item_attributes, error=error)
|
||||||
|
except Exception as e:
|
||||||
|
# Handle unexpected errors
|
||||||
|
error = f"An unexpected error occurred: {str(e)}"
|
||||||
|
return render_template('update.html', item=item, item_attributes=item_attributes, error=error)
|
||||||
|
|
||||||
|
# Redirect to /viewall on success
|
||||||
return redirect('/viewall')
|
return redirect('/viewall')
|
@ -1,11 +1,9 @@
|
|||||||
from flask import Blueprint, request, render_template, redirect, session
|
from flask import Blueprint, request, render_template, redirect, session, flash
|
||||||
import csv
|
|
||||||
from io import TextIOWrapper
|
|
||||||
from definitions.models import Asset, db
|
from definitions.models import Asset, db
|
||||||
from functions.process_csv import get_csv_data # Import the CSV processing function
|
from functions.process_csv import get_csv_data
|
||||||
from config import item_attributes # Import the configuration
|
from functions.validate_values import validate_values
|
||||||
|
from config import item_attributes
|
||||||
|
|
||||||
# Create a Blueprint for upload
|
|
||||||
upload_bp = Blueprint('uploadcsv', __name__)
|
upload_bp = Blueprint('uploadcsv', __name__)
|
||||||
|
|
||||||
@upload_bp.route('/uploadcsv', methods=['GET', 'POST'])
|
@upload_bp.route('/uploadcsv', methods=['GET', 'POST'])
|
||||||
@ -13,33 +11,46 @@ def upload_file():
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Check if a file was uploaded
|
# Check if a file was uploaded
|
||||||
if 'file' not in request.files:
|
if 'file' not in request.files:
|
||||||
return redirect('uploadcsv')
|
flash("No file uploaded.", "error")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
file = request.files['file']
|
file = request.files['file']
|
||||||
|
|
||||||
# Check if the file is a CSV
|
# Check if the file is a CSV
|
||||||
if file.filename == '' or not file.filename.endswith('.csv'):
|
if file.filename == '' or not file.filename.endswith('.csv'):
|
||||||
return redirect('uploadcsv')
|
flash("Please upload a valid CSV file.", "error")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Extract data from the CSV file
|
# Extract and validate CSV data
|
||||||
csvdata = get_csv_data(file)
|
csvdata = get_csv_data(file)
|
||||||
|
|
||||||
# Identify the primary attribute
|
# Validate each row of data
|
||||||
primary_attrib = next(
|
errors = []
|
||||||
(attrib for attrib, config in item_attributes.items() if config.primary),
|
for i, row in enumerate(csvdata, start=1):
|
||||||
None
|
error = validate_values(row)
|
||||||
)
|
if error:
|
||||||
|
errors.append(f"Row {i}: {error}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
# Pass errors as a list to the template
|
||||||
|
return render_template(
|
||||||
|
'upload.html',
|
||||||
|
errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
# Identify the primary attribute
|
||||||
|
primary_attrib = next((attrib for attrib in item_attributes if attrib.primary), None)
|
||||||
if not primary_attrib:
|
if not primary_attrib:
|
||||||
return "Primary attribute not defined in configuration."
|
flash("Primary attribute not defined in configuration.", "error")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
# Separate new and existing assets
|
# Separate new and existing assets
|
||||||
new_assets = []
|
new_assets = []
|
||||||
existing_assets = []
|
existing_assets = []
|
||||||
for row in csvdata:
|
for row in csvdata:
|
||||||
primary_value = row[primary_attrib]
|
primary_value = row[primary_attrib.attrib_name]
|
||||||
asset_exists = Asset.query.filter_by(**{primary_attrib: primary_value}).first()
|
asset_exists = Asset.query.filter_by(**{primary_attrib.attrib_name: primary_value}).first()
|
||||||
if asset_exists:
|
if asset_exists:
|
||||||
existing_assets.append(row)
|
existing_assets.append(row)
|
||||||
else:
|
else:
|
||||||
@ -48,12 +59,17 @@ def upload_file():
|
|||||||
session['assets'] = new_assets # Store new assets in session
|
session['assets'] = new_assets # Store new assets in session
|
||||||
|
|
||||||
# Redirect to preview page with the CSV data
|
# Redirect to preview page with the CSV data
|
||||||
return render_template('csv_preview.html', new_assets=new_assets, existing=existing_assets, item_attributes=item_attributes)
|
return render_template(
|
||||||
|
'csv_preview.html',
|
||||||
|
new_assets=new_assets,
|
||||||
|
existing=existing_assets,
|
||||||
|
item_attributes=item_attributes
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle errors during file processing
|
# Handle errors during file processing
|
||||||
print(f"Error processing CSV file: {str(e)}")
|
flash(f"Error processing CSV file: {str(e)}", "error")
|
||||||
return redirect('uploadcsv')
|
return redirect(request.url)
|
||||||
|
|
||||||
# Render the upload page for GET requests
|
# Render the upload page for GET requests
|
||||||
return render_template('upload.html')
|
return render_template('upload.html')
|
@ -6,9 +6,18 @@ viewall_bp = Blueprint('viewall', __name__)
|
|||||||
|
|
||||||
@viewall_bp.route('/viewall/', methods=['GET'])
|
@viewall_bp.route('/viewall/', methods=['GET'])
|
||||||
def view_list():
|
def view_list():
|
||||||
|
# Fetch all items from the database
|
||||||
items = Asset.query.all()
|
items = Asset.query.all()
|
||||||
primary_attrib = next(
|
|
||||||
(attrib for attrib, config in item_attributes.items() if config.primary),
|
# Identify the primary attribute
|
||||||
None
|
primary_attrib = next((attrib for attrib in item_attributes if attrib.primary), None)
|
||||||
|
if not primary_attrib:
|
||||||
|
return "Primary attribute not defined in configuration."
|
||||||
|
|
||||||
|
# Render the template with items, attributes, and primary attribute
|
||||||
|
return render_template(
|
||||||
|
'viewList.html',
|
||||||
|
items=items,
|
||||||
|
item_attributes=item_attributes,
|
||||||
|
primary_attrib=primary_attrib.attrib_name
|
||||||
)
|
)
|
||||||
return render_template('viewList.html', items=items, item_attributes=item_attributes, primary_attrib=primary_attrib)
|
|
@ -1,8 +1,10 @@
|
|||||||
function collectEditedData(event) {
|
function collectEditedData(event) {
|
||||||
|
// Extract headers (attribute names) from the table
|
||||||
const headers = [...document.querySelectorAll('.table-new-assets thead th')].map(th => th.dataset.attrib);
|
const headers = [...document.querySelectorAll('.table-new-assets thead th')].map(th => th.dataset.attrib);
|
||||||
const rows = document.querySelectorAll('.table-new-assets tbody tr');
|
const rows = document.querySelectorAll('.table-new-assets tbody tr');
|
||||||
const assets = [];
|
const assets = [];
|
||||||
|
|
||||||
|
// Iterate through rows and collect data
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
const cells = row.querySelectorAll('td');
|
const cells = row.querySelectorAll('td');
|
||||||
let asset = {};
|
let asset = {};
|
||||||
@ -10,12 +12,14 @@ function collectEditedData(event) {
|
|||||||
assets.push(asset);
|
assets.push(asset);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create a hidden input field to store the collected data
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'hidden';
|
input.type = 'hidden';
|
||||||
input.name = 'assets';
|
input.name = 'assets';
|
||||||
input.value = JSON.stringify(assets);
|
input.value = JSON.stringify(assets);
|
||||||
document.querySelector('form').appendChild(input);
|
document.querySelector('form').appendChild(input);
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
event.target.submit();
|
event.target.submit();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
@ -7,47 +7,44 @@
|
|||||||
<body>
|
<body>
|
||||||
<h2 align="center">Add new Item</h2>
|
<h2 align="center">Add new Item</h2>
|
||||||
|
|
||||||
<form method = "POST">
|
<!-- Display error message if any -->
|
||||||
{% for attrib, properties in item_attributes.items() -%}
|
{% if error %}
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Form for adding a new item -->
|
||||||
|
<form method="POST">
|
||||||
|
{% for attrib in item_attributes -%}
|
||||||
<p>
|
<p>
|
||||||
<label for="{{ attrib }}">{{ properties.display_name }}:</label>
|
<label for="{{ attrib.attrib_name }}">{{ attrib.display_name }}:</label>
|
||||||
{%- if properties.html_input_type == "select" %}
|
{% if attrib.html_input_type == "select" %}
|
||||||
<select
|
<!-- Render a dropdown for select attributes -->
|
||||||
id="{{ attrib }}"
|
<select
|
||||||
name="{{ attrib }}"
|
id="{{ attrib.attrib_name }}"
|
||||||
{%- if properties.required %} required {% endif -%}
|
name="{{ attrib.attrib_name }}"
|
||||||
>
|
{% if attrib.required %} required {% endif %}
|
||||||
{% for option in properties.options -%}
|
>
|
||||||
<option value="{{ option }}">{{ option }}</option>
|
{% for option in attrib.options -%}
|
||||||
{% endfor -%}
|
<option value="{{ option }}">{{ option }}</option>
|
||||||
</select>
|
{% endfor %}
|
||||||
|
</select>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<!-- Render an input field for other attributes -->
|
||||||
<input
|
<input
|
||||||
id="{{ attrib }}"
|
id="{{ attrib.attrib_name }}"
|
||||||
type="{{ properties.html_input_type }}"
|
type="{{ attrib.html_input_type }}"
|
||||||
name="{{ attrib }}"
|
name="{{ attrib.attrib_name }}"
|
||||||
{%- if properties.required %} required {% endif -%}
|
{% if attrib.required %} required {% endif %}
|
||||||
{%- if properties.min is not none %} min="{{ properties.min }}" {% endif -%}
|
{% if attrib.html_input_type == "number" %}
|
||||||
{%- if properties.max is not none %} max="{{ properties.max }}" {% endif -%}
|
{% if attrib.min_val is not none %} min="{{ attrib.min_val }}" {% endif %}
|
||||||
{%- if properties.default_val %} value="{{ properties.default_val }}" {% endif -%}
|
{% if attrib.max_val is not none %} max="{{ attrib.max_val }}" {% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if attrib.default_val is not none %} value="{{ attrib.default_val }}" {% endif %}
|
||||||
/>
|
/>
|
||||||
{% endif -%}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<p><input type = "submit" value = "Submit" /></p>
|
<p><input type="submit" value="Submit" /></p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
{%- if exc == 'integrity' -%}
|
|
||||||
Item with the same assettag already exists
|
|
||||||
{%- endif -%}
|
|
||||||
{%- if exc == 'status' -%}
|
|
||||||
Data input error. Invalid status value
|
|
||||||
{%- endif -%}
|
|
||||||
{%- if exc == 'staffnum' -%}
|
|
||||||
Data input error. Staff number must be an integer
|
|
||||||
{%- endif -%}
|
|
||||||
</p>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@ -9,50 +9,35 @@
|
|||||||
<body>
|
<body>
|
||||||
<h1>CSV Preview</h1>
|
<h1>CSV Preview</h1>
|
||||||
|
|
||||||
<!-- Display CSV data in a table -->
|
<!-- Render tables for new and existing assets -->
|
||||||
{% if new_assets %}
|
{% for table_name, assets, editable in [
|
||||||
<p>New assets:</p>
|
('New Assets', new_assets, true),
|
||||||
<table border="1" class="table-new-assets">
|
('Existing Assets', existing, false)
|
||||||
<thead>
|
] %}
|
||||||
<tr>
|
{% if assets %}
|
||||||
{% for attrib, config in item_attributes.items() %}
|
<h2>{{ table_name }}</h2>
|
||||||
<th data-attrib="{{ attrib }}">{{ config.display_name }}</th>
|
<table border="1" class="table-new-assets">
|
||||||
{% endfor %}
|
<thead>
|
||||||
</tr>
|
<tr>
|
||||||
</thead>
|
{% for attrib in item_attributes %}
|
||||||
<tbody>
|
<th data-attrib="{{ attrib.attrib_name }}">{{ attrib.display_name }}</th>
|
||||||
{% for asset in new_assets %}
|
{% endfor %}
|
||||||
<tr>
|
</tr>
|
||||||
{% for attrib, config in item_attributes.items() %}
|
</thead>
|
||||||
<td contenteditable="true">{{ asset[attrib] }}</td>
|
<tbody>
|
||||||
|
{% for asset in assets %}
|
||||||
|
<tr>
|
||||||
|
{% for attrib in item_attributes %}
|
||||||
|
<td {% if editable %}contenteditable="true"{% endif %}>
|
||||||
|
{{ asset[attrib.attrib_name] }}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tbody>
|
||||||
{% endfor %}
|
</table>
|
||||||
</tbody>
|
{% endif %}
|
||||||
</table>
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if existing %}
|
|
||||||
<p>These assets are already in the database:</p>
|
|
||||||
<table border="1" class="table-existing-assets">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{% for attrib, config in item_attributes.items() %}
|
|
||||||
<th >{{ config.display_name }}</th>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for asset in existing %}
|
|
||||||
<tr>
|
|
||||||
{% for attrib, config in item_attributes.items() %}
|
|
||||||
<td>{{ asset[attrib] }}</td>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Form button to confirm and save data -->
|
<!-- Form button to confirm and save data -->
|
||||||
{% if new_assets %}
|
{% if new_assets %}
|
||||||
|
@ -7,46 +7,44 @@
|
|||||||
<body>
|
<body>
|
||||||
<h2 align="center">Update Item</h2>
|
<h2 align="center">Update Item</h2>
|
||||||
|
|
||||||
<form action='' method = "POST">
|
<!-- Display error message if any -->
|
||||||
{% for attrib, properties in item_attributes.items() -%}
|
{% if error %}
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Form for updating an item -->
|
||||||
|
<form method="POST">
|
||||||
|
{% for attrib in item_attributes -%}
|
||||||
<p>
|
<p>
|
||||||
<label for="{{ attrib }}">{{ properties.display_name }}:</label>
|
<label for="{{ attrib.attrib_name }}">{{ attrib.display_name }}:</label>
|
||||||
{%- if properties.html_input_type == "select" %}
|
{% if attrib.html_input_type == "select" %}
|
||||||
<select
|
<!-- Render a dropdown for select attributes -->
|
||||||
id="{{ attrib }}"
|
<select
|
||||||
name="{{ attrib }}"
|
id="{{ attrib.attrib_name }}"
|
||||||
{%- if properties.required %} required {% endif -%}
|
name="{{ attrib.attrib_name }}"
|
||||||
>
|
{% if attrib.required %} required {% endif %}
|
||||||
{% for option in properties.options -%}
|
>
|
||||||
<option value="{{ option }}" {% if item[attrib] == option %}selected{% endif %}>{{ option }}</option>
|
{% for option in attrib.options -%}
|
||||||
{% endfor -%}
|
<option value="{{ option }}" {% if item[attrib.attrib_name] == option %}selected{% endif %}>{{ option }}</option>
|
||||||
</select>
|
{% endfor %}
|
||||||
|
</select>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<!-- Render an input field for other attributes -->
|
||||||
<input
|
<input
|
||||||
id="{{ attrib }}"
|
id="{{ attrib.attrib_name }}"
|
||||||
type="{{ properties.html_input_type }}"
|
type="{{ attrib.html_input_type }}"
|
||||||
name="{{ attrib }}"
|
name="{{ attrib.attrib_name }}"
|
||||||
{%- if properties.required %} required {% endif -%}
|
{% if attrib.required %} required {% endif %}
|
||||||
{%- if properties.min is not none %} min="{{ properties.min }}" {% endif -%}
|
{% if attrib.html_input_type == "number" %}
|
||||||
{%- if properties.max is not none %} max="{{ properties.max }}" {% endif -%}
|
{% if attrib.min_val is not none %} min="{{ attrib.min_val }}" {% endif %}
|
||||||
{%- if item[attrib] %} value="{{ item[attrib] }}" {% endif -%}
|
{% if attrib.max_val is not none %} max="{{ attrib.max_val }}" {% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if item[attrib.attrib_name] %} value="{{ item[attrib.attrib_name] }}" {% endif %}
|
||||||
/>
|
/>
|
||||||
{% endif -%}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<p><input type = "submit" value = "Update" /></p>
|
<p><input type="submit" value="Update" /></p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
{%- if exc == 'integrity' -%}
|
|
||||||
Item with the same assettag already exists
|
|
||||||
{%- endif -%}
|
|
||||||
{%- if exc == 'status' -%}
|
|
||||||
Data input error. Invalid status value
|
|
||||||
{%- endif -%}
|
|
||||||
{%- if exc == 'staffnum' -%}
|
|
||||||
Data input error. Staff number must be an integer
|
|
||||||
{%- endif -%}
|
|
||||||
</p>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -7,15 +7,36 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Upload CSV File</h1>
|
<h1>Upload CSV File</h1>
|
||||||
|
|
||||||
|
<!-- Display flash messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<p style="color: red;">{{ message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<!-- Display validation errors if any -->
|
||||||
|
{% if errors %}
|
||||||
|
<details style="margin-bottom: 20px;">
|
||||||
|
<summary style="color: red; font-weight: bold;">
|
||||||
|
Errors found in the CSV file (click to expand):
|
||||||
|
</summary>
|
||||||
|
<ul>
|
||||||
|
{% for error in errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Upload form -->
|
||||||
<form action="/uploadcsv" method="POST" enctype="multipart/form-data">
|
<form action="/uploadcsv" method="POST" enctype="multipart/form-data">
|
||||||
<label for="file">Choose a CSV file:</label>
|
<label for="file">Choose a CSV file:</label>
|
||||||
<input type="file" id="file" name="file" accept=".csv" required>
|
<input type="file" id="file" name="file" accept=".csv" required>
|
||||||
<br><br>
|
<br><br>
|
||||||
<button type="submit">Upload</button>
|
<button type="submit">Upload</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if error %}
|
|
||||||
<p style="color: red;">{{ error }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -10,8 +10,8 @@
|
|||||||
<table border="1" align="center">
|
<table border="1" align="center">
|
||||||
<tr>
|
<tr>
|
||||||
<!-- Table headers -->
|
<!-- Table headers -->
|
||||||
{% for attrib, config in item_attributes.items() %}
|
{% for attrib in item_attributes %}
|
||||||
<th>{{ config.display_name }}</th>
|
<th>{{ attrib.display_name }}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<th colspan="2">Actions</th>
|
<th colspan="2">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -19,8 +19,8 @@
|
|||||||
<!-- Table rows -->
|
<!-- Table rows -->
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<tr>
|
<tr>
|
||||||
{% for attrib, config in item_attributes.items() %}
|
{% for attrib in item_attributes %}
|
||||||
<td>{{ item[attrib] }}</td>
|
<td>{{ item[attrib.attrib_name] }}</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<td>
|
<td>
|
||||||
<form action="/update/{{ item[primary_attrib] }}" method="get">
|
<form action="/update/{{ item[primary_attrib] }}" method="get">
|
||||||
|
Loading…
Reference in New Issue
Block a user