Compare commits

..

No commits in common. "main" and "recover-commit" have entirely different histories.

19 changed files with 278 additions and 497 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, sql_conf from config import item_attributes
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'] = f'mysql://{sql_conf.SQL_USER}:{sql_conf.SQL_PASSWORD}@{sql_conf.SQL_HOST}/{sql_conf.SQL_DB}' app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://assetadmin:1234@localhost/asset_test_db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app) db.init_app(app)

View File

@ -1,48 +1,40 @@
from definitions.attribute import textAttribute, intAttribute, dateAttribute, selectAttribute from definitions.attribute import Attribute
# MySQL information item_attributes = {
class sql_conf: "assettag": Attribute(
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
), ),
textAttribute( "hostname": Attribute(
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
), ),
dateAttribute( "warrantyfrom": Attribute(
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
), ),
selectAttribute( "status": Attribute(
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"
), ),
intAttribute( "staffnum": Attribute(
attrib_name="staffnum",
display_name="Staff No.", display_name="Staff No.",
html_input_type="number",
required=True, required=True,
min_val=100000, # 6 digits min=100000, # 6 digits
max_val=99999999, # 8 digits max=99999999, # 8 digits
) )
] }

View File

@ -1,205 +1,16 @@
from datetime import datetime
import re
from typing import List, Optional, Tuple
class Attribute: class Attribute:
"""Base class for all attribute types.""" 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):
def __init__( self.display_name = display_name # Input label or table column header.
self, self.html_input_type = html_input_type # HTML form input type. Determines MySQL data type.
attrib_name: str, self.required = required # HTML form input "required" attribute and MySQL "Not Null" constraint
display_name: str, self.unique = unique # MySQL "unique" constraint
html_input_type: str, self.primary = primary # MySQL "primary key" constraint
required: bool = False, self.regex = regex # Regex for value validation
unique: bool = False, self.min = min # HTML form input "min" attribute. Sets minimum value.
primary: bool = False, self.max = max # HTML form input "max" attribute. Sets maximum value.
default_val: Optional[str] = None, self.options = options # List of options for "select" inputs
compareto: Optional[List[Tuple[str, str]]] = None, self.default_val = default_val # HTML form input "value" attribute. Sets default value.
): self.auto_increment = auto_increment # bool: MySQL autoincrement
self.attrib_name = attrib_name self.index = index # bool: MySQL index
self.display_name = display_name self.comment = comment # Description text
self.html_input_type = html_input_type self.compareto = compareto # Compare to another attribute of the item for validation: ["comparison", "referenceattrib"]
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,43 +1,39 @@
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__': sql_conf.SQL_TABLE, # Table name from config '__tablename__': 'asset_test', # Table name
'__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.attrib_name for attrib in item_attributes if attrib.primary))}>" # Representation '__repr__': lambda self: f"<Asset {getattr(self, next(attrib for attrib, config in item_attributes.items() if config.primary))}>" # Representation
} }
# Define columns based on item_attributes for attrib, config in item_attributes.items():
for attrib in item_attributes: # Determine the column type based on the configuration
# Determine the column type if config.html_input_type == "text":
if isinstance(attrib, textAttribute):
column_type = String(50) column_type = String(50)
elif isinstance(attrib, dateAttribute): elif config.html_input_type == "date":
column_type = Date column_type = Date
elif isinstance(attrib, selectAttribute): elif config.html_input_type == "select":
column_type = Enum(*attrib.options) column_type = Enum(*config.options)
elif isinstance(attrib, (intAttribute, floatAttribute)): elif config.html_input_type == "number":
column_type = Integer if isinstance(attrib, intAttribute) else Float column_type = Integer
else:
raise ValueError(f"Unsupported attribute type: {type(attrib)}")
# Define the column with additional properties # Define the column with additional properties
attrs[attrib.attrib_name] = Column( attrs[attrib] = Column(
column_type, column_type,
primary_key=attrib.primary, primary_key=config.primary,
unique=attrib.unique, unique=config.unique,
nullable=not attrib.required, nullable=not config.required,
default=attrib.default_val default=config.default_val,
comment=config.comment
) )
# Create the Asset class dynamically # Create the Asset class dynamically

View File

@ -7,22 +7,16 @@ 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 = {attrib.display_name for attrib in item_attributes} # Use display names required_headers = set(item_attributes.keys()) # Get required headers as per configuration
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(csv_headers): if not required_headers.issubset(reader.fieldnames):
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 [ return [{attrib: row[attrib] for attrib in item_attributes} for row in reader]
{display_to_attrib[header]: row[header] for header in required_headers}
for row in reader
]

View File

@ -1,32 +1,106 @@
from typing import List import re
from definitions.attribute import Attribute from datetime import datetime
def validate_config(item_attributes: List[Attribute]) -> str: def _validate_number(attrib, attrib_name):
""" """Validate number-specific attributes."""
Validate the configuration file to ensure all attributes are properly defined. if attrib.min and not _is_int(attrib.min):
Returns "Ok" if everything is valid, otherwise returns an error message. return False
""" if attrib.max and not _is_int(attrib.max):
# Check for duplicate attribute names return False
attrib_names = [attrib.attrib_name for attrib in item_attributes] if attrib.min and attrib.max and int(attrib.max) < int(attrib.min):
if len(attrib_names) != len(set(attrib_names)): return False
return "Duplicate attribute names are not allowed." if attrib.default_val and not _is_int(attrib.default_val):
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
# Validate each attribute def _validate_date(attrib, attrib_name):
for attrib in item_attributes: """Validate date-specific attributes."""
error = attrib.validate() if attrib.min and not _is_date(attrib.min):
if error: return False
return error if attrib.max and not _is_date(attrib.max):
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
# Validate comparison (if applicable) def _validate_text(attrib, attrib_name):
"""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 not isinstance(attrib.compareto, list) or not all( if attrib.compareto[1] not in item_attributes:
isinstance(pair, tuple) and len(pair) == 2 for pair in attrib.compareto return f"Invalid reference attribute '{attrib.compareto[1]}' for comparison in attribute '{attrib_name}'."
): if attrib.compareto[0] not in ["lt", "gt", "le", "ge", "eq"]:
return f"Invalid comparison format for attribute '{attrib.attrib_name}'. Expected a list of tuples." return f"Invalid comparison operator for attribute '{attrib_name}'. Valid operators are: lt, gt, le, ge, eq."
for cmp, ref_attr in attrib.compareto: # Return "Ok" if everything is valid, otherwise return an error message.
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,83 +1,43 @@
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
def _is_int(value: str) -> bool: # Validate the input value based on the attribute's constraints
"""Check if a value is a valid integer.""" def validate_value(attrib, input_val):
try: # Check if the value is required
int(value) if attrib.required and not input_val:
return True return f"{attrib.display_name} is required."
except (ValueError, TypeError):
return False
def _is_float(value: str) -> bool: # Validate based on input type
"""Check if a value is a valid float.""" if attrib.html_input_type == "number":
try: try:
float(value) input_val = int(input_val)
return True except ValueError:
except (ValueError, TypeError): return f"{attrib.display_name} must be a valid number."
return False
def _is_date(value: str) -> bool: if attrib.min is not None and input_val < attrib.min:
"""Check if a value is a valid date in YYYY-MM-DD format.""" return f"{attrib.display_name} must be at least {attrib.min}."
try: if attrib.max is not None and input_val > attrib.max:
datetime.strptime(value, "%Y-%m-%d") return f"{attrib.display_name} must be at most {attrib.max}."
return True elif attrib.html_input_type == "date":
except (ValueError, TypeError): try:
return False input_val = datetime.strptime(input_val, "%Y-%m-%d") # Validate date format
if attrib.min is not None and input_val < datetime.strptime(attrib.min, "%Y-%m-%d"):
return f"{attrib.display_name} must be on or after {attrib.min}."
if attrib.max is not None and input_val > datetime.strptime(attrib.max, "%Y-%m-%d"):
return f"{attrib.display_name} must be on or before {attrib.max}."
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}."
def validate_values(form_data: Dict[str, str]) -> Optional[str]: # Validate comparison
""" #if attrib.compareto:
Validate the submitted form values against the item_attributes configuration. # compare attrib value
Returns an error message if invalid, otherwise None. #return ""
"""
for attrib in item_attributes:
value = form_data.get(attrib.attrib_name)
# Check required fields # If all checks pass, return "Ok"
if attrib.required and not value: return "Ok"
return f"{attrib.display_name} is required."
# Skip validation for empty optional fields
if not value:
continue
# Validate based on attribute type
if isinstance(attrib, textAttribute):
if attrib.regex and not re.match(attrib.regex, value):
return f"Invalid value for {attrib.display_name}. Must match the pattern: {attrib.regex}."
elif isinstance(attrib, intAttribute):
if not _is_int(value):
return f"{attrib.display_name} must be an integer."
value_int = int(value)
if attrib.min_val is not None and value_int < 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_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."
value_date = datetime.strptime(value, "%Y-%m-%d").date()
if attrib.min_val and value_date < datetime.strptime(attrib.min_val, "%Y-%m-%d").date():
return f"{attrib.display_name} cannot be earlier than {attrib.min_val}."
if attrib.max_val and value_date > datetime.strptime(attrib.max_val, "%Y-%m-%d").date():
return f"{attrib.display_name} cannot be later than {attrib.max_val}."
elif isinstance(attrib, selectAttribute):
if value not in attrib.options:
return f"{attrib.display_name} must be one of: {', '.join(attrib.options)}."
return None # No errors

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 from config import item_attributes # Import the configuration
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.attrib_name: asset_data[attrib.attrib_name] attrib: asset_data[attrib]
for attrib in item_attributes for attrib in item_attributes
}) })
db.session.add(asset) db.session.add(asset)

View File

@ -2,7 +2,6 @@ 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__)
@ -10,35 +9,17 @@ 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( return render_template('create.html', item_attributes=item_attributes)
'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.attrib_name: request.form[attrib.attrib_name] for attrib in item_attributes} form_data = {attrib: request.form[attrib] 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 in item_attributes if attrib.primary), (attrib for attrib, config in item_attributes.items() if config.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.attrib_name: primary_value}).first() item = Asset.query.filter_by(**{primary_attrib: primary_value}).first()
if not item: if not item:
abort(404) # Item not found abort(404) # Item not found

View File

@ -1,6 +1,5 @@
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
@ -15,13 +14,12 @@ 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 (use display names from item_attributes) # Write headers
headers = [attrib.display_name for attrib in item_attributes] writer.writerow([column.name for column in Asset.__mapper__.columns])
writer.writerow(headers)
# Write data rows # Write data rows
for record in records: for record in records:
writer.writerow([getattr(record, attrib.attrib_name) for attrib in item_attributes]) writer.writerow([getattr(record, column.name) for column in Asset.__mapper__.columns])
# Prepare the response # Prepare the response
response = Response( response = Response(

View File

@ -2,7 +2,6 @@ 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__)
@ -10,7 +9,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 in item_attributes if attrib.primary), (attrib for attrib, config in item_attributes.items() if config.primary),
None None
) )
@ -18,42 +17,32 @@ 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.attrib_name: primary_value}).first() item = Asset.query.filter_by(**{primary_attrib: primary_value}).first()
if not item: if not item:
return f"{primary_attrib.display_name} '{primary_value}' not found." return f"{item_attributes[primary_attrib].display_name} '{primary_value}' not found." # Move this to a template
if request.method == 'GET': if request.method == 'GET':
return render_template( return render_template('update.html', item=item, item_attributes=item_attributes)
'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.attrib_name: request.form[attrib.attrib_name] for attrib in item_attributes} form_data = {attrib: request.form[attrib] for attrib in item_attributes}
# Validate status (if it's an option field) # 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' in item_attributes and item_attributes['status'].html_input_type == "select":
if status_attrib and isinstance(status_attrib, selectAttribute): if form_data['status'] not in item_attributes['status'].options:
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)
staffnum_attrib = next((attrib for attrib in item_attributes if attrib.attrib_name == 'staffnum'), None) if 'staffnum' in item_attributes and item_attributes['staffnum'].html_input_type == "number":
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 in item_attributes: for attrib, value in form_data.items():
setattr(item, attrib.attrib_name, form_data[attrib.attrib_name]) setattr(item, attrib, value)
try: try:
db.session.commit() db.session.commit()

View File

@ -1,8 +1,11 @@
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 from functions.process_csv import get_csv_data # Import the CSV processing function
from config import item_attributes from config import item_attributes # Import the configuration
# 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'])
@ -24,7 +27,7 @@ def upload_file():
# Identify the primary attribute # Identify the primary attribute
primary_attrib = next( primary_attrib = next(
(attrib for attrib in item_attributes if attrib.primary), (attrib for attrib, config in item_attributes.items() if config.primary),
None None
) )
@ -35,8 +38,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.attrib_name] primary_value = row[primary_attrib]
asset_exists = Asset.query.filter_by(**{primary_attrib.attrib_name: primary_value}).first() asset_exists = Asset.query.filter_by(**{primary_attrib: primary_value}).first()
if asset_exists: if asset_exists:
existing_assets.append(row) existing_assets.append(row)
else: else:
@ -45,12 +48,7 @@ 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( return render_template('csv_preview.html', new_assets=new_assets, existing=existing_assets, item_attributes=item_attributes)
'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,18 +6,9 @@ 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(
# Identify the primary attribute (attrib for attrib, config in item_attributes.items() if config.primary),
primary_attrib = next((attrib for attrib in item_attributes if attrib.primary), None) 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,10 +1,8 @@
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 = {};
@ -12,14 +10,12 @@ 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

@ -7,34 +7,34 @@
<body> <body>
<h2 align="center">Add new Item</h2> <h2 align="center">Add new Item</h2>
<form method="POST"> <form method = "POST">
{% for attrib in item_attributes -%} {% for attrib, properties in item_attributes.items() -%}
<p> <p>
<label for="{{ attrib.attrib_name }}">{{ attrib.display_name }}:</label> <label for="{{ attrib }}">{{ properties.display_name }}:</label>
{%- if isinstance(attrib, selectAttribute) %} {%- if properties.html_input_type == "select" %}
<select <select
id="{{ attrib.attrib_name }}" id="{{ attrib }}"
name="{{ attrib.attrib_name }}" name="{{ attrib }}"
{%- if attrib.required %} required {% endif -%} {%- if properties.required %} required {% endif -%}
> >
{% for option in attrib.options -%} {% for option in properties.options -%}
<option value="{{ option }}">{{ option }}</option> <option value="{{ option }}">{{ option }}</option>
{% endfor -%} {% endfor -%}
</select> </select>
{% else %} {% else %}
<input <input
id="{{ attrib.attrib_name }}" id="{{ attrib }}"
type="{{ attrib.html_input_type }}" type="{{ properties.html_input_type }}"
name="{{ attrib.attrib_name }}" name="{{ attrib }}"
{%- if attrib.required %} required {% endif -%} {%- if properties.required %} required {% endif -%}
{%- if hasattr(attrib, 'min_val') and attrib.min_val is not none %} min="{{ attrib.min_val }}" {% endif -%} {%- if properties.min is not none %} min="{{ properties.min }}" {% endif -%}
{%- if hasattr(attrib, 'max_val') and attrib.max_val is not none %} max="{{ attrib.max_val }}" {% endif -%} {%- if properties.max is not none %} max="{{ properties.max }}" {% endif -%}
{%- if attrib.default_val is not none %} value="{{ attrib.default_val }}" {% endif -%} {%- if properties.default_val %} value="{{ properties.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"> <p align="center">
@ -50,3 +50,4 @@
</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 in item_attributes %} {% for attrib, config in item_attributes.items() %}
<th data-attrib="{{ attrib.attrib_name }}">{{ attrib.display_name }}</th> <th data-attrib="{{ attrib }}">{{ config.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 in item_attributes %} {% for attrib, config in item_attributes.items() %}
<td contenteditable="true">{{ asset[attrib.attrib_name] }}</td> <td contenteditable="true">{{ asset[attrib] }}</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 in item_attributes %} {% for attrib, config in item_attributes.items() %}
<th>{{ attrib.display_name }}</th> <th >{{ config.display_name }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for asset in existing %} {% for asset in existing %}
<tr> <tr>
{% for attrib in item_attributes %} {% for attrib, config in item_attributes.items() %}
<td>{{ asset[attrib.attrib_name] }}</td> <td>{{ asset[attrib] }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}

View File

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

View File

@ -10,8 +10,8 @@
<table border="1" align="center"> <table border="1" align="center">
<tr> <tr>
<!-- Table headers --> <!-- Table headers -->
{% for attrib in item_attributes %} {% for attrib, config in item_attributes.items() %}
<th>{{ attrib.display_name }}</th> <th>{{ config.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 in item_attributes %} {% for attrib, config in item_attributes.items() %}
<td>{{ item[attrib.attrib_name] }}</td> <td>{{ item[attrib] }}</td>
{% endfor %} {% endfor %}
<td> <td>
<form action="/update/{{ item[primary_attrib] }}" method="get"> <form action="/update/{{ item[primary_attrib] }}" method="get">