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
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)
if config_status != "Ok":
@ -23,7 +23,7 @@ from routes.confirm_save import confirm_save_bp
app = Flask(__name__)
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
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 = {
"assettag": Attribute(
# MySQL information
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",
html_input_type="text",
required=True,
unique=True,
primary=True,
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",
html_input_type="text",
required=True,
unique=True,
regex=r"^[a-z0-9._-]+$" # Lowercase letters, numbers, dots, underscores, hyphens
),
"warrantyfrom": Attribute(
dateAttribute(
attrib_name="warrantyfrom",
display_name="Warranty From",
html_input_type="date",
default_val="2020-03-09",
required=True
),
"status": Attribute(
selectAttribute(
attrib_name="status",
display_name="Status",
html_input_type="select",
required=True,
options=["Active", "Inactive"], # Allowed values
default_val="Active"
),
"staffnum": Attribute(
intAttribute(
attrib_name="staffnum",
display_name="Staff No.",
html_input_type="number",
required=True,
min=100000, # 6 digits
max=99999999, # 8 digits
min_val=100000, # 6 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:
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):
self.display_name = display_name # Input label or table column header.
self.html_input_type = html_input_type # HTML form input type. Determines MySQL data type.
self.required = required # HTML form input "required" attribute and MySQL "Not Null" constraint
self.unique = unique # MySQL "unique" constraint
self.primary = primary # MySQL "primary key" constraint
self.regex = regex # Regex for value validation
self.min = min # HTML form input "min" attribute. Sets minimum value.
self.max = max # HTML form input "max" attribute. Sets maximum value.
self.options = options # List of options for "select" inputs
self.default_val = default_val # HTML form input "value" attribute. Sets default value.
self.auto_increment = auto_increment # bool: MySQL autoincrement
self.index = index # bool: MySQL index
self.comment = comment # Description text
self.compareto = compareto # Compare to another attribute of the item for validation: ["comparison", "referenceattrib"]
"""Base class for all attribute types."""
def __init__(
self,
attrib_name: str,
display_name: str,
html_input_type: str,
required: bool = False,
unique: bool = False,
primary: bool = False,
default_val: Optional[str] = None,
compareto: Optional[List[Tuple[str, str]]] = None,
):
self.attrib_name = attrib_name
self.display_name = display_name
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 config import item_attributes # Import the configuration
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()
# Dynamically create the Asset model
def create_asset_model():
"""
Dynamically creates the Asset model based on the configuration in item_attributes.
"""
# Define the table name and basic methods
attrs = {
'__tablename__': 'asset_test', # Table name
'__tablename__': sql_conf.SQL_TABLE, # Table name from config
'__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():
# Determine the column type based on the configuration
if config.html_input_type == "text":
# Define columns based on item_attributes
for attrib in item_attributes:
# Determine the column type
if isinstance(attrib, textAttribute):
column_type = String(50)
elif config.html_input_type == "date":
elif isinstance(attrib, dateAttribute):
column_type = Date
elif config.html_input_type == "select":
column_type = Enum(*config.options)
elif config.html_input_type == "number":
column_type = Integer
elif isinstance(attrib, selectAttribute):
column_type = Enum(*attrib.options)
elif isinstance(attrib, (intAttribute, floatAttribute)):
column_type = Integer if isinstance(attrib, intAttribute) else Float
else:
raise ValueError(f"Unsupported attribute type: {type(attrib)}")
# Define the column with additional properties
attrs[attrib] = Column(
attrs[attrib.attrib_name] = Column(
column_type,
primary_key=config.primary,
unique=config.unique,
nullable=not config.required,
default=config.default_val,
comment=config.comment
primary_key=attrib.primary,
unique=attrib.unique,
nullable=not attrib.required,
default=attrib.default_val
)
# Create the Asset class dynamically

View File

@ -7,16 +7,22 @@ def get_csv_data(file):
reader = csv.DictReader(csv_file, delimiter='|')
# Validate CSV headers based on config
required_headers = set(item_attributes.keys()) # Get required headers as per configuration
csv_headers = set(reader.fieldnames) # Get headers present in the csv file
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
# 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)}.")
# Check for invalid headers
if extra_headers := csv_headers - required_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
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 datetime import datetime
from typing import List
from definitions.attribute import Attribute
def _validate_number(attrib, attrib_name):
"""Validate number-specific attributes."""
if attrib.min and not _is_int(attrib.min):
return False
if attrib.max and not _is_int(attrib.max):
return False
if attrib.min and attrib.max and int(attrib.max) < int(attrib.min):
return False
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
def validate_config(item_attributes: List[Attribute]) -> str:
"""
Validate the configuration file to ensure all attributes are properly defined.
Returns "Ok" if everything is valid, otherwise returns an error message.
"""
# Check for duplicate attribute names
attrib_names = [attrib.attrib_name for attrib in item_attributes]
if len(attrib_names) != len(set(attrib_names)):
return "Duplicate attribute names are not allowed."
def _validate_date(attrib, attrib_name):
"""Validate date-specific attributes."""
if attrib.min and not _is_date(attrib.min):
return False
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 each attribute
for attrib in item_attributes:
error = attrib.validate()
if error:
return error
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
# Validate comparison (if applicable)
if attrib.compareto:
if attrib.compareto[1] not in item_attributes:
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 operator for attribute '{attrib_name}'. Valid operators are: lt, gt, le, ge, eq."
# Return "Ok" if everything is valid, otherwise return an error message.
if not isinstance(attrib.compareto, list) or not all(
isinstance(pair, tuple) and len(pair) == 2 for pair in attrib.compareto
):
return f"Invalid comparison format for attribute '{attrib.attrib_name}'. Expected a list of tuples."
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"

View File

@ -1,43 +1,83 @@
import re
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 validate_value(attrib, input_val):
# Check if the value is required
if attrib.required and not input_val:
return f"{attrib.display_name} is required."
def _is_int(value: str) -> bool:
"""Check if a value is a valid integer."""
try:
int(value)
return True
except (ValueError, TypeError):
return False
# Validate based on input type
if attrib.html_input_type == "number":
try:
input_val = int(input_val)
except ValueError:
return f"{attrib.display_name} must be a valid number."
def _is_float(value: str) -> bool:
"""Check if a value is a valid float."""
try:
float(value)
return True
except (ValueError, TypeError):
return False
if attrib.min is not None and input_val < attrib.min:
return f"{attrib.display_name} must be at least {attrib.min}."
if attrib.max is not None and input_val > attrib.max:
return f"{attrib.display_name} must be at most {attrib.max}."
elif attrib.html_input_type == "date":
try:
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 _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
# Validate comparison
#if attrib.compareto:
# compare attrib value
#return ""
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)
# If all checks pass, return "Ok"
return "Ok"
# Check required fields
if attrib.required and not value:
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 definitions.models import Asset, db
import json
from config import item_attributes # Import the configuration
from config import item_attributes
confirm_save_bp = Blueprint('confirm_save', __name__)
@ -20,7 +20,7 @@ def confirm_save():
for asset_data in edited_assets:
# Dynamically create the Asset object using item_attributes
asset = Asset(**{
attrib: asset_data[attrib]
attrib.attrib_name: asset_data[attrib.attrib_name]
for attrib in item_attributes
})
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 config import item_attributes
from sqlalchemy import exc # Import exc for database exceptions
from definitions.attribute import selectAttribute, intAttribute # Import attribute classes
addasset_bp = Blueprint('addasset', __name__)
@ -9,17 +10,35 @@ addasset_bp = Blueprint('addasset', __name__)
def create():
if request.method == 'GET':
# 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
if request.method == 'POST':
# Get data from form
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)
form_data = {attrib.attrib_name: request.form[attrib.attrib_name] for attrib in item_attributes}
# 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)
try:

View File

@ -8,7 +8,7 @@ delete_bp = Blueprint('deleteasset', __name__)
def delete(primary_value):
# Identify the primary attribute
primary_attrib = next(
(attrib for attrib, config in item_attributes.items() if config.primary),
(attrib for attrib in item_attributes if attrib.primary),
None
)
@ -16,7 +16,7 @@ def delete(primary_value):
return "Primary attribute not defined in configuration."
# 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:
abort(404) # Item not found

View File

@ -1,5 +1,6 @@
from flask import Blueprint, Response
from definitions.models import Asset
from config import item_attributes
import csv
import io
@ -14,12 +15,13 @@ def export_csv():
# Fetch all records from the database
records = Asset.query.all()
# Write headers
writer.writerow([column.name for column in Asset.__mapper__.columns])
# Write headers (use display names from item_attributes)
headers = [attrib.display_name for attrib in item_attributes]
writer.writerow(headers)
# Write data rows
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
response = Response(

View File

@ -2,6 +2,7 @@ from flask import Blueprint, request, render_template, redirect
from definitions.models import Asset, db
from config import item_attributes
from sqlalchemy import exc # Import exc for database exceptions
from definitions.attribute import selectAttribute, intAttribute # Import attribute classes
update_bp = Blueprint('editasset', __name__)
@ -9,7 +10,7 @@ update_bp = Blueprint('editasset', __name__)
def update(primary_value):
# Identify the primary attribute
primary_attrib = next(
(attrib for attrib, config in item_attributes.items() if config.primary),
(attrib for attrib in item_attributes if attrib.primary),
None
)
@ -17,32 +18,42 @@ def update(primary_value):
return "Primary attribute not defined in configuration."
# 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:
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':
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':
# 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)
if 'status' in item_attributes and item_attributes['status'].html_input_type == "select":
if form_data['status'] not in item_attributes['status'].options:
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('update.html', item=item, exc='status', item_attributes=item_attributes)
# 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:
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
for attrib, value in form_data.items():
setattr(item, attrib, value)
for attrib in item_attributes:
setattr(item, attrib.attrib_name, form_data[attrib.attrib_name])
try:
db.session.commit()

View File

@ -1,11 +1,8 @@
from flask import Blueprint, request, render_template, redirect, session
import csv
from io import TextIOWrapper
from definitions.models import Asset, db
from functions.process_csv import get_csv_data # Import the CSV processing function
from config import item_attributes # Import the configuration
from functions.process_csv import get_csv_data
from config import item_attributes
# Create a Blueprint for upload
upload_bp = Blueprint('uploadcsv', __name__)
@upload_bp.route('/uploadcsv', methods=['GET', 'POST'])
@ -27,7 +24,7 @@ def upload_file():
# Identify the primary attribute
primary_attrib = next(
(attrib for attrib, config in item_attributes.items() if config.primary),
(attrib for attrib in item_attributes if attrib.primary),
None
)
@ -38,8 +35,8 @@ def upload_file():
new_assets = []
existing_assets = []
for row in csvdata:
primary_value = row[primary_attrib]
asset_exists = Asset.query.filter_by(**{primary_attrib: primary_value}).first()
primary_value = row[primary_attrib.attrib_name]
asset_exists = Asset.query.filter_by(**{primary_attrib.attrib_name: primary_value}).first()
if asset_exists:
existing_assets.append(row)
else:
@ -48,7 +45,12 @@ def upload_file():
session['assets'] = new_assets # Store new assets in session
# 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:
# Handle errors during file processing

View File

@ -6,9 +6,18 @@ viewall_bp = Blueprint('viewall', __name__)
@viewall_bp.route('/viewall/', methods=['GET'])
def view_list():
# Fetch all items from the database
items = Asset.query.all()
primary_attrib = next(
(attrib for attrib, config in item_attributes.items() if config.primary),
None
# Identify the primary attribute
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) {
// Extract headers (attribute names) from the table
const headers = [...document.querySelectorAll('.table-new-assets thead th')].map(th => th.dataset.attrib);
const rows = document.querySelectorAll('.table-new-assets tbody tr');
const assets = [];
// Iterate through rows and collect data
rows.forEach(row => {
const cells = row.querySelectorAll('td');
let asset = {};
@ -10,12 +12,14 @@ function collectEditedData(event) {
assets.push(asset);
});
// Create a hidden input field to store the collected data
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'assets';
input.value = JSON.stringify(assets);
document.querySelector('form').appendChild(input);
// Submit the form
event.target.submit();
return true;
}

View File

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

View File

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

View File

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

View File

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