diff --git a/definitions/attribute.py b/definitions/attribute.py index 62eaef0..afeb377 100644 --- a/definitions/attribute.py +++ b/definitions/attribute.py @@ -1,137 +1,205 @@ from datetime import datetime -from typing import Optional, List, Tuple +import re +from typing import List, Optional, Tuple -# Base Attribute class class Attribute: + """Base class for all attribute types.""" def __init__( self, - attrib_name: str, # Attribute name and id. - display_name: str, # Input label or table column header. - html_input_type: str = "text", # HTML form input type. Determines MySQL data type. - placeholder: str = "", # HTML form input placeholder. - required: bool = False, # HTML form input "required" attribute and MySQL "Not Null" constraint - unique: bool = False, # MySQL "unique" constraint - primary: bool = False, # MySQL "primary key" constraint - index: bool = False, # bool: MySQL index - compareto: Optional[List[Tuple[str, str]]] = None, # Compare to another attribute of the item for validation - title: str = None, # Description text, HTML "title" attribute + 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, ): - if not attrib_name: - raise ValueError("Attribute name cannot be empty.") - if not display_name: - raise ValueError(f"Display name cannot be empty for attribute '{attrib_name}'.") self.attrib_name = attrib_name self.display_name = display_name self.html_input_type = html_input_type - self.placeholder = placeholder self.required = required self.unique = unique self.primary = primary - self.index = index + self.default_val = default_val self.compareto = compareto - self.title = title - # Validate compareto - if self.compareto is not None: - validate_comparison(self) + 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 + -# Text Attribute class textAttribute(Attribute): + """Class for text attributes.""" def __init__( self, - regex: Optional[str] = None, # Regex for value validation - default_val: str = None, # Default value - compareto: Optional[List[Tuple[str, str]]] = None, - **kwargs # Additional arguments for the base class + attrib_name: str, + display_name: str, + regex: Optional[str] = None, + **kwargs ): - super().__init__(html_input_type="text", **kwargs) + super().__init__(attrib_name, display_name, html_input_type="text", **kwargs) self.regex = regex - self.default_val = default_val - self.compareto = compareto -# Integer Attribute + 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, - min_val: Optional[int] = None, # Min value - max_val: Optional[int] = None, # Max value - step_val: Optional[int] = None, # Increment step - default_val: Optional[int] = None, # Default value - auto_increment: bool = False, # bool: MySQL autoincrement - compareto: Optional[List[Tuple[str, str]]] = None, - **kwargs # Additional arguments for the base class + attrib_name: str, + display_name: str, + min_val: Optional[int] = None, + max_val: Optional[int] = None, + **kwargs ): - super().__init__(html_input_type="number", **kwargs) + super().__init__(attrib_name, display_name, html_input_type="number", **kwargs) self.min_val = min_val self.max_val = max_val - self.step_val = step_val - self.default_val = default_val - self.auto_increment = auto_increment - self.compareto = compareto - # Validate min_val and 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: - raise ValueError(f"min_val ({self.min_val}) cannot be greater than max_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 + -# Float Attribute class floatAttribute(Attribute): + """Class for float attributes.""" def __init__( self, - min_val: Optional[float] = None, # Min value - max_val: Optional[float] = None, # Max value - step_val: Optional[float] = None, # Increment step - default_val: Optional[float] = None, # Default value - auto_increment: bool = False, # bool: MySQL autoincrement - compareto: Optional[List[Tuple[str, str]]] = None, - **kwargs # Additional arguments for the base class + attrib_name: str, + display_name: str, + min_val: Optional[float] = None, + max_val: Optional[float] = None, + **kwargs ): - super().__init__(html_input_type="number", **kwargs) + super().__init__(attrib_name, display_name, html_input_type="number", **kwargs) self.min_val = min_val self.max_val = max_val - self.step_val = step_val - self.default_val = default_val - self.auto_increment = auto_increment - self.compareto = compareto - # Validate min_val and 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: - raise ValueError(f"min_val ({self.min_val}) cannot be greater than max_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 + -# Date Attribute class dateAttribute(Attribute): + """Class for date attributes.""" def __init__( self, - min_val: Optional[str] = None, # Min value (string in "YYYY-MM-DD" format) - max_val: Optional[str] = None, # Max value (string in "YYYY-MM-DD" format) - default_val: Optional[str] = None, # Default value (string in "YYYY-MM-DD" format) - compareto: Optional[List[Tuple[str, str]]] = None, - **kwargs # Additional arguments for the base class + attrib_name: str, + display_name: str, + min_val: Optional[str] = None, + max_val: Optional[str] = None, + **kwargs ): - super().__init__(html_input_type="date", **kwargs) - self.min_val = self._parse_date(min_val) if min_val else None - self.max_val = self._parse_date(max_val) if max_val else None - self.default_val = self._parse_date(default_val) if default_val else None - self.compareto = compareto + super().__init__(attrib_name, display_name, html_input_type="date", **kwargs) + self.min_val = min_val + self.max_val = max_val - def _parse_date(self, date_str: str) -> datetime: - """Parse a date string into a datetime object.""" + def _is_date(self, value: str) -> bool: + """Check if a value is a valid date in YYYY-MM-DD format.""" try: - return datetime.strptime(date_str, "%Y-%m-%d") - except ValueError as e: - raise ValueError(f"Invalid date format for '{date_str}'. Expected 'YYYY-MM-DD'.") from e + 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 + -# Select Attribute class selectAttribute(Attribute): + """Class for select attributes.""" def __init__( self, - options: Optional[List[str]] = None, # List of options - default_val: Optional[str] = None, # Default value - **kwargs # Additional arguments for the base class + attrib_name: str, + display_name: str, + options: List[str], + **kwargs ): - super().__init__(html_input_type="select", **kwargs) - self.options = options if options else [] - self.default_val = default_val if default_val else (self.options[0] if self.options else None) + super().__init__(attrib_name, display_name, html_input_type="select", **kwargs) + self.options = options - # Validate default_val - if self.default_val and self.default_val not in self.options: - raise ValueError(f"Default value '{self.default_val}' is not in the options list.") \ No newline at end of file + 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 \ No newline at end of file diff --git a/definitions/models.py b/definitions/models.py index f1f07c8..130f5f7 100644 --- a/definitions/models.py +++ b/definitions/models.py @@ -1,31 +1,35 @@ from flask_sqlalchemy import SQLAlchemy -from config import item_attributes # Import the configuration from sqlalchemy import Enum, Integer, String, Date, Column -from config import sql_conf +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__': sql_conf.SQL_TABLE, # Table name + '__tablename__': sql_conf.SQL_TABLE, # Table name from config '__init__': lambda self, **kwargs: self.__dict__.update(kwargs), # Constructor - '__repr__': lambda self: f"" # Representation + '__repr__': lambda self: f"" # Representation } + # Define columns based on item_attributes for attrib in item_attributes: - # Determine the column type based on the configuration - if attrib.html_input_type == "text": + # Determine the column type + if isinstance(attrib, textAttribute): column_type = String(50) - elif attrib.html_input_type == "date": + elif isinstance(attrib, dateAttribute): column_type = Date - elif attrib.html_input_type == "select": + elif isinstance(attrib, selectAttribute): column_type = Enum(*attrib.options) - elif attrib.html_input_type == "number": - column_type = Integer + 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.attrib_name] = Column( @@ -33,8 +37,7 @@ def create_asset_model(): primary_key=attrib.primary, unique=attrib.unique, nullable=not attrib.required, - default=attrib.default_val, - #title=attrib.title + default=attrib.default_val ) # Create the Asset class dynamically diff --git a/functions/process_csv.py b/functions/process_csv.py index 2fae422..ee96241 100644 --- a/functions/process_csv.py +++ b/functions/process_csv.py @@ -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 + ] \ No newline at end of file diff --git a/functions/validate_config.py b/functions/validate_config.py index e7e2703..3dca491 100644 --- a/functions/validate_config.py +++ b/functions/validate_config.py @@ -1,121 +1,32 @@ -from datetime import datetime -import re -from typing import List, Optional -from definitions.attribute import textAttribute, intAttribute, dateAttribute, selectAttribute +from typing import List +from definitions.attribute import Attribute -def _is_int(value) -> bool: - """Check if a value is a valid integer.""" - return isinstance(value, int) - -def _is_date(value) -> bool: - """Check if a value is a valid date in YYYY-MM-DD format.""" - if not isinstance(value, str): - return False - try: - datetime.strptime(value, "%Y-%m-%d") - return True - except ValueError: - return False - -def _validate_number(attrib: intAttribute) -> Optional[str]: - """Validate number-specific attributes. Returns an error message if invalid, otherwise None.""" - if attrib.min_val is not None and not _is_int(attrib.min_val): - return f"min_val must be an integer for attribute '{attrib.attrib_name}'." - if attrib.max_val is not None and not _is_int(attrib.max_val): - return f"max_val must be an integer for attribute '{attrib.attrib_name}'." - if attrib.min_val is not None and attrib.max_val is not None and attrib.min_val > attrib.max_val: - return f"min_val cannot be greater than max_val for attribute '{attrib.attrib_name}'." - if attrib.default_val is not None and not _is_int(attrib.default_val): - return f"default_val must be an integer for attribute '{attrib.attrib_name}'." - if attrib.default_val is not None: - if attrib.min_val is not None and attrib.default_val < attrib.min_val: - return f"default_val cannot be less than min_val for attribute '{attrib.attrib_name}'." - if attrib.max_val is not None and attrib.default_val > attrib.max_val: - return f"default_val cannot be greater than max_val for attribute '{attrib.attrib_name}'." - return None - -def _validate_date(attrib: dateAttribute) -> Optional[str]: - """Validate date-specific attributes. Returns an error message if invalid, otherwise None.""" - if attrib.min_val is not None and not _is_date(attrib.min_val): - return f"min_val must be a valid date (YYYY-MM-DD) for attribute '{attrib.attrib_name}'." - if attrib.max_val is not None and not _is_date(attrib.max_val): - return f"max_val must be a valid date (YYYY-MM-DD) for attribute '{attrib.attrib_name}'." - if attrib.min_val is not None and attrib.max_val is not None: - min_date = datetime.strptime(attrib.min_val, "%Y-%m-%d") - max_date = datetime.strptime(attrib.max_val, "%Y-%m-%d") - if max_date < min_date: - return f"max_val cannot be earlier than min_val for attribute '{attrib.attrib_name}'." - if attrib.default_val is not None and not _is_date(attrib.default_val): - return f"default_val must be a valid date (YYYY-MM-DD) for attribute '{attrib.attrib_name}'." - if attrib.default_val is not None: - default_date = datetime.strptime(attrib.default_val, "%Y-%m-%d") - if attrib.min_val is not None: - min_date = datetime.strptime(attrib.min_val, "%Y-%m-%d") - if default_date < min_date: - return f"default_val cannot be earlier than min_val for attribute '{attrib.attrib_name}'." - if attrib.max_val is not None: - max_date = datetime.strptime(attrib.max_val, "%Y-%m-%d") - if default_date > max_date: - return f"default_val cannot be later than max_val for attribute '{attrib.attrib_name}'." - return None - -def _validate_text(attrib: textAttribute) -> Optional[str]: - """Validate text-specific attributes. Returns an error message if invalid, otherwise None.""" - if attrib.default_val is not None and attrib.regex is not None: - if not re.match(attrib.regex, str(attrib.default_val)): - return f"default_val does not match the regex pattern for attribute '{attrib.attrib_name}'." - return None - -def _validate_select(attrib: selectAttribute) -> Optional[str]: - """Validate select-specific attributes. Returns an error message if invalid, otherwise None.""" - if not attrib.options: - return f"options cannot be empty for attribute '{attrib.attrib_name}'." - if attrib.default_val is not None and attrib.default_val not in attrib.options: - return f"default_val must be one of the options for attribute '{attrib.attrib_name}'." - return None - -def validate_config(item_attributes: List) -> str: +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. """ - for attrib in item_attributes: - # Validate essential properties - if not attrib.attrib_name: - return f"Missing attribute name for the {item_attributes.index(attrib) + 1}th attribute." - if not attrib.display_name: - return f"Missing display name for attribute '{attrib.attrib_name}'." - if not attrib.html_input_type: - return f"Missing input type for attribute '{attrib.attrib_name}'." - if attrib.html_input_type not in ["text", "number", "date", "select"]: - return f"Invalid input type for attribute '{attrib.attrib_name}'." + # 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." - # Validate type-specific attributes - if isinstance(attrib, intAttribute): - error = _validate_number(attrib) - if error: - return error - elif isinstance(attrib, dateAttribute): - error = _validate_date(attrib) - if error: - return error - elif isinstance(attrib, textAttribute): - error = _validate_text(attrib) - if error: - return error - elif isinstance(attrib, selectAttribute): - error = _validate_select(attrib) - if error: - return error + # Validate each attribute + for attrib in item_attributes: + error = attrib.validate() + if error: + return error # Validate comparison (if applicable) if attrib.compareto: - if not isinstance(attrib.compareto, list) or not all(isinstance(pair, tuple) and len(pair) == 2 for pair in attrib.compareto): + 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 [a.attrib_name for a in item_attributes]: + if ref_attr not in attrib_names: return f"Invalid reference attribute '{ref_attr}' for comparison in attribute '{attrib.attrib_name}'." return "Ok" \ No newline at end of file diff --git a/routes/confirm_save.py b/routes/confirm_save.py index 1b1e09c..ed3f7e8 100644 --- a/routes/confirm_save.py +++ b/routes/confirm_save.py @@ -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) diff --git a/routes/create.py b/routes/create.py index dfeadff..7c115c4 100644 --- a/routes/create.py +++ b/routes/create.py @@ -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: diff --git a/routes/delete.py b/routes/delete.py index 4b6114c..11dff6c 100644 --- a/routes/delete.py +++ b/routes/delete.py @@ -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 diff --git a/routes/export_csv.py b/routes/export_csv.py index 89ed959..5a57b6c 100644 --- a/routes/export_csv.py +++ b/routes/export_csv.py @@ -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( diff --git a/routes/update.py b/routes/update.py index f539cc9..39e0474 100644 --- a/routes/update.py +++ b/routes/update.py @@ -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() diff --git a/routes/upload.py b/routes/upload.py index 4fc6457..0598285 100644 --- a/routes/upload.py +++ b/routes/upload.py @@ -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 diff --git a/routes/viewall.py b/routes/viewall.py index 8f501aa..26abaf3 100644 --- a/routes/viewall.py +++ b/routes/viewall.py @@ -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 - ) - return render_template('viewList.html', items=items, item_attributes=item_attributes, primary_attrib=primary_attrib) \ No newline at end of file + + # 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 + ) \ No newline at end of file diff --git a/templates/create.html b/templates/create.html index e1c8e0b..764e725 100644 --- a/templates/create.html +++ b/templates/create.html @@ -7,34 +7,34 @@

Add new Item

-
- {% for attrib, properties in item_attributes.items() -%} + + {% for attrib in item_attributes -%}

- - {%- if properties.html_input_type == "select" %} + + {%- if isinstance(attrib, selectAttribute) %} {% else %} {% endif -%}

{% endfor %} -

+

@@ -49,5 +49,4 @@ {%- endif -%}

- - + \ No newline at end of file diff --git a/templates/csv_preview.html b/templates/csv_preview.html index 461d0b4..d73a2f0 100644 --- a/templates/csv_preview.html +++ b/templates/csv_preview.html @@ -15,16 +15,16 @@ - {% for attrib, config in item_attributes.items() %} - + {% for attrib in item_attributes %} + {% endfor %} {% for asset in new_assets %} - {% for attrib, config in item_attributes.items() %} - + {% for attrib in item_attributes %} + {% endfor %} {% endfor %} @@ -37,16 +37,16 @@
{{ config.display_name }}{{ attrib.display_name }}
{{ asset[attrib] }}{{ asset[attrib.attrib_name] }}
- {% for attrib, config in item_attributes.items() %} - + {% for attrib in item_attributes %} + {% endfor %} {% for asset in existing %} - {% for attrib, config in item_attributes.items() %} - + {% for attrib in item_attributes %} + {% endfor %} {% endfor %} diff --git a/templates/update.html b/templates/update.html index f8ec41c..d99b1a5 100644 --- a/templates/update.html +++ b/templates/update.html @@ -7,34 +7,34 @@

Update Item

- - {% for attrib, properties in item_attributes.items() -%} + + {% for attrib in item_attributes -%}

- - {%- if properties.html_input_type == "select" %} + + {%- if isinstance(attrib, selectAttribute) %} {% else %} {% endif -%}

{% endfor %} -

+

diff --git a/templates/viewList.html b/templates/viewList.html index f05a0b0..48ca566 100644 --- a/templates/viewList.html +++ b/templates/viewList.html @@ -10,8 +10,8 @@

{{ config.display_name }}{{ attrib.display_name }}
{{ asset[attrib] }}{{ asset[attrib.attrib_name] }}
- {% for attrib, config in item_attributes.items() %} - + {% for attrib in item_attributes %} + {% endfor %} @@ -19,8 +19,8 @@ {% for item in items %} - {% for attrib, config in item_attributes.items() %} - + {% for attrib in item_attributes %} + {% endfor %}
{{ config.display_name }}{{ attrib.display_name }}Actions
{{ item[attrib] }}{{ item[attrib.attrib_name] }}