From acacf39f8e8e8f887f028d2dbd46c4ba1fdde691 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Mon, 23 Jun 2025 14:51:21 -0500 Subject: [PATCH] Refactor code structure; improve organization and readability across multiple files --- .gitignore | 2 +- models/areas.py | 6 + models/brands.py | 8 +- models/inventory.py | 18 +++ models/items.py | 9 +- models/room_functions.py | 8 +- models/rooms.py | 8 ++ models/users.py | 11 ++ models/work_log.py | 13 ++ routes.py | 231 +++++++++++++++++++++-------------- static/js/widget.js | 9 +- templates/layout.html | 7 +- templates/settings.html | 254 +++++++++++++++++++++------------------ utils.py | 13 ++ 14 files changed, 380 insertions(+), 217 deletions(-) diff --git a/.gitignore b/.gitignore index 28c8309..bdc1a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -__pycache__/ +**/__pycache__/ .venv/ .env *.db diff --git a/models/areas.py b/models/areas.py index a3ebfe9..abb0906 100644 --- a/models/areas.py +++ b/models/areas.py @@ -17,3 +17,9 @@ class Area(db.Model): def __repr__(self): return f"" + + def serialize(self): + return { + 'id': self.id, + 'name': self.name + } diff --git a/models/brands.py b/models/brands.py index 168894e..6eb5bf2 100644 --- a/models/brands.py +++ b/models/brands.py @@ -16,4 +16,10 @@ class Brand(db.Model): inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='brand') def __repr__(self): - return f"" \ No newline at end of file + return f"" + + def serialize(self): + return { + 'id': self.id, + 'name': self.name + } \ No newline at end of file diff --git a/models/inventory.py b/models/inventory.py index febd8e6..f487eb1 100644 --- a/models/inventory.py +++ b/models/inventory.py @@ -69,3 +69,21 @@ class Inventory(db.Model): return f"Serial: {self.serial}" else: return f"ID: {self.id}" + + def serialize(self) -> dict[str, Any]: + return { + 'id': self.id, + 'timestamp': self.timestamp.isoformat() if self.timestamp else None, + 'condition': self.condition, + 'needed': self.needed, + 'type_id': self.type_id, + 'inventory_name': self.inventory_name, + 'serial': self.serial, + 'model': self.model, + 'notes': self.notes, + 'owner_id': self.owner_id, + 'brand_id': self.brand_id, + 'location_id': self.location_id, + 'barcode': self.barcode, + 'shared': self.shared + } diff --git a/models/items.py b/models/items.py index 0e0ae0e..447bee4 100644 --- a/models/items.py +++ b/models/items.py @@ -17,4 +17,11 @@ class Item(db.Model): inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='item') def __repr__(self): - return f"" \ No newline at end of file + return f"" + + def serialize(self): + return { + 'id': self.id, + 'name': self.description, + 'category': self.category + } \ No newline at end of file diff --git a/models/room_functions.py b/models/room_functions.py index 8683174..e52b2f6 100644 --- a/models/room_functions.py +++ b/models/room_functions.py @@ -16,4 +16,10 @@ class RoomFunction(db.Model): rooms: Mapped[List['Room']] = relationship('Room', back_populates='room_function') def __repr__(self): - return f"" \ No newline at end of file + return f"" + + def serialize(self): + return { + 'id': self.id, + 'name': self.description + } \ No newline at end of file diff --git a/models/rooms.py b/models/rooms.py index 9323535..8af94b0 100644 --- a/models/rooms.py +++ b/models/rooms.py @@ -31,4 +31,12 @@ class Room(db.Model): name = self.name or "" 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 + } diff --git a/models/users.py b/models/users.py index 7df3c17..8c8df85 100644 --- a/models/users.py +++ b/models/users.py @@ -34,3 +34,14 @@ class User(db.Model): def __repr__(self): return f"" + + def serialize(self): + return { + 'id': self.id, + 'first_name': self.first_name, + 'last_name': self.last_name, + 'location_id': self.location_id, + 'supervisor_id': self.supervisor_id, + 'staff': self.staff, + 'active': self.active + } diff --git a/models/work_log.py b/models/work_log.py index 74db981..ef6ff31 100644 --- a/models/work_log.py +++ b/models/work_log.py @@ -32,3 +32,16 @@ class WorkLog(db.Model): return f"" + + def serialize(self): + return { + 'id': self.id, + 'start_time': self.start_time.isoformat() if self.start_time else None, + 'end_time': self.end_time.isoformat() if self.end_time else None, + 'notes': self.notes, + 'complete': self.complete, + 'followup': self.followup, + 'contact_id': self.contact_id, + 'analysis': self.analysis, + 'work_item_id': self.work_item_id + } \ No newline at end of file diff --git a/routes.py b/routes.py index 6d7697f..795b6f4 100644 --- a/routes.py +++ b/routes.py @@ -1,12 +1,10 @@ from flask import Blueprint, render_template, url_for, request, redirect, flash from flask import current_app as app from .models import Brand, Item, Inventory, RoomFunction, User, WorkLog, Room, Area -from sqlalchemy import or_ +from sqlalchemy import or_, delete from sqlalchemy.orm import aliased -from typing import Callable, Any, List from . import db -from .utils import eager_load_user_relationships, eager_load_inventory_relationships, eager_load_room_relationships, eager_load_worklog_relationships, chunk_list -from datetime import datetime, timedelta +from .utils import eager_load_user_relationships, eager_load_inventory_relationships, eager_load_room_relationships, eager_load_worklog_relationships, chunk_list, add_named_entities import pandas as pd import traceback import json @@ -400,6 +398,37 @@ def search(): @main.route('/settings', methods=['GET', 'POST']) def settings(): + def add_named_entities(items: list[str], model, attr: str, mapper: dict | None = None): + for name in items: + clean = name.strip() + if clean: + new_obj = model(**{attr: clean}) # type: ignore + db.session.add(new_obj) + if mapper is not None: + db.session.flush() + mapper[clean] = new_obj.id + + def resolve_id(raw_id, fallback_list, id_map, label): + try: + resolved_id = int(raw_id) + except (TypeError, ValueError): + resolved_id = None + + if resolved_id is not None and resolved_id >= 0: + return resolved_id + + index = abs(resolved_id + 1) if resolved_id is not None else 0 + try: + entry = fallback_list[index] + key = entry["name"] if isinstance(entry, dict) else str(entry).strip() + except (IndexError, KeyError, TypeError): + raise ValueError(f"Invalid {label} index or format at index {index}: {entry}") + + final_id = id_map.get(key) + if not final_id: + raise ValueError(f"Unresolved {label}: {key}") + return final_id + if request.method == 'POST': print("⚠️⚠️⚠️ POST /settings reached! ⚠️⚠️⚠️") form = request.form @@ -407,108 +436,124 @@ def settings(): try: state = json.loads(form['formState']) - except Exception as e: + except Exception: flash("Invalid form state submitted. JSON decode failed.", "danger") traceback.print_exc() return redirect(url_for('main.settings')) - - # === 0. Create new Brands === - for name in state.get('brands', []): - clean = name.strip() - if clean: - print(f"🧪 Creating new brand: {clean}") - new_brand = Brand(name=clean) # type: ignore - db.session.add(new_brand) - # === 0.5 Create new Types === - for name in state.get('types', []): - clean = name.strip() - if clean: - print(f"🧪 Creating new item type: {clean}") - new_type = Item(description=clean) # type: ignore - db.session.add(new_type) - - # === 1. Create new Sections === - section_map = {} - for name in state.get('sections', []): - clean = name.strip() - if clean: - print(f"🧪 Creating new section: {clean}") - new_section = Area(name=clean) # type: ignore - db.session.add(new_section) - db.session.flush() - section_map[clean] = new_section.id - print(f"✅ New section '{clean}' ID: {new_section.id}") - - # === 2. Create new Functions === - function_map = {} - for name in state.get('functions', []): - clean = name.strip() - if clean: - print(f"🧪 Creating new function: {clean}") - new_function = RoomFunction(description=clean) # type: ignore - db.session.add(new_function) - db.session.flush() - function_map[clean] = new_function.id - print(f"✅ New function '{clean}' ID: {new_function.id}") - - # === 3. Create new Rooms === - for idx, room in enumerate(state.get('rooms', [])): - name = room.get('name', '').strip() - raw_section = room.get('section_id') - raw_function = room.get('function_id') - - if not name: - print(f"⚠️ Skipping room at index {idx} due to missing name.") - continue - - try: - section_id = int(raw_section) if raw_section is not None else None - function_id = int(raw_function) if raw_function is not None else None - - # Resolve negative or unmapped IDs - if section_id is None or section_id < 0: - section_idx = abs(section_id + 1) if section_id is not None else 0 - section_name = state['sections'][section_idx] - section_id = section_map.get(section_name) - if not section_id: - raise ValueError(f"Unresolved section: {section_name}") - - if function_id is None or function_id < 0: - function_idx = abs(function_id + 1) if function_id is not None else 0 - function_name = state['functions'][function_idx] - function_id = function_map.get(function_name) - if not function_id: - raise ValueError(f"Unresolved function: {function_name}") - - print(f"🏗️ Creating room '{name}' with section ID {section_id} and function ID {function_id}") - new_room = Room(name=name, area_id=section_id, function_id=function_id) # type: ignore - db.session.add(new_room) - - except Exception as e: - print(f"❌ Failed to process room at index {idx}: {e}") - traceback.print_exc() - flash(f"Error processing room '{name}': {e}", "danger") - - - # === 4. Commit changes === try: - print("🚀 Attempting commit...") - db.session.commit() - print("✅ Commit succeeded.") + with db.session.begin(): + # === BRANDS === + existing_brands = {b.name for b in db.session.query(Brand).all()} + submitted_brands = { + str(b.get("name", "")).strip() + for b in state.get("brands", []) + if isinstance(b, dict) and str(b.get("name", "")).strip() + } + print(f"🔍 Brands in DB: {existing_brands}") + print(f"🆕 Brands submitted: {submitted_brands}") + print(f"➖ Brands to delete: {existing_brands - submitted_brands}") + for name in submitted_brands - existing_brands: + db.session.add(Brand(name=name)) + for name in existing_brands - submitted_brands: + print(f"🗑️ Deleting brand: {name}") + db.session.execute(delete(Brand).where(Brand.name == name)) + + # === TYPES === + existing_types = {i.description for i in db.session.query(Item).all()} + submitted_types = { + str(t.get("name") or t.get("description") or "").strip() + for t in state.get("types", []) + if isinstance(t, dict) and str(t.get("name") or t.get("description") or "").strip() + } + print(f"🔍 Types in DB: {existing_types}") + print(f"🆕 Types submitted: {submitted_types}") + print(f"➖ Types to delete: {existing_types - submitted_types}") + for desc in submitted_types - existing_types: + db.session.add(Item(description=desc)) + for desc in existing_types - submitted_types: + print(f"🗑️ Deleting type: {desc}") + db.session.execute(delete(Item).where(Item.description == desc)) + + # === SECTIONS === + existing_sections = {a.name for a in db.session.query(Area).all()} + submitted_sections = { + str(s.get("name", "")).strip() + for s in state.get("sections", []) + if isinstance(s, dict) and str(s.get("name", "")).strip() + } + print(f"🔍 Sections in DB: {existing_sections}") + print(f"🆕 Sections submitted: {submitted_sections}") + print(f"➖ Sections to delete: {existing_sections - submitted_sections}") + for name in submitted_sections - existing_sections: + db.session.add(Area(name=name)) + for name in existing_sections - submitted_sections: + print(f"🗑️ Deleting section: {name}") + db.session.execute(delete(Area).where(Area.name == name)) + + # === FUNCTIONS === + existing_funcs = {f.description for f in db.session.query(RoomFunction).all()} + submitted_funcs = { + str(f.get("name", "")).strip() + for f in state.get("functions", []) + if isinstance(f, dict) and str(f.get("name", "")).strip() + } + print(f"🔍 Functions in DB: {existing_funcs}") + print(f"🆕 Functions submitted: {submitted_funcs}") + print(f"➖ Functions to delete: {existing_funcs - submitted_funcs}") + for desc in submitted_funcs - existing_funcs: + db.session.add(RoomFunction(description=desc)) + for desc in existing_funcs - submitted_funcs: + print(f"🗑️ Deleting function: {desc}") + db.session.execute(delete(RoomFunction).where(RoomFunction.description == desc)) + + # === REPOPULATE MAPS === + section_map = {a.name: a.id for a in db.session.query(Area).all()} + function_map = {f.description: f.id for f in db.session.query(RoomFunction).all()} + + # === ROOMS === + existing_rooms = {r.name: r.id for r in db.session.query(Room).all()} + submitted_rooms = { + str(r.get("name", "")).strip(): r + for r in state.get("rooms", []) + if str(r.get("name", "")).strip() + } + print(f"🔍 Rooms in DB: {list(existing_rooms.keys())}") + print(f"🆕 Rooms submitted: {list(submitted_rooms.keys())}") + print(f"➖ Rooms to delete: {set(existing_rooms.keys()) - set(submitted_rooms.keys())}") + for name, room_data in submitted_rooms.items(): + if name not in existing_rooms: + section_id = resolve_id(room_data.get("section_id"), state["sections"], section_map, "section") if room_data.get("section_id") is not None else None + function_id = resolve_id(room_data.get("function_id"), state["functions"], function_map, "function") if room_data.get("function_id") is not None else None + db.session.add(Room(name=name, area_id=section_id, function_id=function_id)) + + for name in set(existing_rooms.keys()) - set(submitted_rooms.keys()): + print(f"🗑️ Deleting room: {name}") + db.session.execute(delete(Room).where(Room.name == name)) + + print("✅ COMMIT executed.") + flash("Changes saved.", "success") + except Exception as e: - db.session.rollback() print("❌ COMMIT FAILED ❌") traceback.print_exc() flash(f"Error saving changes: {e}", "danger") - flash("Changes saved.", "success") return redirect(url_for('main.settings')) # === GET === brands = db.session.query(Brand).order_by(Brand.name).all() - types = db.session.query(Item.id, Item.description.label("name")).order_by(Item.description).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.id, RoomFunction.description.label("name")).order_by(RoomFunction.description).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() - return render_template('settings.html', title="Settings", brands=brands, types=types, sections=sections, functions=functions, rooms=rooms) + + 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], + ) + diff --git a/static/js/widget.js b/static/js/widget.js index 323edbc..cdbffbe 100644 --- a/static/js/widget.js +++ b/static/js/widget.js @@ -30,7 +30,7 @@ const ComboBoxWidget = (() => { return; } - const option = new Option(value, value); + const option = createOption(value); // Already built to handle temp IDs select.add(option); formState[stateArray].push(value); input.value = ''; @@ -94,6 +94,11 @@ const ComboBoxWidget = (() => { } currentlyEditing = null; } else { + if (config.onAdd) { + config.onAdd(newItem, list, createOption); + return; // Skip the default logic! + } + const exists = Array.from(list.options).some(opt => opt.textContent === newItem); if (exists) { alert(`"${newItem}" already exists.`); @@ -105,7 +110,7 @@ const ComboBoxWidget = (() => { const key = config.stateArray ?? `${ns}s`; // fallback to pluralization if (Array.isArray(formState?.[key])) { - formState[key].push(newItem); + formState[key].push({ name: newItem }); } if (config.sort !== false) { diff --git a/templates/layout.html b/templates/layout.html index c6758f7..4f6fd27 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -52,7 +52,12 @@ -
{% block content %}{% endblock %}
+
+
+ {% block content %}{% endblock %} + +
+