Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5a4adfe
test
Sep 5, 2025
ed84744
added back gen_key.sh script for dev
Sep 5, 2025
2661ddc
fixed run.sh
Sep 5, 2025
bf8c23d
mkdir -p for run.sh
Sep 5, 2025
3402d6c
added .venv creation prompt and creation into the run.sh
Sep 13, 2025
5dd6176
fixed run.sh to properly handle .venv creation and key generation
Sep 13, 2025
374acea
updated ip and added ip address verification function
Sep 13, 2025
f74f348
checks for uncompatable .venv and propmts overwrite
Sep 13, 2025
bc9b049
added manage_useres.html page
Sep 20, 2025
2e6fab0
added other fixes
Sep 20, 2025
1308fa1
small button changes
Sep 21, 2025
47e429b
fixed https requirement for client hash
Sep 24, 2025
a9f28a5
switched from conda to normal venv in run.sh and fixed the dependance…
Sep 25, 2025
3f61eed
added login.js
Oct 2, 2025
37bae36
changed delimiter in printcsv method to account for new entries with …
Oct 22, 2025
1aedfb7
made fail reason expandable
Oct 22, 2025
5482ab0
made truncated / expanded cells better
Oct 22, 2025
42487a2
expansion now expands all cells in column not just single cell
Oct 22, 2025
97652e8
sticky cm column no heading
Oct 23, 2025
4d9c227
sticky column entires and heading, need to fix transparency
Oct 23, 2025
a02c76f
finished sticky column
Oct 24, 2025
9cce98c
removing unused div
Oct 24, 2025
df1649f
colors on sticky column now
Oct 24, 2025
60e02b2
added show column filters
Oct 24, 2025
d79aafe
added column sorting feature
Oct 26, 2025
20657d4
fixed text clipping
Oct 26, 2025
7a511d1
deticated view
Oct 29, 2025
10de4eb
added entry_detail.html
Oct 29, 2025
72ca836
fixed autorefresh
Oct 29, 2025
dca50b5
fixed reset visibility filter
Oct 29, 2025
162777f
fixed column visibility issues
Oct 29, 2025
197cf59
fixed csv method i think
Nov 8, 2025
569e311
csv broken per expanded test view fixed for id not serial
loganprosser Dec 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion admin_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ def list_admin_commands():

return render_template('admin/admin_commands.html', commands=commands)


# data generation commands - old as of 7/21 - not necessasary for time being

@admin_bp.route('/add_dummy_entry')
Expand Down
127 changes: 103 additions & 24 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@
# TODO fix formatting of code and make constantly repeated code into helper functions?
# TODO block using back button on forms?
# TODO have files visible in js for form.html
# TODO make a @loginrequired

import os
import io
import csv
import json
import re
from datetime import datetime
from flask import Flask, render_template, request, redirect, url_for, session, send_file, flash, send_from_directory, abort
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy import and_


from models import db, User, TestEntry
from form_config import FORMS_NON_DICT
Expand All @@ -44,6 +49,11 @@
'users': f"sqlite:///{os.path.join(data_path, 'users.db')}"
}

app.config['SESSION_COOKIE_SECURE'] = False # allow HTTP
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # 'None' on HTTP will be rejected
app.config['SESSION_COOKIE_HTTPONLY'] = True


db.init_app(app)

app.register_blueprint(admin_bp)
Expand Down Expand Up @@ -85,6 +95,11 @@ def register():
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
print("DEBUG /login payload:",
"js_ready=", request.form.get('js_ready'),
"keys=", list(request.form.keys()),
"password.len=", len(request.form.get('password', "")))
# continue with your existing logic...
username = request.form['username'].strip()
password = request.form['password']

Expand All @@ -107,14 +122,12 @@ def form_complete():

return render_template('form_complete.html')


@app.route('/logout')
def logout():
"""logout route"""
session.pop('user_id', None)
return redirect(url_for('login'))


@app.route('/')
def home():
"""home page route"""
Expand Down Expand Up @@ -452,55 +465,122 @@ def history():

return render_template('history.html', entries=entries, fields=all_fields, show_unique=unique_toggle, now=datetime.now(EASTERN_TZ))

@app.route('/entry/<int:entry_id>')
def entry_detail(entry_id):
"""Expanded view of entry."""
if 'user_id' not in session:
return redirect(url_for('login'))

entry = db.session.get(TestEntry, entry_id)

if not entry:
flash(f"No entry found with ID{entry_id}.", "warning")
return redirect(url_for('history'))

# collect all visible fields (like in /history)
all_fields = []
for form_page in FORMS_NON_DICT:
all_fields.extend([f for f in form_page.fields if getattr(f, "display_history", True)])

return render_template(
"entry_detail.html",
entry=entry,
fields=all_fields,
)

@app.route('/export_csv')
def export_csv():
"""Export all test entries to CSV."""
"""Export only visible (display_history=True) fields to CSV."""
if 'user_id' not in session:
return redirect(url_for('login'))

unique_toggle = request.args.get('unique') == "true"

# Combine all fields from all forms for CSV export
all_fields = []
for single_form in FORMS_NON_DICT:
all_fields.extend(single_form.fields)

unique_toggle = request.args.get('unique') == "true"

# ---------- 1) Only include visible fields ----------
visible_fields = []
for page in FORMS_NON_DICT:
for field in getattr(page, "fields", []) or []:
if getattr(field, "display_history", False):
visible_fields.append(field)

# ---------- 2) Prepare serializer to clean up text ----------
clean_ws = re.compile(r"[\r\n\t]+")
def safe(val):
if val is None:
return ""
if isinstance(val, datetime):
return val.replace(microsecond=0).isoformat(sep=" ")
if isinstance(val, bool):
return "yes" if val else "no"
if isinstance(val, (dict, list)):
return json.dumps(val, ensure_ascii=False, separators=(",", ":"))
return clean_ws.sub(" ", str(val)).strip()

# ---------- 3) Query entries ----------
if unique_toggle:
subquery = (
subq = (
db.session.query(
TestEntry.data["CM_serial"].as_integer().label("cm_serial"),
db.func.max(TestEntry.timestamp).label("latest")
)
.filter(TestEntry.data["CM_serial"].isnot(None))
.group_by(TestEntry.data["CM_serial"].as_integer())
.subquery()
)

entries = (
db.session.query(TestEntry)
.join(subquery, db.and_(
TestEntry.data["CM_serial"].as_integer() == subquery.c.cm_serial,
TestEntry.timestamp == subquery.c.latest
))
.join(
subq,
and_(
TestEntry.data["CM_serial"].as_integer() == subq.c.cm_serial,
TestEntry.timestamp == subq.c.latest
)
)
.order_by(TestEntry.timestamp.desc())
.all()
)
else:
entries = TestEntry.query.order_by(TestEntry.timestamp.desc()).all()

output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['Time', 'User'] + [f.label for f in all_fields] + ['File', "Test Aborted", "Reason Aborted"])
# ---------- 4) Write CSV ----------
output = io.StringIO(newline='')
writer = csv.writer(output, delimiter=',', lineterminator='\n', quoting=csv.QUOTE_MINIMAL)

headers = (
["Time", "User"]
+ [safe(getattr(f, "label", None) or f.name) for f in visible_fields]
+ ["File", "Test Aborted", "Reason Aborted"]
)
writer.writerow(headers)

def username_for(entry):
u = getattr(entry, "user", None)
if u and getattr(u, "username", None):
return u.username
if isinstance(entry.contributors, list) and entry.contributors:
return str(entry.contributors[-1])
return ""

for e in entries:
row = [e.timestamp, e.user.username]
row += [e.data.get(f.name) for f in all_fields]
row += [e.file_name, "yes" if e.failure else "no", e.fail_reason or ""]
data = e.data or {}
row = [safe(e.timestamp), safe(username_for(e))]
for f in visible_fields:
row.append(safe(data.get(f.name)))
row += [safe(e.file_name), "yes" if e.failure else "no", safe(e.fail_reason)]
writer.writerow(row)

output.seek(0)
return send_file(io.BytesIO(output.read().encode()), mimetype='text/csv',
as_attachment=True, download_name='test_results.csv')
csv_bytes = ("\ufeff" + output.getvalue()).encode("utf-8")
return send_file(
io.BytesIO(csv_bytes),
mimetype='text/csv',
as_attachment=True,
download_name='test_results.csv'
)




@app.route('/help')
def help_button():
Expand All @@ -524,7 +604,6 @@ def help_button():

return render_template("help.html", grouped_help_fields=grouped_help_fields)


@app.route('/prod_test_doc')
def prod_test_doc():
"""Bring up Apollo Production Testing Document."""
Expand Down
14 changes: 14 additions & 0 deletions gen_key.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/sh
# Simple script to generate and set FLASK_SECRET_KEY locally for dev
# Run with: . ./gen_key.sh

# Generate a 64-character hex key
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")

# Export it to the current shell environment
export FLASK_SECRET_KEY="$SECRET_KEY"

# Print to confirm
echo "FLASK_SECRET_KEY has been set:"
echo "$FLASK_SECRET_KEY"

Loading