Complete and total rework ahead.
This commit is contained in:
parent
559fd56f33
commit
e420110fb3
95 changed files with 394 additions and 6351 deletions
|
|
@ -1,155 +0,0 @@
|
|||
import logging
|
||||
|
||||
from flask import Blueprint, g
|
||||
from sqlalchemy.engine import ScalarResult
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.sql import Select
|
||||
from typing import Iterable, Any, cast
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from . import inventory, user, worklog, settings, index, search, hooks
|
||||
from .. import db
|
||||
from ..ui.blueprint import get_model_class, call
|
||||
from ..ui.defaults import default_query
|
||||
|
||||
def _eager_from_fields(Model, fields: Iterable[str]):
|
||||
rels = {f.split(".", 1)[0] for f in fields if "." in f}
|
||||
opts = []
|
||||
for r in rels:
|
||||
rel_attr = getattr(Model, r, None)
|
||||
if getattr(rel_attr, "property", None) is not None:
|
||||
opts.append(joinedload(rel_attr))
|
||||
return opts
|
||||
|
||||
def _cell_cache():
|
||||
if not hasattr(g, '_cell_cache'):
|
||||
g._cell_cache = {}
|
||||
return g._cell_cache
|
||||
|
||||
def _tmpl_cache(name: str):
|
||||
if not hasattr(g, "_tmpl_caches"):
|
||||
g._tmpl_caches = {}
|
||||
return g._tmpl_caches.setdefault(name, {})
|
||||
|
||||
def _project_row(obj: Any, fields: Iterable[str]) -> dict[str, Any]:
|
||||
out = {"id": obj.id}
|
||||
Model = type(obj)
|
||||
allow = getattr(Model, "ui_value_allow", None)
|
||||
for f in fields:
|
||||
if allow and f not in allow:
|
||||
out[f] = None
|
||||
continue
|
||||
if "." in f:
|
||||
rel, attr = f.split(".", 1)
|
||||
relobj = getattr(obj, rel, None)
|
||||
out[f] = getattr(relobj, attr, None) if relobj else None
|
||||
else:
|
||||
out[f] = getattr(obj, f, None)
|
||||
return out
|
||||
|
||||
@main.app_template_global()
|
||||
def cell(model_name: str, id_: int, field: str, default: str = ""):
|
||||
from ..ui.blueprint import get_model_class, call
|
||||
from ..ui.defaults import default_value
|
||||
key = (model_name, int(id_), field)
|
||||
cache = _cell_cache()
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
|
||||
try:
|
||||
Model = get_model_class(model_name)
|
||||
val = call(Model, 'ui_value', db.session, id_=id_, field=field)
|
||||
if val is None:
|
||||
val = default_value(db.session, Model, id_=id_, field=field)
|
||||
except Exception as e:
|
||||
log.warning(f"cell() error for {model_name} {id_} {field}: {e}")
|
||||
val = default
|
||||
if val is None:
|
||||
val = default
|
||||
cache[key] = val
|
||||
return val
|
||||
|
||||
@main.app_template_global()
|
||||
def cells(model_name: str, id_: int, *fields: str):
|
||||
from ..ui.blueprint import get_model_class, call
|
||||
from ..ui.defaults import default_values
|
||||
fields = [f for f in fields if f]
|
||||
key = (model_name, int(id_), tuple(fields))
|
||||
cache = _cell_cache()
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
|
||||
try:
|
||||
Model = get_model_class(model_name)
|
||||
data = call(Model, 'ui_values', db.session, id_=int(id_), fields=fields)
|
||||
if data is None:
|
||||
data = default_values(db.session, Model, id_=int(id_), fields=fields)
|
||||
except Exception:
|
||||
data = {f: None for f in fields}
|
||||
cache[key] = data
|
||||
return data
|
||||
|
||||
@main.app_template_global()
|
||||
def row(model_name: str, id_: int, *fields: str):
|
||||
"""
|
||||
One row, many fields. Returns a dict like {'id': 1, 'a':..., 'rel.b': ...}
|
||||
"""
|
||||
fields = [f for f in fields if f]
|
||||
key = (model_name, int(id_), tuple(fields))
|
||||
cache = _tmpl_cache("row")
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
|
||||
Model = get_model_class(model_name)
|
||||
obj = db.session.get(Model, int(id_))
|
||||
data = _project_row(obj, fields) if obj else {"id": int(id_), **{f: None for f in fields}}
|
||||
cache[key] = data
|
||||
return data
|
||||
|
||||
@main.app_template_global()
|
||||
def table(model_name: str, fields: Iterable[str], *,
|
||||
q: str = None, sort: str = None, direction: str = "asc",
|
||||
limit: int = 100, offset: int = 0):
|
||||
"""
|
||||
Many rows, many fields — mirrors /list behavior, but returns only the requested columns.
|
||||
Uses ui_query(Model, session, **qkwargs) if present else default_query. Cached per request.
|
||||
"""
|
||||
fields = [f.strip() for f in (fields or []) if f and f.strip()]
|
||||
key = (model_name, tuple(fields), q or "", sort or "", direction or "asc", int(limit), int(offset))
|
||||
cache = _tmpl_cache("table")
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
|
||||
Model = get_model_class(model_name)
|
||||
qkwargs = dict(text=(q or None), limit=int(limit), offset=int(offset),
|
||||
sort=(sort or None), direction=(direction or "asc").lower())
|
||||
extra_opts = _eager_from_fields(Model, fields)
|
||||
if extra_opts:
|
||||
if isinstance(rows_any, Select):
|
||||
rows_any = rows_any.options(*extra_opts)
|
||||
elif rows_any is None:
|
||||
original = getattr(Model, 'ui_eagerload', ())
|
||||
def dyn_opts():
|
||||
base = original() if callable(original) else original
|
||||
return tuple(base) + tuple(extra_opts)
|
||||
setattr(Model, 'ui_eagerload', dyn_opts)
|
||||
rows_any: Any = call(Model, "ui_query", db.session, **qkwargs)
|
||||
|
||||
if rows_any is None:
|
||||
objs = default_query(db.session, Model, **qkwargs)
|
||||
elif isinstance(rows_any, list):
|
||||
objs = rows_any
|
||||
elif isinstance(rows_any, Select):
|
||||
objs = list(cast(ScalarResult[Any], db.session.execute(rows_any).scalars()))
|
||||
else:
|
||||
scalars = getattr(rows_any, "scalars", None)
|
||||
if callable(scalars):
|
||||
objs = list(cast(ScalarResult[Any], scalars()))
|
||||
else:
|
||||
objs = list(rows_any)
|
||||
|
||||
data = [_project_row(o, fields) for o in objs]
|
||||
cache[key] = data
|
||||
return data
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
import base64
|
||||
import csv
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
|
||||
from flask import url_for, jsonify, request
|
||||
from flask import current_app as app
|
||||
|
||||
from ..models import User, Inventory, WorkLog
|
||||
|
||||
from ..models.image import ImageAttachable
|
||||
|
||||
ROUTE_BREADCRUMBS = {
|
||||
'main.user': {
|
||||
'trail': [('Users', 'main.list_users')],
|
||||
'model': User,
|
||||
'arg': 'id',
|
||||
'label_attr': 'identifier',
|
||||
'url_func': lambda i: url_for('main.user', id=i.id)
|
||||
},
|
||||
'main.inventory_item': {
|
||||
'trail': [('Inventory', 'main.list_inventory')],
|
||||
'model': Inventory,
|
||||
'arg': 'id',
|
||||
'label_attr': 'identifier',
|
||||
'url_func': lambda i: url_for('main.inventory_item', id=i.id)
|
||||
},
|
||||
'main.worklog': {
|
||||
'trail': [('Work Log', 'main.list_worklog')],
|
||||
'model': WorkLog,
|
||||
'arg': 'id',
|
||||
'label_attr': 'identifier',
|
||||
'url_func': lambda i: url_for('main.worklog', id=i.id)
|
||||
}
|
||||
}
|
||||
|
||||
inventory_headers = {
|
||||
"Date Entered": lambda i: {"field": "timestamp", "text": i.timestamp.strftime("%Y-%m-%d") if i.timestamp else None},
|
||||
"Identifier": lambda i: {"field": "identifier", "text": i.identifier},
|
||||
"Name": lambda i: {"field": "name", "text": i.name},
|
||||
"Serial Number": lambda i: {"field": "serial", "text": i.serial},
|
||||
"Bar Code": lambda i: {"field": "barcode", "text": i.barcode},
|
||||
"Brand": lambda i: {"field": "brand.name", "text": i.brand.name} if i.brand else {"text": None},
|
||||
"Model": lambda i: {"field": "model", "text": i.model},
|
||||
"Item Type": lambda i: {"field": "device_type.description", "text": i.device_type.description} if i.device_type else {"text": None},
|
||||
"Shared?": lambda i: {"field": "shared", "text": i.shared, "type": "bool", "html": checked_box if i.shared else unchecked_box},
|
||||
"Owner": lambda i: {"field": "owner.identifier", "text": i.owner.identifier, "url": url_for("main.user_item", id=i.owner.id)} if i.owner else {"text": None},
|
||||
"Location": lambda i: {"field": "location.identifier", "text": i.location.identifier} if i.location else {"Text": None},
|
||||
"Condition": lambda i: {"field": "condition", "text": i.condition}
|
||||
}
|
||||
|
||||
checked_box = '''
|
||||
<i class="bi bi-check2"></i>
|
||||
'''
|
||||
unchecked_box = ''
|
||||
|
||||
ACTIVE_STATUSES = [
|
||||
"Working",
|
||||
"Deployed",
|
||||
"Partially Inoperable",
|
||||
"Unverified"
|
||||
]
|
||||
|
||||
INACTIVE_STATUSES = [
|
||||
"Inoperable",
|
||||
"Removed",
|
||||
"Disposed"
|
||||
]
|
||||
|
||||
FILTER_MAP = {
|
||||
'user': Inventory.owner_id,
|
||||
'location': Inventory.location_id,
|
||||
'type': Inventory.type_id,
|
||||
}
|
||||
|
||||
user_headers = {
|
||||
"Last Name": lambda i: {"field": "last_name","text": i.last_name},
|
||||
"First Name": lambda i: {"field": "first_name","text": i.first_name},
|
||||
"Title": lambda i: {"field": "title","text": i.title},
|
||||
"Supervisor": lambda i: {"field": "supervisor,identifier","text": i.supervisor.identifier, "url": url_for("main.user_item", id=i.supervisor.id)} if i.supervisor else {"text": None},
|
||||
"Location": lambda i: {"field": "location,identifier","text": i.location.identifier} if i.location else {"text": None},
|
||||
"Staff?": lambda i: {"field": "staff","text": i.staff, "type": "bool", "html": checked_box if i.staff else unchecked_box},
|
||||
"Active?": lambda i: {"field": "active","text": i.active, "type": "bool", "html": checked_box if i.active else unchecked_box}
|
||||
}
|
||||
|
||||
worklog_headers = {
|
||||
"Contact": lambda i: {"text": i.contact.identifier, "url": url_for("main.user_item", id=i.contact.id)} if i.contact else {"Text": None},
|
||||
"Work Item": lambda i: {"text": i.work_item.identifier, "url": url_for('main.inventory_item',id=i.work_item.id)} if i.work_item else {"text": None},
|
||||
"Start Time": lambda i: {"text": i.start_time.strftime("%Y-%m-%d")},
|
||||
"End Time": lambda i: {"text": i.end_time.strftime("%Y-%m-%d")} if i.end_time else {"text": None},
|
||||
"Complete?": lambda i: {"text": i.complete, "type": "bool", "html": checked_box if i.complete else unchecked_box},
|
||||
"Follow Up?": lambda i: {"text": i.followup, "type": "bool", "html": checked_box if i.followup else unchecked_box, "highlight": i.followup},
|
||||
"Quick Analysis?": lambda i: {"text": i.analysis, "type": "bool", "html": checked_box if i.analysis else unchecked_box},
|
||||
}
|
||||
|
||||
def link(text, endpoint, **values):
|
||||
return {"text": text, "url": url_for(endpoint, **values)}
|
||||
|
||||
|
||||
def generate_hashed_filename(file) -> str:
|
||||
content = file.read()
|
||||
file.seek(0) # Reset after reading
|
||||
|
||||
hash = hashlib.sha256(content).hexdigest()
|
||||
ext = os.path.splitext(file.filename)[1]
|
||||
return f"{hash}_{file.filename}"
|
||||
|
||||
def get_image_attachable_class_by_name(name: str):
|
||||
for cls in ImageAttachable.__subclasses__():
|
||||
if getattr(cls, '__tablename__', None) == name:
|
||||
return cls
|
||||
return None
|
||||
|
||||
def make_csv(export_func, columns, rows):
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
writer.writerow(columns)
|
||||
|
||||
for row in rows:
|
||||
writer.writerow([export_func(row, col) for col in columns])
|
||||
|
||||
csv_string = output.getvalue()
|
||||
output.close()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"csv": base64.b64encode(csv_string.encode()).decode(),
|
||||
"count": len(rows)
|
||||
})
|
||||
|
||||
def generate_breadcrumbs():
|
||||
crumbs = []
|
||||
|
||||
endpoint = request.endpoint
|
||||
view_args = request.view_args or {}
|
||||
|
||||
if endpoint in ROUTE_BREADCRUMBS:
|
||||
print(endpoint, view_args)
|
||||
config = ROUTE_BREADCRUMBS[endpoint]
|
||||
|
||||
for label, ep in config.get('trail', []):
|
||||
crumbs.append({'label': label, 'url': url_for(ep)})
|
||||
|
||||
obj_id = view_args.get(config['arg'])
|
||||
if obj_id:
|
||||
obj = config['model'].query.get(obj_id)
|
||||
if obj:
|
||||
crumbs.append({
|
||||
'label': getattr(obj, config['label_attr'], str(obj)),
|
||||
'url': config['url_func'](obj)
|
||||
})
|
||||
else:
|
||||
# fallback to ugly slashes
|
||||
path = request.path.strip('/').split('/')
|
||||
accumulated = ''
|
||||
for segment in path:
|
||||
accumulated += '/' + segment
|
||||
crumbs.append({'label': segment.title(), 'url': accumulated})
|
||||
|
||||
return crumbs
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
from bs4 import BeautifulSoup
|
||||
from flask import current_app as app
|
||||
|
||||
from . import main
|
||||
|
||||
@main.after_request
|
||||
def prettify_or_minify_html_response(response):
|
||||
if response.content_type.startswith("text/html"):
|
||||
try:
|
||||
html = response.get_data(as_text=True)
|
||||
soup = BeautifulSoup(html, 'html5lib')
|
||||
|
||||
if app.debug:
|
||||
pretty_html = soup.prettify()
|
||||
response.set_data(pretty_html.encode("utf-8")) # type: ignore
|
||||
#else:
|
||||
# # Minify by stripping extra whitespace between tags and inside text
|
||||
# minified_html = re.sub(r">\s+<", "><", str(soup)) # collapse whitespace between tags
|
||||
# minified_html = re.sub(r"\s{2,}", " ", minified_html) # collapse multi-spaces to one
|
||||
# minified_html = re.sub(r"\n+", "", minified_html) # remove newlines
|
||||
|
||||
# response.set_data(minified_html.encode("utf-8")) # type: ignore
|
||||
|
||||
response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||
except Exception as e:
|
||||
print(f"⚠️ Prettifying/Minifying failed: {e}")
|
||||
|
||||
return response
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import os
|
||||
import posixpath
|
||||
|
||||
from flask import Blueprint, current_app, request, jsonify
|
||||
|
||||
from .helpers import generate_hashed_filename, get_image_attachable_class_by_name
|
||||
from .. import db
|
||||
from ..models import Image
|
||||
|
||||
image_bp = Blueprint("image_api", __name__)
|
||||
|
||||
def save_image(file, model: str) -> str:
|
||||
assert current_app.static_folder
|
||||
|
||||
filename = generate_hashed_filename(file)
|
||||
rel_path = posixpath.join("uploads", "images", model, filename)
|
||||
abs_path = os.path.join(current_app.static_folder, rel_path)
|
||||
|
||||
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
||||
|
||||
file.save(abs_path)
|
||||
return rel_path
|
||||
|
||||
@image_bp.route("/api/images", methods=["POST"])
|
||||
def upload_image():
|
||||
file = request.files.get("file")
|
||||
model = request.form.get("target_model")
|
||||
model_id = request.form.get("model_id")
|
||||
caption = request.form.get("caption", "")
|
||||
|
||||
if not file or not model or not model_id:
|
||||
return jsonify({"success": False, "error": "Missing file, model, or model_id"}), 400
|
||||
|
||||
ModelClass = get_image_attachable_class_by_name(model)
|
||||
if not ModelClass:
|
||||
return jsonify({"success": False, "error": f"Model '{model}' does not support image attachments."}), 400
|
||||
|
||||
try:
|
||||
model_id = int(model_id)
|
||||
except ValueError:
|
||||
return jsonify({"success": False, "error": "model_id must be an integer"}), 400
|
||||
|
||||
# Save file
|
||||
rel_path = save_image(file, model)
|
||||
|
||||
# Create Image row
|
||||
image = Image(filename=rel_path, caption=caption)
|
||||
db.session.add(image)
|
||||
|
||||
# Attach image to model
|
||||
target = db.session.get(ModelClass, model_id)
|
||||
if not target:
|
||||
return jsonify({"success": False, "error": f"No {model} found with ID {model_id}"}), 404
|
||||
|
||||
target.attach_image(image)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({"success": True, "id": image.id}), 201
|
||||
|
||||
@image_bp.route("/api/images/<int:image_id>", methods=["GET"])
|
||||
def get_image(image_id: int):
|
||||
image = db.session.get(Image, image_id)
|
||||
if not image:
|
||||
return jsonify({"success": False, "error": f"No image found with ID {image_id}"}), 404
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"id": image.id,
|
||||
"filename": image.filename,
|
||||
"caption": image.caption,
|
||||
"timestamp": image.timestamp.isoformat() if image.timestamp else None,
|
||||
"url": f"/static/{image.filename}"
|
||||
})
|
||||
|
||||
@image_bp.route("/api/images/<int:image_id>", methods=["DELETE"])
|
||||
def delete_image(image_id):
|
||||
image = db.session.get(Image, image_id)
|
||||
if not image:
|
||||
return jsonify({"success": False, "error": "Image not found"})
|
||||
|
||||
rel_path = posixpath.normpath(str(image.filename))
|
||||
static_dir = str(current_app.static_folder) # appease the gods
|
||||
abs_path = os.path.join(static_dir, rel_path)
|
||||
|
||||
if os.path.exists(abs_path):
|
||||
os.remove(abs_path)
|
||||
|
||||
db.session.delete(image)
|
||||
db.session.commit()
|
||||
return jsonify({"success": True})
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
from flask import render_template, request
|
||||
import pandas as pd
|
||||
import random
|
||||
|
||||
from . import main
|
||||
from .helpers import worklog_headers
|
||||
from .. import db
|
||||
from ..models import WorkLog, Inventory
|
||||
from ..utils.load import eager_load_worklog_relationships, eager_load_inventory_relationships
|
||||
|
||||
def generate_solvable_matrix(level, seed_clicks=None):
|
||||
size = level + 3
|
||||
matrix = [[True for _ in range(size)] for _ in range(size)]
|
||||
presses = []
|
||||
|
||||
def press(x, y):
|
||||
# record the press (once)
|
||||
presses.append((x, y))
|
||||
# apply its effect
|
||||
for dx, dy in [(0,0),(-1,0),(1,0),(0,-1),(0,1)]:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 0 <= nx < size and 0 <= ny < size:
|
||||
matrix[nx][ny] = not matrix[nx][ny]
|
||||
|
||||
num_clicks = seed_clicks if seed_clicks is not None else random.randint(size, size * 2)
|
||||
for _ in range(num_clicks):
|
||||
x = random.randint(0, size - 1)
|
||||
y = random.randint(0, size - 1)
|
||||
press(x, y)
|
||||
|
||||
return matrix, presses # return the PRESS LIST as the “solution”
|
||||
|
||||
@main.route("/12648243")
|
||||
def coffee():
|
||||
level = request.args.get('level', 0, int)
|
||||
score = request.args.get('score', 0, int)
|
||||
matrix, clicked = generate_solvable_matrix(level)
|
||||
return render_template("coffee.html", matrix=matrix, level=level, clicked=clicked, score=score)
|
||||
|
||||
@main.route("/playground")
|
||||
def playground():
|
||||
return render_template("playground.html")
|
||||
|
||||
@main.route("/")
|
||||
def index():
|
||||
worklog_query = eager_load_worklog_relationships(
|
||||
db.session.query(WorkLog)
|
||||
).all()
|
||||
|
||||
active_worklogs = [log for log in worklog_query if not log.complete]
|
||||
|
||||
active_count = len(active_worklogs)
|
||||
active_worklog_headers = {
|
||||
k: v for k, v in worklog_headers.items()
|
||||
if k not in ['End Time', 'Quick Analysis?', 'Complete?', 'Follow Up?']
|
||||
}
|
||||
|
||||
inventory_query = eager_load_inventory_relationships(
|
||||
db.session.query(Inventory)
|
||||
)
|
||||
|
||||
results = inventory_query.all()
|
||||
|
||||
data = [{
|
||||
'id': item.id,
|
||||
'condition': item.condition
|
||||
} for item in results]
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Count items per condition
|
||||
expected_conditions = [
|
||||
'Deployed','Inoperable', 'Partially Inoperable',
|
||||
'Unverified', 'Working'
|
||||
]
|
||||
if 'condition' in df.columns:
|
||||
pivot = df['condition'].value_counts().reindex(expected_conditions, fill_value=0)
|
||||
else:
|
||||
pivot = pd.Series([0] * len(expected_conditions), index=expected_conditions)
|
||||
|
||||
# Convert pandas/numpy int64s to plain old Python ints
|
||||
pivot = pivot.astype(int)
|
||||
labels = list(pivot.index)
|
||||
data = [int(x) for x in pivot.values]
|
||||
|
||||
datasets = {}
|
||||
|
||||
datasets['summary'] = [{
|
||||
'type': 'pie',
|
||||
'labels': labels,
|
||||
'values': data,
|
||||
'name': 'Inventory Conditions'
|
||||
}]
|
||||
|
||||
users = set([log.contact for log in worklog_query if log.contact])
|
||||
work_summary = {}
|
||||
|
||||
for user in sorted(users, key=lambda u: u.identifier):
|
||||
work_summary[user.identifier] = {}
|
||||
work_summary[user.identifier]['active_count'] = len([log for log in worklog_query if log.contact == user and not log.complete])
|
||||
work_summary[user.identifier]['complete_count'] = len([log for log in worklog_query if log.contact == user and log.complete])
|
||||
|
||||
datasets['work_summary'] = [{
|
||||
'type': 'bar',
|
||||
'x': list(work_summary.keys()),
|
||||
'y': [work_summary[user]['active_count'] for user in work_summary],
|
||||
'name': 'Active Worklogs',
|
||||
'marker': {'color': 'red'}
|
||||
}, {
|
||||
'type': 'bar',
|
||||
'x': list(work_summary.keys()),
|
||||
'y': [work_summary[user]['complete_count'] for user in work_summary],
|
||||
'name': 'Completed Worklogs',
|
||||
'marker': {'color': 'green'}
|
||||
}]
|
||||
|
||||
active_worklog_rows = []
|
||||
for log in active_worklogs:
|
||||
# Create a dictionary of {column name: cell dict}
|
||||
cells_by_key = {k: fn(log) for k, fn in worklog_headers.items()}
|
||||
|
||||
# Use original, full header set for logic
|
||||
highlight = cells_by_key.get("Follow Up?", {}).get("highlight", False)
|
||||
|
||||
# Use only filtered headers — and in exact order
|
||||
cells = [cells_by_key[k] for k in active_worklog_headers]
|
||||
|
||||
active_worklog_rows.append({
|
||||
"id": log.id,
|
||||
"cells": cells,
|
||||
"highlight": highlight
|
||||
})
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
active_count=active_count,
|
||||
active_worklog_headers=active_worklog_headers,
|
||||
active_worklog_rows=active_worklog_rows,
|
||||
labels=labels,
|
||||
datasets=datasets
|
||||
)
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
import datetime
|
||||
from flask import request, render_template, jsonify
|
||||
|
||||
from . import main
|
||||
from .helpers import FILTER_MAP, inventory_headers, worklog_headers, make_csv
|
||||
|
||||
from .. import db
|
||||
from ..models import Inventory, User, Room, Item, RoomFunction, Brand, WorkLog
|
||||
from ..utils.load import eager_load_inventory_relationships, eager_load_user_relationships, eager_load_worklog_relationships, eager_load_room_relationships, chunk_list
|
||||
|
||||
@main.route("/inventory")
|
||||
def list_inventory():
|
||||
filter_by = request.args.get('filter_by', type=str)
|
||||
id = request.args.get('id', type=int)
|
||||
|
||||
filter_name = None
|
||||
|
||||
query = db.session.query(Inventory)
|
||||
query = eager_load_inventory_relationships(query)
|
||||
# query = query.order_by(Inventory.name, Inventory.barcode, Inventory.serial)
|
||||
|
||||
if filter_by and id:
|
||||
column = FILTER_MAP.get(filter_by)
|
||||
if column is not None:
|
||||
filter_name = None
|
||||
if filter_by == 'user':
|
||||
if not (user := db.session.query(User).filter(User.id == id).first()):
|
||||
return "Invalid User ID", 400
|
||||
filter_name = user.identifier
|
||||
elif filter_by == 'location':
|
||||
if not (room := db.session.query(Room).filter(Room.id == id).first()):
|
||||
return "Invalid Location ID", 400
|
||||
filter_name = room.identifier
|
||||
else:
|
||||
if not (item := db.session.query(Item).filter(Item.id == id).first()):
|
||||
return "Invalid Type ID", 400
|
||||
filter_name = item.description
|
||||
|
||||
query = query.filter(column == id)
|
||||
else:
|
||||
return "Invalid filter_by parameter", 400
|
||||
|
||||
inventory = query.all()
|
||||
inventory = sorted(inventory, key=lambda i: i.identifier)
|
||||
|
||||
rows=[{"id": item.id, "cells": [row_fn(item) for row_fn in inventory_headers.values()]} for item in inventory]
|
||||
fields = [d['field'] for d in rows[0]['cells']]
|
||||
|
||||
return render_template(
|
||||
'table.html',
|
||||
title=f"Inventory Listing ({filter_name})" if filter_by else "Inventory Listing",
|
||||
header=inventory_headers,
|
||||
fields=fields,
|
||||
rows=rows,
|
||||
entry_route = 'inventory_item',
|
||||
csv_route = 'inventory',
|
||||
model_name = 'inventory'
|
||||
)
|
||||
|
||||
@main.route("/inventory/index")
|
||||
def inventory_index():
|
||||
category = request.args.get('category')
|
||||
listing = None
|
||||
|
||||
if category == 'user':
|
||||
users = db.session.query(User.id, User.first_name, User.last_name).order_by(User.first_name, User.last_name).all()
|
||||
listing = chunk_list([(user.id, f"{user.first_name or ''} {user.last_name or ''}".strip()) for user in users], 12)
|
||||
elif category == 'location':
|
||||
rooms = (
|
||||
db.session.query(Room.id, Room.name, RoomFunction.description)
|
||||
.join(RoomFunction, Room.function_id == RoomFunction.id)
|
||||
.order_by(Room.name, RoomFunction.description)
|
||||
.all()
|
||||
)
|
||||
listing = chunk_list([(room.id, f"{room.name or ''} - {room.description or ''}".strip()) for room in rooms], 12)
|
||||
elif category == 'type':
|
||||
types = db.session.query(Item.id, Item.description).order_by(Item.description).all()
|
||||
listing = chunk_list(types, 12)
|
||||
elif category:
|
||||
return f"Dude, why {category}?"
|
||||
|
||||
return render_template(
|
||||
'inventory_index.html',
|
||||
title=f"Inventory ({category.capitalize()} Index)" if category else "Inventory",
|
||||
category=category,
|
||||
listing=listing
|
||||
)
|
||||
|
||||
@main.route("/inventory_item/<id>", methods=['GET', 'POST'])
|
||||
def inventory_item(id):
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
return render_template('error.html', title="Bad ID", message="ID must be an integer", endpoint='inventory_item', endpoint_args={'id': -1})
|
||||
|
||||
inventory_query = db.session.query(Inventory)
|
||||
item = eager_load_inventory_relationships(inventory_query).filter(Inventory.id == id).first()
|
||||
brands = db.session.query(Brand).order_by(Brand.name).all()
|
||||
users = eager_load_user_relationships(db.session.query(User).filter(User.active == True).order_by(User.first_name, User.last_name)).all()
|
||||
rooms = eager_load_room_relationships(db.session.query(Room).order_by(Room.name)).all()
|
||||
worklog_query = db.session.query(WorkLog).filter(WorkLog.work_item_id == id)
|
||||
worklog = eager_load_worklog_relationships(worklog_query).all()
|
||||
notes = [note for log in worklog for note in log.updates]
|
||||
types = db.session.query(Item).order_by(Item.description).all()
|
||||
filtered_worklog_headers = {k: v for k, v in worklog_headers.items() if k not in ['Work Item', 'Contact', 'Follow Up?', 'Quick Analysis?']}
|
||||
|
||||
if item:
|
||||
title = f"Inventory Record - {item.identifier}"
|
||||
else:
|
||||
title = "Inventory Record - Not Found"
|
||||
return render_template('error.html',
|
||||
title=title,
|
||||
message=f'Inventory item with id {id} not found!',
|
||||
endpoint='inventory_item',
|
||||
endpoint_args={'id': -1})
|
||||
|
||||
return render_template("inventory.html", title=title, item=item,
|
||||
brands=brands, users=users, rooms=rooms,
|
||||
worklog=worklog,
|
||||
worklog_headers=filtered_worklog_headers,
|
||||
worklog_rows=[{"id": log.id, "cells": [fn(log) for fn in filtered_worklog_headers.values()]} for log in worklog],
|
||||
types=types,
|
||||
notes=notes
|
||||
)
|
||||
|
||||
@main.route("/inventory_item/new", methods=["GET"])
|
||||
def new_inventory_item():
|
||||
brands = db.session.query(Brand).order_by(Brand.name).all()
|
||||
users = eager_load_user_relationships(db.session.query(User).filter(User.active == True).order_by(User.first_name, User.last_name)).all()
|
||||
rooms = eager_load_room_relationships(db.session.query(Room).order_by(Room.name)).all()
|
||||
types = db.session.query(Item).order_by(Item.description).all()
|
||||
|
||||
item = Inventory(
|
||||
timestamp=datetime.datetime.now(),
|
||||
condition="Unverified",
|
||||
type_id=None,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"inventory.html",
|
||||
item=item,
|
||||
brands=brands,
|
||||
users=users,
|
||||
rooms=rooms,
|
||||
types=types,
|
||||
worklog=[],
|
||||
worklog_headers={},
|
||||
worklog_rows=[]
|
||||
)
|
||||
|
||||
@main.route("/api/inventory", methods=["POST"])
|
||||
def create_inventory_item():
|
||||
try:
|
||||
data = request.get_json(force=True)
|
||||
|
||||
new_item = Inventory.from_dict(data)
|
||||
|
||||
db.session.add(new_item)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"success": True, "id": new_item.id}), 201
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
@main.route("/api/inventory/<int:id>", methods=["PUT"])
|
||||
def update_inventory_item(id):
|
||||
try:
|
||||
data = request.get_json(force=True)
|
||||
item = db.session.query(Inventory).get(id)
|
||||
|
||||
if not item:
|
||||
return jsonify({"success": False, "error": f"Inventory item with ID {id} not found."}), 404
|
||||
|
||||
item.timestamp = datetime.datetime.fromisoformat(data.get("timestamp")) if data.get("timestamp") else item.timestamp
|
||||
item.condition = data.get("condition", item.condition)
|
||||
item.type_id = data.get("type_id", item.type_id)
|
||||
item.name = data.get("name", item.name)
|
||||
item.serial = data.get("serial", item.serial)
|
||||
item.model = data.get("model", item.model)
|
||||
item.notes = data.get("notes", item.notes)
|
||||
item.owner_id = data.get("owner_id", item.owner_id)
|
||||
item.brand_id = data.get("brand_id", item.brand_id)
|
||||
item.location_id = data.get("location_id", item.location_id)
|
||||
item.barcode = data.get("barcode", item.barcode)
|
||||
item.shared = bool(data.get("shared", item.shared))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"success": True, "id": item.id}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
@main.route("/api/inventory/<int:id>", methods=["DELETE"])
|
||||
def delete_inventory_item(id):
|
||||
try:
|
||||
item = db.session.query(Inventory).get(id)
|
||||
|
||||
if not item:
|
||||
return jsonify({"success": False, "error": f"Item with ID {id} not found"}), 404
|
||||
|
||||
db.session.delete(item)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"success": True}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
@main.route("/api/inventory/export", methods=["POST"])
|
||||
def get_inventory_csv():
|
||||
def export_value(item, col):
|
||||
try:
|
||||
match col:
|
||||
case "brand":
|
||||
return item.brand.name
|
||||
case "location":
|
||||
return item.location.identifier
|
||||
case "owner":
|
||||
return item.owner.identifier
|
||||
case "type":
|
||||
return item.device_type.description
|
||||
case _:
|
||||
return getattr(item, col, "")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
data = request.get_json()
|
||||
ids = data.get('ids', [])
|
||||
|
||||
if not ids:
|
||||
return jsonify({"success": False, "error": "No IDs provided"}), 400
|
||||
|
||||
rows = eager_load_inventory_relationships(db.session.query(Inventory).filter(Inventory.id.in_(ids))).all()
|
||||
|
||||
columns = [
|
||||
"id",
|
||||
"timestamp",
|
||||
"condition",
|
||||
"type",
|
||||
"name",
|
||||
"serial",
|
||||
"model",
|
||||
"notes",
|
||||
"owner",
|
||||
"brand",
|
||||
"location",
|
||||
"barcode",
|
||||
"shared"
|
||||
]
|
||||
|
||||
return make_csv(export_value, columns, rows)
|
||||
|
||||
@main.route("/inventory_available")
|
||||
def inventory_available():
|
||||
query = eager_load_inventory_relationships(db.session.query(Inventory).filter(Inventory.condition == "Working"))
|
||||
|
||||
inventory = query.all()
|
||||
inventory = sorted(inventory, key=lambda i: i.identifier)
|
||||
|
||||
return render_template(
|
||||
"table.html",
|
||||
title = "Available Inventory",
|
||||
header=inventory_headers,
|
||||
rows=[{"id": item.id, "cells": [row_fn(item) for row_fn in inventory_headers.values()]} for item in inventory],
|
||||
entry_route = 'inventory_item'
|
||||
)
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
from flask import request, redirect, url_for, render_template
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from . import main
|
||||
from .helpers import inventory_headers, user_headers, worklog_headers
|
||||
from .. import db
|
||||
from ..models import Inventory, User, WorkLog
|
||||
from ..utils.load import eager_load_inventory_relationships, eager_load_user_relationships, eager_load_worklog_relationships
|
||||
|
||||
@main.route("/search")
|
||||
def search():
|
||||
query = request.args.get('q', '').strip()
|
||||
|
||||
if not query:
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
InventoryAlias = aliased(Inventory)
|
||||
UserAlias = aliased(User)
|
||||
|
||||
inventory_query = eager_load_inventory_relationships(db.session.query(Inventory).join(UserAlias, Inventory.owner)).filter(
|
||||
or_(
|
||||
Inventory.name.ilike(f"%{query}%"),
|
||||
Inventory.serial.ilike(f"%{query}%"),
|
||||
Inventory.barcode.ilike(f"%{query}%"),
|
||||
Inventory.notes.ilike(f"%{query}%"),
|
||||
UserAlias.first_name.ilike(f"%{query}%"),
|
||||
UserAlias.last_name.ilike(f"%{query}%"),
|
||||
UserAlias.title.ilike(f"%{query}%")
|
||||
))
|
||||
inventory_results = inventory_query.all()
|
||||
user_query = eager_load_user_relationships(db.session.query(User).outerjoin(UserAlias, User.supervisor)).filter(
|
||||
or_(
|
||||
User.first_name.ilike(f"%{query}%"),
|
||||
User.last_name.ilike(f"%{query}%"),
|
||||
User.title.ilike(f"%{query}%"),
|
||||
UserAlias.first_name.ilike(f"%{query}%"),
|
||||
UserAlias.last_name.ilike(f"%{query}%"),
|
||||
UserAlias.title.ilike(f"%{query}%")
|
||||
))
|
||||
user_results = user_query.all()
|
||||
worklog_query = eager_load_worklog_relationships(db.session.query(WorkLog).join(UserAlias, WorkLog.contact).join(InventoryAlias, WorkLog.work_item)).filter(
|
||||
or_(
|
||||
WorkLog.notes.ilike(f"%{query}%"),
|
||||
UserAlias.first_name.ilike(f"%{query}%"),
|
||||
UserAlias.last_name.ilike(f"%{query}%"),
|
||||
UserAlias.title.ilike(f"%{query}%"),
|
||||
InventoryAlias.name.ilike(f"%{query}%"),
|
||||
InventoryAlias.serial.ilike(f"%{query}%"),
|
||||
InventoryAlias.barcode.ilike(f"%{query}%")
|
||||
))
|
||||
worklog_results = worklog_query.all()
|
||||
|
||||
results = {
|
||||
'inventory': {
|
||||
'results': inventory_query,
|
||||
'headers': inventory_headers,
|
||||
'rows': [{"id": item.id, "cells": [fn(item) for fn in inventory_headers.values()]} for item in inventory_results]
|
||||
},
|
||||
'users': {
|
||||
'results': user_query,
|
||||
'headers': user_headers,
|
||||
'rows': [{"id": user.id, "cells": [fn(user) for fn in user_headers.values()]} for user in user_results]
|
||||
},
|
||||
'worklog': {
|
||||
'results': worklog_query,
|
||||
'headers': worklog_headers,
|
||||
'rows': [{"id": log.id, "cells": [fn(log) for fn in worklog_headers.values()]} for log in worklog_results]
|
||||
}
|
||||
}
|
||||
|
||||
return render_template('search.html', title=f"Database Search ({query})" if query else "Database Search", results=results, query=query)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from flask import render_template
|
||||
|
||||
from . import main
|
||||
from .. import db
|
||||
from ..models import Image
|
||||
from ..utils.load import chunk_list
|
||||
|
||||
@main.route('/settings')
|
||||
def settings():
|
||||
images = chunk_list(db.session.query(Image).order_by(Image.timestamp).all(), 6)
|
||||
|
||||
return render_template('settings.html',
|
||||
title="Settings",
|
||||
image_list=images
|
||||
)
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
import base64
|
||||
import csv
|
||||
import io
|
||||
|
||||
from flask import render_template, request, jsonify
|
||||
|
||||
from . import main
|
||||
from .helpers import ACTIVE_STATUSES, user_headers, inventory_headers, worklog_headers, make_csv
|
||||
from .. import db
|
||||
from ..utils.load import eager_load_user_relationships, eager_load_room_relationships, eager_load_inventory_relationships, eager_load_worklog_relationships
|
||||
from ..models import User, Room, Inventory, WorkLog
|
||||
|
||||
@main.route("/users")
|
||||
def list_users():
|
||||
return render_template(
|
||||
'table.html',
|
||||
header = user_headers,
|
||||
model_name = 'user',
|
||||
title = "Users",
|
||||
entry_route = 'user_item',
|
||||
csv_route = 'user',
|
||||
fields = ['last_name', 'first_name', 'title', 'supervisor.identifier', 'location.identifier', 'staff', 'active'],
|
||||
)
|
||||
|
||||
@main.route("/user/<id>")
|
||||
def user_item(id):
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
return render_template('error.html', title='Bad ID', message='ID must be an integer.', endpoint='user_item', endpoint_args={'id': -1})
|
||||
|
||||
users_query = db.session.query(User).order_by(User.first_name, User.last_name)
|
||||
users = eager_load_user_relationships(users_query).all()
|
||||
user = next((u for u in users if u.id == id), None)
|
||||
rooms_query = db.session.query(Room)
|
||||
rooms = eager_load_room_relationships(rooms_query).all()
|
||||
inventory_query = (
|
||||
eager_load_inventory_relationships(db.session.query(Inventory))
|
||||
.filter(Inventory.owner_id == id) # type: ignore
|
||||
.filter(Inventory.condition.in_(ACTIVE_STATUSES))
|
||||
)
|
||||
|
||||
inventory = inventory_query.all()
|
||||
filtered_inventory_headers = {k: v for k, v in inventory_headers.items() if k not in ['Date Entered', 'Name', 'Serial Number',
|
||||
'Bar Code', 'Condition', 'Owner', 'Notes',
|
||||
'Brand', 'Model', 'Shared?', 'Location']}
|
||||
worklog_query = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.contact_id == id)
|
||||
worklog = worklog_query.order_by(WorkLog.start_time.desc()).all()
|
||||
filtered_worklog_headers = {k: v for k, v in worklog_headers.items() if k not in ['Contact', 'Follow Up?', 'Quick Analysis?']}
|
||||
|
||||
if user:
|
||||
title = f"User Record - {user.identifier}" if user.active else f"User Record - {user.identifier} (Inactive)"
|
||||
else:
|
||||
title = f"User Record - User Not Found"
|
||||
return render_template(
|
||||
'error.html',
|
||||
title=title,
|
||||
message=f"User with id {id} not found!"
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"user.html",
|
||||
title=title,
|
||||
user=user, users=users, rooms=rooms, assets=inventory,
|
||||
inventory_headers=filtered_inventory_headers,
|
||||
inventory_rows=[{"id": item.id, "cells": [fn(item) for fn in filtered_inventory_headers.values()]} for item in inventory],
|
||||
worklog=worklog,
|
||||
worklog_headers=filtered_worklog_headers,
|
||||
worklog_rows=[{"id": log.id, "cells": [fn(log) for fn in filtered_worklog_headers.values()]} for log in worklog]
|
||||
)
|
||||
|
||||
@main.route("/user/<id>/org")
|
||||
def user_org(id):
|
||||
user = eager_load_user_relationships(db.session.query(User).filter(User.id == id).order_by(User.first_name, User.last_name)).first()
|
||||
if not user:
|
||||
return render_template('error.html', title='User Not Found', message=f'User with ID {id} not found.')
|
||||
|
||||
current_user = user
|
||||
org_chart = []
|
||||
while current_user:
|
||||
subordinates = (
|
||||
eager_load_user_relationships(
|
||||
db.session.query(User).filter(User.supervisor_id == current_user.id).order_by(User.first_name, User.last_name)
|
||||
).all()
|
||||
)
|
||||
org_chart.insert(0, {
|
||||
"user": current_user,
|
||||
"subordinates": [subordinate for subordinate in subordinates if subordinate.active and subordinate.staff]
|
||||
})
|
||||
current_user = current_user.supervisor
|
||||
|
||||
return render_template(
|
||||
"user_org.html",
|
||||
user=user,
|
||||
org_chart=org_chart
|
||||
)
|
||||
|
||||
@main.route("/user/new", methods=["GET"])
|
||||
def new_user():
|
||||
rooms = eager_load_room_relationships(db.session.query(Room)).all()
|
||||
users = eager_load_user_relationships(db.session.query(User)).all()
|
||||
|
||||
user = User(
|
||||
active=True
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"user.html",
|
||||
title="New User",
|
||||
user=user,
|
||||
users=users,
|
||||
rooms=rooms
|
||||
)
|
||||
|
||||
@main.route("/api/user", methods=["POST"])
|
||||
def create_user():
|
||||
try:
|
||||
data = request.get_json(force=True)
|
||||
|
||||
new_user = User.from_dict(data)
|
||||
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"success": True, "id": new_user.id}), 201
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
@main.route("/api/user/<int:id>", methods=["PUT"])
|
||||
def update_user(id):
|
||||
try:
|
||||
data = request.get_json(force=True)
|
||||
user = db.session.query(User).get(id)
|
||||
|
||||
if not user:
|
||||
return jsonify({"success": False, "error": f"User with ID {id} not found."}), 404
|
||||
|
||||
user.staff = bool(data.get("staff", user.staff))
|
||||
user.active = bool(data.get("active", user.active))
|
||||
user.last_name = data.get("last_name", user.last_name)
|
||||
user.first_name = data.get("first_name", user.first_name)
|
||||
user.title = data.get("title", user.title)
|
||||
user.location_id = data.get("location_id", user.location_id)
|
||||
user.supervisor_id = data.get("supervisor_id", user.supervisor_id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"success": True, "id": user.id}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
@main.route("/api/user/export", methods=["POST"])
|
||||
def get_user_csv():
|
||||
def export_value(user, col):
|
||||
try:
|
||||
match col:
|
||||
case "location":
|
||||
return user.location.identifier
|
||||
case "supervisor":
|
||||
return user.supervisor.identifier
|
||||
case _:
|
||||
return getattr(user, col, "")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
data = request.get_json()
|
||||
ids = data.get('ids', [])
|
||||
|
||||
if not ids:
|
||||
return jsonify({"success": False, "error": "No IDs provided"}), 400
|
||||
|
||||
rows = eager_load_user_relationships(db.session.query(User).filter(User.id.in_(ids))).all()
|
||||
|
||||
columns = [
|
||||
"id",
|
||||
"staff",
|
||||
"active",
|
||||
"last_name",
|
||||
"first_name",
|
||||
"title",
|
||||
"location",
|
||||
"supervisor"
|
||||
]
|
||||
|
||||
return make_csv(export_value, columns, rows)
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import base64
|
||||
import csv
|
||||
import datetime
|
||||
import io
|
||||
|
||||
from flask import request, render_template, jsonify
|
||||
|
||||
from . import main
|
||||
from .helpers import worklog_headers, make_csv
|
||||
from .. import db
|
||||
from ..models import WorkLog, User, Inventory, WorkNote
|
||||
from ..utils.load import eager_load_worklog_relationships, eager_load_user_relationships, eager_load_inventory_relationships
|
||||
|
||||
@main.route("/worklog")
|
||||
def list_worklog():
|
||||
query = eager_load_worklog_relationships(db.session.query(WorkLog))
|
||||
return render_template(
|
||||
'table.html',
|
||||
header=worklog_headers,
|
||||
model_name='worklog',
|
||||
title="Work Log",
|
||||
fields = ['contact.identifier', 'work_item.identifier', 'start_time', 'end_time', 'complete', 'followup', 'analysis'],
|
||||
entry_route='worklog_item',
|
||||
csv_route='worklog'
|
||||
)
|
||||
|
||||
@main.route("/worklog/<id>")
|
||||
def worklog_item(id):
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
return render_template('error.html', title='Bad ID', message='ID must be an integer.', endpoint='worklog_item', endpoint_args={'id': -1})
|
||||
|
||||
log = eager_load_worklog_relationships(db.session.query(WorkLog)).get(id)
|
||||
user_query = db.session.query(User).order_by(User.first_name)
|
||||
users = eager_load_user_relationships(user_query).all()
|
||||
item_query = db.session.query(Inventory)
|
||||
items = eager_load_inventory_relationships(item_query).all()
|
||||
items = sorted(items, key=lambda i: i.identifier)
|
||||
|
||||
if log:
|
||||
title = f'Work Log - Entry #{id}'
|
||||
else:
|
||||
title = "Work Log - Entry Not Found"
|
||||
return render_template(
|
||||
'error.html',
|
||||
title=title,
|
||||
message=f"The work log with ID {id} is not found!"
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"worklog.html",
|
||||
title=title,
|
||||
log=log,
|
||||
users=users,
|
||||
items=items
|
||||
)
|
||||
|
||||
@main.route("/worklog_item/new", methods=["GET"])
|
||||
def new_worklog():
|
||||
items = eager_load_inventory_relationships(db.session.query(Inventory)).all()
|
||||
users = eager_load_user_relationships(db.session.query(User).order_by(User.first_name)).all()
|
||||
|
||||
items = sorted(items, key=lambda i: i.identifier)
|
||||
|
||||
log = WorkLog(
|
||||
start_time=datetime.datetime.now(),
|
||||
followup=True
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"worklog.html",
|
||||
title="New Entry",
|
||||
log=log,
|
||||
users=users,
|
||||
items=items
|
||||
)
|
||||
|
||||
@main.route("/api/worklog", methods=["POST"])
|
||||
def create_worklog():
|
||||
try:
|
||||
data = request.get_json(force=True)
|
||||
|
||||
new_worklog = WorkLog.from_dict(data)
|
||||
|
||||
db.session.add(new_worklog)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"success": True, "id": new_worklog.id}), 201
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
@main.route("/api/worklog/<int:id>", methods=["PUT"])
|
||||
def update_worklog(id):
|
||||
try:
|
||||
data = request.get_json(force=True)
|
||||
log = eager_load_worklog_relationships(db.session.query(WorkLog)).get(id)
|
||||
|
||||
if not log:
|
||||
return jsonify({"success": False, "error": f"Work Log with ID {id} not found."}), 404
|
||||
|
||||
log.start_time = datetime.datetime.fromisoformat(data.get("start_time")) if data.get("start_time") else log.start_time
|
||||
log.end_time = datetime.datetime.fromisoformat(data.get("end_time")) if data.get("end_time") else log.end_time
|
||||
log.complete = bool(data.get("complete", log.complete))
|
||||
log.followup = bool(data.get("followup", log.followup))
|
||||
log.analysis = bool(data.get("analysis", log.analysis))
|
||||
log.contact_id = data.get("contact_id", log.contact_id)
|
||||
log.work_item_id = data.get("work_item_id", log.work_item_id)
|
||||
existing = {str(note.id): note for note in log.updates}
|
||||
incoming = data.get("updates", [])
|
||||
new_updates = []
|
||||
|
||||
for note_data in incoming:
|
||||
if isinstance(note_data, dict):
|
||||
if "id" in note_data and str(note_data["id"]) in existing:
|
||||
note = existing[str(note_data["id"])]
|
||||
note.content = note_data.get("content", note.content)
|
||||
new_updates.append(note)
|
||||
elif "content" in note_data:
|
||||
new_updates.append(WorkNote(content=note_data["content"]))
|
||||
|
||||
log.updates[:] = new_updates # This replaces in-place
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"success": True, "id": log.id}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
@main.route("/api/worklog/<int:id>", methods=["DELETE"])
|
||||
def delete_worklog(id):
|
||||
try:
|
||||
log = db.session.query(WorkLog).get(id)
|
||||
|
||||
if not log:
|
||||
return jsonify({"success": False, "errpr": f"Item with ID {id} not found!"}), 404
|
||||
|
||||
db.session.delete(log)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"success": True}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
@main.route("/api/worklog/export", methods=["POST"])
|
||||
def get_worklog_csv():
|
||||
def export_value(log, col):
|
||||
try:
|
||||
match col:
|
||||
case "contact":
|
||||
return log.contact.identifier
|
||||
case "work_item":
|
||||
return log.work_item.identifier
|
||||
case "latest_update":
|
||||
if log.updates:
|
||||
return log.updates[-1].content
|
||||
return ""
|
||||
case _:
|
||||
return getattr(log, col, "")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
data = request.get_json()
|
||||
ids = data.get('ids', [])
|
||||
|
||||
if not ids:
|
||||
return jsonify({"success": False, "error": "No IDs provided"}), 400
|
||||
|
||||
rows = eager_load_worklog_relationships(db.session.query(WorkLog).filter(WorkLog.id.in_(ids))).all()
|
||||
|
||||
columns = [
|
||||
"id",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"complete",
|
||||
"followup",
|
||||
"contact",
|
||||
"work_item",
|
||||
"analysis",
|
||||
"latest_update"
|
||||
]
|
||||
|
||||
return make_csv(export_value, columns, rows)
|
||||
|
||||
# return jsonify({
|
||||
# "success": True,
|
||||
# "csv": base64.b64encode(csv_string.encode()).decode(),
|
||||
# "count": len(rows)
|
||||
# })
|
||||
Loading…
Add table
Add a link
Reference in a new issue