Compare commits

...

9 Commits

19 changed files with 497 additions and 278 deletions

4
app.py
View File

@ -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)

View File

@ -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
) )
} ]

View File

@ -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

View File

@ -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

View File

@ -7,16 +7,22 @@ def get_csv_data(file):
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
]

View File

@ -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"

View File

@ -1,43 +1,83 @@
import re import re
from datetime import datetime from datetime import datetime
from typing import Dict, Optional
from config import item_attributes
from definitions.attribute import textAttribute, intAttribute, floatAttribute, dateAttribute, selectAttribute
# Validate the input value based on the attribute's constraints def _is_int(value: str) -> bool:
def validate_value(attrib, input_val): """Check if a value is a valid integer."""
# Check if the value is required try:
if attrib.required and not input_val: int(value)
return True
except (ValueError, TypeError):
return False
def _is_float(value: str) -> bool:
"""Check if a value is a valid float."""
try:
float(value)
return True
except (ValueError, TypeError):
return False
def _is_date(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, TypeError):
return False
def validate_values(form_data: Dict[str, str]) -> Optional[str]:
"""
Validate the submitted form values against the item_attributes configuration.
Returns an error message if invalid, otherwise None.
"""
for attrib in item_attributes:
value = form_data.get(attrib.attrib_name)
# Check required fields
if attrib.required and not value:
return f"{attrib.display_name} is required." return f"{attrib.display_name} is required."
# Validate based on input type # Skip validation for empty optional fields
if attrib.html_input_type == "number": if not value:
try: continue
input_val = int(input_val)
except ValueError:
return f"{attrib.display_name} must be a valid number."
if attrib.min is not None and input_val < attrib.min: # Validate based on attribute type
return f"{attrib.display_name} must be at least {attrib.min}." if isinstance(attrib, textAttribute):
if attrib.max is not None and input_val > attrib.max: if attrib.regex and not re.match(attrib.regex, value):
return f"{attrib.display_name} must be at most {attrib.max}." return f"Invalid value for {attrib.display_name}. Must match the pattern: {attrib.regex}."
elif attrib.html_input_type == "date":
try: elif isinstance(attrib, intAttribute):
input_val = datetime.strptime(input_val, "%Y-%m-%d") # Validate date format if not _is_int(value):
if attrib.min is not None and input_val < datetime.strptime(attrib.min, "%Y-%m-%d"): return f"{attrib.display_name} must be an integer."
return f"{attrib.display_name} must be on or after {attrib.min}." value_int = int(value)
if attrib.max is not None and input_val > datetime.strptime(attrib.max, "%Y-%m-%d"): if attrib.min_val is not None and value_int < attrib.min_val:
return f"{attrib.display_name} must be on or before {attrib.max}." return f"{attrib.display_name} must be greater than or equal to {attrib.min_val}."
except: if attrib.max_val is not None and value_int > attrib.max_val:
return f"{attrib.display_name} must be less than or equal to {attrib.max_val}."
elif isinstance(attrib, floatAttribute):
if not _is_float(value):
return f"{attrib.display_name} must be a number."
value_float = float(value)
if attrib.min_val is not None and value_float < attrib.min_val:
return f"{attrib.display_name} must be greater than or equal to {attrib.min_val}."
if attrib.max_val is not None and value_float > attrib.max_val:
return f"{attrib.display_name} must be less than or equal to {attrib.max_val}."
elif isinstance(attrib, dateAttribute):
if not _is_date(value):
return f"{attrib.display_name} must be a valid date in YYYY-MM-DD format." return f"{attrib.display_name} must be a valid date in YYYY-MM-DD format."
elif attrib.html_input_type == "select": value_date = datetime.strptime(value, "%Y-%m-%d").date()
if input_val not in attrib.options: if attrib.min_val and value_date < datetime.strptime(attrib.min_val, "%Y-%m-%d").date():
return f"{attrib.display_name} must be one of {attrib.options}." return f"{attrib.display_name} cannot be earlier than {attrib.min_val}."
elif attrib.html_input_type == "text": if attrib.max_val and value_date > datetime.strptime(attrib.max_val, "%Y-%m-%d").date():
if attrib.regex and not re.match(attrib.regex, input_val): return f"{attrib.display_name} cannot be later than {attrib.max_val}."
return f"{attrib.display_name} is invalid. Allowed pattern: {attrib.regex}."
# Validate comparison elif isinstance(attrib, selectAttribute):
#if attrib.compareto: if value not in attrib.options:
# compare attrib value return f"{attrib.display_name} must be one of: {', '.join(attrib.options)}."
#return ""
# If all checks pass, return "Ok" return None # No errors
return "Ok"

View File

@ -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)

View File

@ -2,6 +2,7 @@ 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 definitions.attribute import selectAttribute, intAttribute # Import attribute classes
addasset_bp = Blueprint('addasset', __name__) addasset_bp = Blueprint('addasset', __name__)
@ -9,17 +10,35 @@ addasset_bp = Blueprint('addasset', __name__)
def create(): def create():
if request.method == 'GET': if request.method == 'GET':
# Render the "add item" form # Render the "add item" form
return render_template('create.html', item_attributes=item_attributes) return render_template(
'create.html',
item_attributes=item_attributes,
isinstance=isinstance, # Pass isinstance to the template
hasattr=hasattr, # Pass hasattr to the template
selectAttribute=selectAttribute, # Pass selectAttribute to the template
intAttribute=intAttribute # Pass intAttribute to the template
)
# 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[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)
# Validate status (if it's an option field)
status_attrib = next((attrib for attrib in item_attributes if attrib.attrib_name == 'status'), None)
if status_attrib and isinstance(status_attrib, selectAttribute):
if form_data['status'] not in status_attrib.options:
return render_template('create.html', item_attributes=item_attributes, exc='status')
# Convert staffnum to int (if it's a number field)
staffnum_attrib = next((attrib for attrib in item_attributes if attrib.attrib_name == 'staffnum'), None)
if staffnum_attrib and isinstance(staffnum_attrib, intAttribute):
try:
form_data['staffnum'] = int(form_data['staffnum'])
except ValueError:
return render_template('create.html', item_attributes=item_attributes, exc='staffnum')
# Create the Asset object
item = Asset(**form_data) item = Asset(**form_data)
try: try:

View File

@ -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

View File

@ -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(

View File

@ -2,6 +2,7 @@ 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 definitions.attribute import selectAttribute, intAttribute # Import attribute classes
update_bp = Blueprint('editasset', __name__) update_bp = Blueprint('editasset', __name__)
@ -9,7 +10,7 @@ update_bp = Blueprint('editasset', __name__)
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, config in item_attributes.items() if config.primary), (attrib for attrib in item_attributes if attrib.primary),
None None
) )
@ -17,32 +18,42 @@ def update(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:
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':
return render_template('update.html', item=item, item_attributes=item_attributes) return render_template(
'update.html',
item=item,
item_attributes=item_attributes,
isinstance=isinstance, # Pass isinstance to the template
hasattr=hasattr, # Pass hasattr to the template
selectAttribute=selectAttribute, # Pass selectAttribute to the template
intAttribute=intAttribute # Pass intAttribute to the template
)
if request.method == 'POST': if request.method == 'POST':
# Dynamically get form data using item_attributes # Dynamically get form data using item_attributes
form_data = {attrib: request.form[attrib] for attrib in item_attributes} form_data = {attrib.attrib_name: request.form[attrib.attrib_name] for attrib in item_attributes}
# Validate status (if it's an option field) # Validate status (if it's an option field)
if 'status' in item_attributes and item_attributes['status'].html_input_type == "select": status_attrib = next((attrib for attrib in item_attributes if attrib.attrib_name == 'status'), None)
if form_data['status'] not in item_attributes['status'].options: if status_attrib and isinstance(status_attrib, selectAttribute):
if form_data['status'] not in status_attrib.options:
return render_template('update.html', item=item, exc='status', item_attributes=item_attributes) return render_template('update.html', item=item, exc='status', item_attributes=item_attributes)
# Convert staffnum to int (if it's a number field) # Convert staffnum to int (if it's a number field)
if 'staffnum' in item_attributes and item_attributes['staffnum'].html_input_type == "number": staffnum_attrib = next((attrib for attrib in item_attributes if attrib.attrib_name == 'staffnum'), None)
if staffnum_attrib and isinstance(staffnum_attrib, intAttribute):
try: try:
form_data['staffnum'] = int(form_data['staffnum']) form_data['staffnum'] = int(form_data['staffnum'])
except ValueError: except ValueError:
return render_template('update.html', item=item, exc='staffnum', item_attributes=item_attributes) 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()

View File

@ -1,11 +1,8 @@
from flask import Blueprint, request, render_template, redirect, session from flask import Blueprint, request, render_template, redirect, session
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 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'])
@ -27,7 +24,7 @@ def upload_file():
# 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
) )
@ -38,8 +35,8 @@ def upload_file():
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,7 +45,12 @@ 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

View File

@ -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)

View File

@ -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;
} }

View File

@ -8,28 +8,28 @@
<h2 align="center">Add new Item</h2> <h2 align="center">Add new Item</h2>
<form method="POST"> <form method="POST">
{% for attrib, properties in item_attributes.items() -%} {% 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 isinstance(attrib, selectAttribute) %}
<select <select
id="{{ attrib }}" id="{{ attrib.attrib_name }}"
name="{{ attrib }}" name="{{ attrib.attrib_name }}"
{%- if properties.required %} required {% endif -%} {%- if attrib.required %} required {% endif -%}
> >
{% for option in properties.options -%} {% for option in attrib.options -%}
<option value="{{ option }}">{{ option }}</option> <option value="{{ option }}">{{ option }}</option>
{% endfor -%} {% endfor -%}
</select> </select>
{% else %} {% else %}
<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 hasattr(attrib, 'min_val') and attrib.min_val is not none %} min="{{ attrib.min_val }}" {% endif -%}
{%- if properties.max is not none %} max="{{ properties.max }}" {% endif -%} {%- if hasattr(attrib, 'max_val') and attrib.max_val is not none %} max="{{ attrib.max_val }}" {% endif -%}
{%- if properties.default_val %} value="{{ properties.default_val }}" {% endif -%} {%- if attrib.default_val is not none %} value="{{ attrib.default_val }}" {% endif -%}
/> />
{% endif -%} {% endif -%}
</p> </p>
@ -50,4 +50,3 @@
</p> </p>
</body> </body>
</html> </html>

View File

@ -15,16 +15,16 @@
<table border="1" class="table-new-assets"> <table border="1" class="table-new-assets">
<thead> <thead>
<tr> <tr>
{% for attrib, config in item_attributes.items() %} {% for attrib in item_attributes %}
<th data-attrib="{{ attrib }}">{{ config.display_name }}</th> <th data-attrib="{{ attrib.attrib_name }}">{{ attrib.display_name }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for asset in new_assets %} {% for asset in new_assets %}
<tr> <tr>
{% for attrib, config in item_attributes.items() %} {% for attrib in item_attributes %}
<td contenteditable="true">{{ asset[attrib] }}</td> <td contenteditable="true">{{ asset[attrib.attrib_name] }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}
@ -37,16 +37,16 @@
<table border="1" class="table-existing-assets"> <table border="1" class="table-existing-assets">
<thead> <thead>
<tr> <tr>
{% for attrib, config in item_attributes.items() %} {% for attrib in item_attributes %}
<th >{{ config.display_name }}</th> <th>{{ attrib.display_name }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for asset in existing %} {% for asset in existing %}
<tr> <tr>
{% for attrib, config in item_attributes.items() %} {% for attrib in item_attributes %}
<td>{{ asset[attrib] }}</td> <td>{{ asset[attrib.attrib_name] }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -7,29 +7,29 @@
<body> <body>
<h2 align="center">Update Item</h2> <h2 align="center">Update Item</h2>
<form action='' method = "POST"> <form method="POST">
{% for attrib, properties in item_attributes.items() -%} {% 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 isinstance(attrib, selectAttribute) %}
<select <select
id="{{ attrib }}" id="{{ attrib.attrib_name }}"
name="{{ attrib }}" name="{{ attrib.attrib_name }}"
{%- if properties.required %} required {% endif -%} {%- if attrib.required %} required {% endif -%}
> >
{% for option in properties.options -%} {% for option in attrib.options -%}
<option value="{{ option }}" {% if item[attrib] == option %}selected{% endif %}>{{ option }}</option> <option value="{{ option }}" {% if item[attrib.attrib_name] == option %}selected{% endif %}>{{ option }}</option>
{% endfor -%} {% endfor -%}
</select> </select>
{% else %} {% else %}
<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 hasattr(attrib, 'min_val') and attrib.min_val is not none %} min="{{ attrib.min_val }}" {% endif -%}
{%- if properties.max is not none %} max="{{ properties.max }}" {% endif -%} {%- if hasattr(attrib, 'max_val') and attrib.max_val is not none %} max="{{ attrib.max_val }}" {% endif -%}
{%- if item[attrib] %} value="{{ item[attrib] }}" {% endif -%} {%- if item[attrib.attrib_name] %} value="{{ item[attrib.attrib_name] }}" {% endif -%}
/> />
{% endif -%} {% endif -%}
</p> </p>

View File

@ -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">