diff --git a/inventory/models/areas.py b/inventory/models/areas.py
index dbf86b0..45ad4f5 100644
--- a/inventory/models/areas.py
+++ b/inventory/models/areas.py
@@ -6,12 +6,9 @@ from sqlalchemy import Identity, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db
-from ..temp import is_temp_id
-from ..utils.validation import ValidatableMixin
-class Area(ValidatableMixin, db.Model):
+class Area(db.Model):
__tablename__ = 'area'
- VALIDATION_LABEL = "Area"
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
@@ -23,95 +20,3 @@ class Area(ValidatableMixin, db.Model):
def __repr__(self):
return f""
-
- def serialize(self):
- return {
- 'id': self.id,
- 'name': self.name
- }
-
- @classmethod
- def sync_from_state(cls, submitted_items: list[dict]) -> dict[str, int]:
- """
- Syncs the Area table (aka 'sections') with the submitted list.
- Supports add, update, and delete.
- Also returns a mapping of temp IDs to real IDs for resolution.
- """
- submitted_clean = []
- seen_ids = set()
- temp_id_map = {}
-
- for item in submitted_items:
- if not isinstance(item, dict):
- continue
- name = str(item.get("name", "")).strip()
- raw_id = item.get("id")
- if not name:
- continue
- submitted_clean.append({"id": raw_id, "name": name})
-
- # Record real (non-temp) IDs
- try:
- if raw_id is not None:
- parsed_id = int(raw_id)
- if parsed_id >= 0:
- seen_ids.add(parsed_id)
- except (ValueError, TypeError):
- pass
-
- existing_query = db.session.query(cls)
- existing_by_id = {area.id: area for area in existing_query.all()}
- existing_ids = set(existing_by_id.keys())
-
- for entry in submitted_clean:
- submitted_id_raw = entry.get("id")
- submitted_name = entry["name"]
-
- if is_temp_id(submitted_id_raw):
- new_area = cls(name=submitted_name)
- db.session.add(new_area)
- db.session.flush() # Get the real ID
- temp_id_map[submitted_id_raw] = new_area.id
- else:
- try:
- submitted_id = int(submitted_id_raw)
- except (ValueError, TypeError):
- continue # Skip malformed ID
-
- if submitted_id in existing_by_id:
- area = existing_by_id[submitted_id]
- if area.name != submitted_name:
- area.name = submitted_name
-
- for existing_id in existing_ids - seen_ids:
- area = existing_by_id[existing_id]
- db.session.delete(area)
-
- id_map = {
- **{str(i): i for i in seen_ids}, # "1" → 1
- **{str(temp): real for temp, real in temp_id_map.items()} # "temp-1" → 5
- }
- return id_map
-
- @classmethod
- def validate_state(cls, submitted_items: list[dict]) -> list[str]:
- errors = []
-
- for index, item in enumerate(submitted_items):
- if not isinstance(item, dict):
- errors.append(f"Area entry #{index + 1} is not a valid object.")
- continue
-
- name = item.get('name')
- if not name or not str(name).strip():
- errors.append(f"Area entry #{index + 1} is missing a name.")
-
- raw_id = item.get('id')
- if raw_id is not None:
- try:
- _ = int(raw_id)
- except (ValueError, TypeError):
- if not is_temp_id(raw_id):
- errors.append(f"Area entry #{index + 1} has invalid ID: {raw_id}")
-
- return errors
diff --git a/inventory/models/brands.py b/inventory/models/brands.py
index 00fd14a..d2adf17 100644
--- a/inventory/models/brands.py
+++ b/inventory/models/brands.py
@@ -1,4 +1,4 @@
-from typing import List, Optional, TYPE_CHECKING
+from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from .inventory import Inventory
@@ -6,12 +6,9 @@ from sqlalchemy import Identity, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db
-from ..temp import is_temp_id
-from ..utils.validation import ValidatableMixin
-class Brand(ValidatableMixin, db.Model):
+class Brand(db.Model):
__tablename__ = 'brand'
- VALIDATION_LABEL = 'Brand'
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
name: Mapped[str] = mapped_column(Unicode(255), nullable=False)
@@ -24,90 +21,6 @@ class Brand(ValidatableMixin, db.Model):
def __repr__(self):
return f""
- def serialize(self):
- return {
- 'id': self.id,
- 'name': self.name
- }
-
- @classmethod
- def sync_from_state(cls, submitted_items: list[dict]) -> dict[str, int]:
- submitted_clean = []
- seen_ids = set()
- temp_id_map = {}
-
- for item in submitted_items:
- if not isinstance(item, dict):
- continue
- name = str(item.get("name", "")).strip()
- raw_id = item.get("id")
- if not name:
- continue
- submitted_clean.append({"id": raw_id, "name": name})
-
- try:
- if raw_id:
- parsed_id = int(raw_id)
- if parsed_id >= 0:
- seen_ids.add(parsed_id)
- except (ValueError, TypeError):
- pass
-
- existing_by_id = {b.id: b for b in db.session.query(cls).all()}
- existing_ids = set(existing_by_id.keys())
-
- for entry in submitted_clean:
- submitted_id = entry.get("id")
- name = entry["name"]
-
- if is_temp_id(submitted_id):
- obj = cls(name=name)
- db.session.add(obj)
- db.session.flush()
- temp_id_map[submitted_id] = obj.id
- else:
- try:
- parsed_id = int(submitted_id)
- except (ValueError, TypeError):
- continue
-
- if parsed_id in existing_by_id:
- obj = existing_by_id[parsed_id]
- if obj.name != name:
- obj.name = name
-
- for id_to_remove in existing_ids - seen_ids:
- db.session.delete(existing_by_id[id_to_remove])
-
- id_map = {
- **{str(i): i for i in seen_ids}, # "1" → 1
- **{str(temp): real for temp, real in temp_id_map.items()} # "temp-1" → 5
- }
- return id_map
-
- @classmethod
- def validate_state(cls, submitted_items: list[dict]) -> list[str]:
- errors = []
-
- for index, item in enumerate(submitted_items):
- if not isinstance(item, dict):
- errors.append(f"Area entry #{index + 1} is not a valid object.")
- continue
-
- name = item.get('name')
- if not name or not str(name).strip():
- errors.append(f"Area entry #{index + 1} is missing a name.")
-
- raw_id = item.get('id')
- if raw_id is not None:
- try:
- _ = int(raw_id)
- except (ValueError, TypeError):
- if not is_temp_id(raw_id):
- errors.append(f"Area entry #{index + 1} has invalid ID: {raw_id}")
-
- return errors
-
@property
def identifier(self) -> str:
return self.name if self.name else f"ID: {self.id}"
diff --git a/inventory/models/items.py b/inventory/models/items.py
index 1641d91..75c06ef 100644
--- a/inventory/models/items.py
+++ b/inventory/models/items.py
@@ -6,12 +6,9 @@ from sqlalchemy import Identity, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db
-from ..temp import is_temp_id
-from ..utils.validation import ValidatableMixin
-class Item(ValidatableMixin, db.Model):
+class Item(db.Model):
__tablename__ = 'item'
- VALIDATION_LABEL = 'Item'
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
description: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
@@ -24,88 +21,6 @@ class Item(ValidatableMixin, db.Model):
def __repr__(self):
return f"- "
- def serialize(self):
- return {
- 'id': self.id,
- 'name': self.description
- }
-
@property
def identifier(self):
return self.description if self.description else f"Item {self.id}"
-
- @classmethod
- def sync_from_state(cls, submitted_items: list[dict]) -> dict[str, int]:
- submitted_clean = []
- seen_ids = set()
- temp_id_map = {}
-
- for item in submitted_items:
- if not isinstance(item, dict):
- continue
- name = str(item.get("name", "")).strip()
- raw_id = item.get("id")
-
- if not name:
- continue
-
- try:
- if raw_id:
- parsed_id = int(raw_id)
- if parsed_id >= 0:
- seen_ids.add(parsed_id)
- except (ValueError, TypeError):
- pass
-
- submitted_clean.append({"id": raw_id, "description": name})
-
- existing_by_id = {t.id: t for t in db.session.query(cls).all()}
- existing_ids = set(existing_by_id.keys())
-
- for entry in submitted_clean:
- submitted_id = entry["id"]
- description = entry["description"]
-
- if is_temp_id(submitted_id):
- obj = cls(description=description)
- db.session.add(obj)
- db.session.flush()
- temp_id_map[submitted_id] = obj.id
- elif isinstance(submitted_id, int) or submitted_id.isdigit():
- submitted_id_int = int(submitted_id)
- obj = existing_by_id.get(submitted_id_int)
- if obj and obj.description != description:
- obj.description = description
-
- for id_to_remove in existing_ids - seen_ids:
- obj = existing_by_id[id_to_remove]
- db.session.delete(obj)
-
- id_map = {
- **{str(i): i for i in seen_ids},
- **{str(temp): real for temp, real in temp_id_map.items()}
- }
- return id_map
-
- @classmethod
- def validate_state(cls, submitted_items: list[dict]) -> list[str]:
- errors = []
-
- for index, item in enumerate(submitted_items):
- if not isinstance(item, dict):
- errors.append(f"Area entry #{index + 1} is not a valid object.")
- continue
-
- name = item.get('name')
- if not name or not str(name).strip():
- errors.append(f"Area entry #{index + 1} is missing a name.")
-
- raw_id = item.get('id')
- if raw_id is not None:
- try:
- _ = int(raw_id)
- except (ValueError, TypeError):
- if not is_temp_id(raw_id):
- errors.append(f"Area entry #{index + 1} has invalid ID: {raw_id}")
-
- return errors
\ No newline at end of file
diff --git a/inventory/models/room_functions.py b/inventory/models/room_functions.py
index 436d3e7..72c1a1f 100644
--- a/inventory/models/room_functions.py
+++ b/inventory/models/room_functions.py
@@ -6,12 +6,9 @@ from sqlalchemy import Identity, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db
-from ..temp import is_temp_id
-from ..utils.validation import ValidatableMixin
-class RoomFunction(ValidatableMixin, db.Model):
+class RoomFunction(db.Model):
__tablename__ = 'room_function'
- VALIDATION_LABEL = "Function"
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
description: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
@@ -23,62 +20,3 @@ class RoomFunction(ValidatableMixin, db.Model):
def __repr__(self):
return f""
-
- def serialize(self):
- return {
- 'id': self.id,
- 'name': self.description
- }
-
- @classmethod
- def sync_from_state(cls, submitted_items: list[dict]) -> dict[str, int]:
- submitted_clean = []
- seen_ids = set()
- temp_id_map = {}
-
- for item in submitted_items:
- if not isinstance(item, dict):
- continue
- name = str(item.get("name", "")).strip()
- raw_id = item.get("id")
-
- if not name:
- continue
-
- try:
- if raw_id:
- parsed_id = int(raw_id)
- if parsed_id >= 0:
- seen_ids.add(parsed_id)
- except (ValueError, TypeError):
- pass
-
- submitted_clean.append({"id": raw_id, "description": name})
-
- existing_by_id = {f.id: f for f in db.session.query(cls).all()}
- existing_ids = set(existing_by_id.keys())
-
- for entry in submitted_clean:
- submitted_id = entry.get("id")
- description = entry["description"]
-
- if is_temp_id(submitted_id):
- obj = cls(description=description)
- db.session.add(obj)
- db.session.flush()
- temp_id_map[submitted_id] = obj.id
- elif isinstance(submitted_id, int) or submitted_id.isdigit():
- submitted_id_int = int(submitted_id)
- obj = existing_by_id.get(submitted_id_int)
- if obj and obj.description != description:
- obj.description = description
-
- for id_to_remove in existing_ids - seen_ids:
- obj = existing_by_id[id_to_remove]
- db.session.delete(obj)
-
- id_map = {
- **{str(i): i for i in seen_ids},
- **{str(temp): real for temp, real in temp_id_map.items()}
- }
- return id_map
diff --git a/inventory/models/rooms.py b/inventory/models/rooms.py
index 4081e87..cd9b7b2 100644
--- a/inventory/models/rooms.py
+++ b/inventory/models/rooms.py
@@ -10,11 +10,8 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship, joinedload, sele
from . import db
-from ..utils.validation import ValidatableMixin
-
-class Room(ValidatableMixin, db.Model):
+class Room(db.Model):
__tablename__ = 'rooms'
- VALIDATION_LABEL = "Room"
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
@@ -56,148 +53,6 @@ class Room(ValidatableMixin, db.Model):
func = self.room_function.description if self.room_function else ""
return f"{name} - {func}".strip(" -")
- def serialize(self):
- return {
- 'id': self.id,
- 'name': self.name,
- 'area_id': self.area_id,
- 'function_id': self.function_id
- }
-
- @classmethod
- def sync_from_state(cls, submitted_rooms: list[dict], section_map: dict[str, int], function_map: dict[str, int]) -> None:
- """
- Syncs the Rooms table with the submitted room list.
- Resolves foreign keys using section_map and function_map.
- Supports add, update, and delete.
- """
- def resolve_fk(key, fk_map, label):
- if key is None:
- return None
- key = str(key)
- if key.startswith("temp") or not key.isdigit():
- if key in fk_map:
- return fk_map[key]
- raise ValueError(f"Unable to resolve {label} ID: {key}")
- return int(key) # It's already a real ID
-
- submitted_clean = []
- seen_ids = set()
-
- for room in submitted_rooms:
- if not isinstance(room, dict):
- continue
- name = str(room.get("name", "")).strip()
- if not name:
- continue
-
- rid = room.get("id")
- area_id = room.get("area_id")
- function_id = room.get("function_id")
-
- submitted_clean.append({
- "id": rid,
- "name": name,
- "area_id": area_id,
- "function_id": function_id
- })
-
- if rid and not str(rid).startswith("room-"):
- try:
- seen_ids.add(int(rid))
- except ValueError:
- pass # Not valid? Not seen.
-
- existing_query = db.session.query(cls)
- existing_by_id = {room.id: room for room in existing_query.all()}
- existing_ids = set(existing_by_id.keys())
-
- for entry in submitted_clean:
- rid = entry.get("id")
- name = entry["name"]
-
- resolved_area_id = resolve_fk(entry.get("area_id"), section_map, "section")
- resolved_function_id = resolve_fk(entry.get("function_id"), function_map, "function")
-
- if not rid or str(rid).startswith("room-"):
- new_room = cls(name=name, area_id=resolved_area_id, function_id=resolved_function_id)
- db.session.add(new_room)
- else:
- try:
- rid_int = int(rid)
- except ValueError:
- print(f"⚠️ Invalid room ID format: {rid}")
- continue
-
- room = existing_by_id.get(rid_int)
- if not room:
- print(f"⚠️ No matching room in DB for ID: {rid_int}")
- continue
-
- if room.name != name:
- room.name = name
- if room.area_id != resolved_area_id:
- room.area_id = resolved_area_id
- if room.function_id != resolved_function_id:
- room.function_id = resolved_function_id
-
- for existing_id in existing_ids - seen_ids:
- room = existing_by_id.get(existing_id)
- if not room:
- continue
-
- # Skip if a newly added room matches this one — likely duplicate
- if any(
- r["name"] == room.name and
- resolve_fk(r["area_id"], section_map, "section") == room.area_id and
- resolve_fk(r["function_id"], function_map, "function") == room.function_id
- for r in submitted_clean
- if r.get("id") is None or str(r.get("id")).startswith("room-")
- ):
- print(f"⚠️ Skipping deletion of likely duplicate: {room}")
- continue
-
- db.session.delete(room)
-
- @classmethod
- def validate_state(cls, submitted_items: list[dict]) -> list[str]:
- errors = []
-
- for index, item in enumerate(submitted_items):
- label = f"Room #{index + 1}"
-
- if not isinstance(item, dict):
- errors.append(f"{label} is not a valid object.")
- continue
-
- name = item.get("name")
- if not name or not str(name).strip():
- errors.append(f"{label} is missing a name.")
-
- raw_id = item.get("id")
- if raw_id is not None:
- try:
- _ = int(raw_id)
- except (ValueError, TypeError):
- if not str(raw_id).startswith("room-"):
- errors.append(f"{label} has an invalid ID: {raw_id}")
-
- # These fields are FK IDs, so we're just checking for valid formats here.
- for fk_field, fk_label in [("area_id", "Section"), ("function_id", "Function")]:
- fk_val = item.get(fk_field)
-
- if fk_val is None:
- continue # Let the DB enforce nullability
-
- try:
- _ = int(fk_val)
- except (ValueError, TypeError):
- fk_val_str = str(fk_val)
- if not fk_val_str.startswith("temp-"):
- errors.append(f"{label} has invalid {fk_label} ID: {fk_val}")
-
- return errors
-
Room.ui_eagerload = (
joinedload(Room.area),
joinedload(Room.room_function),
diff --git a/inventory/routes/helpers.py b/inventory/routes/helpers.py
index d2bdb18..afb4ccb 100644
--- a/inventory/routes/helpers.py
+++ b/inventory/routes/helpers.py
@@ -112,7 +112,6 @@ def get_image_attachable_class_by_name(name: str):
return cls
return None
-
def make_csv(export_func, columns, rows):
output = io.StringIO()
writer = csv.writer(output)
diff --git a/inventory/routes/hooks.py b/inventory/routes/hooks.py
index d4e4f5c..41fbf9f 100644
--- a/inventory/routes/hooks.py
+++ b/inventory/routes/hooks.py
@@ -1,6 +1,5 @@
from bs4 import BeautifulSoup
from flask import current_app as app
-import re
from . import main
diff --git a/inventory/routes/inventory.py b/inventory/routes/inventory.py
index 56d4351..79b3f79 100644
--- a/inventory/routes/inventory.py
+++ b/inventory/routes/inventory.py
@@ -1,10 +1,5 @@
-import io
-import csv
-import base64
-
import datetime
-from flask import request, render_template, url_for, jsonify
-from sqlalchemy.inspection import inspect
+from flask import request, render_template, jsonify
from . import main
from .helpers import FILTER_MAP, inventory_headers, worklog_headers, make_csv
diff --git a/inventory/routes/settings.py b/inventory/routes/settings.py
index 00cc98c..d257da4 100644
--- a/inventory/routes/settings.py
+++ b/inventory/routes/settings.py
@@ -1,109 +1,15 @@
-import json
-import traceback
-
-from flask import request, flash, redirect, url_for, render_template, jsonify
+from flask import render_template
from . import main
from .. import db
-from ..models import Brand, Item, Area, RoomFunction, Room, Image
-from ..utils.load import eager_load_room_relationships, chunk_list
+from ..models import Image
+from ..utils.load import chunk_list
-@main.route('/settings', methods=['GET', 'POST'])
+@main.route('/settings')
def settings():
- if request.method == 'POST':
- form = request.form
-
- try:
- state = json.loads(form['formState'])
- except Exception:
- flash("Invalid form state submitted. JSON decode failed.", "danger")
- traceback.print_exc()
- return redirect(url_for('main.settings'))
-
- try:
- with db.session.begin():
- # Sync each table and grab temp ID maps
- brand_map = Brand.sync_from_state(state.get("brands", []))
- type_map = Item.sync_from_state(state.get("types", []))
- section_map = Area.sync_from_state(state.get("sections", []))
- function_map = RoomFunction.sync_from_state(state.get("functions", []))
-
- # Fix up room foreign keys based on real IDs
- submitted_rooms = []
- for room in state.get("rooms", []):
- room = dict(room) # shallow copy
- sid = room.get("area_id")
- fid = room.get("function_id")
-
- if sid is not None:
- sid_key = str(sid)
- if sid_key in section_map:
- room["area_id"] = section_map[sid_key]
-
- if fid is not None:
- fid_key = str(fid)
- if fid_key in function_map:
- room["function_id"] = function_map[fid_key]
-
- submitted_rooms.append(room)
-
- Room.sync_from_state(
- submitted_rooms=submitted_rooms,
- section_map=section_map,
- function_map=function_map
- )
-
- flash("Changes saved.", "success")
-
- except Exception as e:
- flash(f"Error saving changes: {e}", "danger")
-
- return redirect(url_for('main.settings'))
-
- # === GET ===
- brands = db.session.query(Brand).order_by(Brand.name).all()
- types = db.session.query(Item).order_by(Item.description).all()
- sections = db.session.query(Area).order_by(Area.name).all()
- functions = db.session.query(RoomFunction).order_by(RoomFunction.description).all()
- rooms = eager_load_room_relationships(db.session.query(Room).order_by(Room.name)).all()
images = chunk_list(db.session.query(Image).order_by(Image.timestamp).all(), 6)
return render_template('settings.html',
title="Settings",
- brands=[b.serialize() for b in brands],
- types=[{"id": t.id, "name": t.description} for t in types],
- sections=[s.serialize() for s in sections],
- functions=[f.serialize() for f in functions],
- rooms=[r.serialize() for r in rooms],
image_list=images
)
-
-@main.route("/api/settings", methods=["POST"])
-def api_settings():
- try:
- payload = request.get_json(force=True)
- except Exception as e:
- return jsonify({"error": "Invalid JSON"}), 400
-
- errors = []
- errors += Brand.validate_state(payload.get("brands", []))
- errors += Item.validate_state(payload.get("types", []))
- errors += Area.validate_state(payload.get("sections", []))
- errors += RoomFunction.validate_state(payload.get("functions", []))
- errors += Room.validate_state(payload.get("rooms", []))
-
- if errors:
- return jsonify({"errors": errors}), 400
-
- try:
- with db.session.begin():
- section_map = Area.sync_from_state(payload["sections"])
- function_map = RoomFunction.sync_from_state(payload["functions"])
- Brand.sync_from_state(payload["brands"])
- Item.sync_from_state(payload["types"])
- Room.sync_from_state(payload["rooms"], section_map, function_map)
- except Exception as e:
- db.session.rollback()
- return jsonify({"errors": [str(e)]}), 500
-
- return jsonify({"message": "Settings updated successfully."}), 200
diff --git a/inventory/temp.py b/inventory/temp.py
deleted file mode 100644
index 64394ab..0000000
--- a/inventory/temp.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def is_temp_id(val):
- return (
- val is None or
- (isinstance(val, int) and val < 0) or
- (isinstance(val, str) and val.startswith("temp-"))
- )
diff --git a/inventory/utils/validation.py b/inventory/utils/validation.py
deleted file mode 100644
index c68e3f9..0000000
--- a/inventory/utils/validation.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from ..temp import is_temp_id
-
-class ValidatableMixin:
- VALIDATION_LABEL = "entry"
-
- @classmethod
- def validate_state(cls, submitted_items: list[dict]) -> list[str]:
- errors = []
- label = cls.VALIDATION_LABEL or cls.__name__
-
- for index, item in enumerate(submitted_items):
- if not isinstance(item, dict):
- errors.append(f"{label.capitalize()} #{index + 1} is not a valid object.")
- continue
-
- name = item.get("name")
- if not name or not str(name).strip():
- errors.append(f"{label.capitalize()} #{index + 1} is missing a name.")
-
- raw_id = item.get('id')
- if raw_id is not None:
- try:
- _ = int(raw_id)
- except (ValueError, TypeError):
- if not is_temp_id(raw_id):
- errors.append(f"{label.capitalize()} #{index + 1} has invalid ID: {raw_id}")
-
- return errors
\ No newline at end of file