first test with local claude code

This commit is contained in:
James Devine 2026-02-25 14:28:53 +00:00
commit 34daffbbee
5 changed files with 574 additions and 0 deletions

285
app.py Normal file
View file

@ -0,0 +1,285 @@
import os
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, flash, session
from datetime import datetime
import sqlite3
import uuid
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
DB_PATH = '/home/jimmy/inspectiontool/db.sqlite3'
# Initialize database
def init_db():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS inspections (id INTEGER PRIMARY KEY AUTOINCREMENT, inspector_name TEXT NOT NULL, location TEXT NOT NULL, inspection_date TEXT NOT NULL, installation_name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP)")
cursor.execute("CREATE TABLE IF NOT EXISTS checklist_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)")
cursor.execute("INSERT OR IGNORE INTO checklist_items (name) VALUES ('Check electrical wiring'), ('Check plumbing'), ('Check fire safety'), ('Check electrical safety'), ('Check water pressure'), ('Check drainage'), ('Check structural integrity'), ('Check emergency exits'), ('Check fire extinguishers'), ('Check ventilation')")
cursor.execute("CREATE TABLE IF NOT EXISTS inspection_photos (id INTEGER PRIMARY KEY AUTOINCREMENT, inspection_id INTEGER NOT NULL, photo_path TEXT NOT NULL, comment TEXT, resolved INTEGER DEFAULT 0, uploaded_at TEXT DEFAULT CURRENT_TIMESTAMP)")
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', logo_path TEXT)")
# Add default admin user
hashed_password = generate_password_hash('admin')
cursor.execute("INSERT OR IGNORE INTO users (username, password_hash, role) VALUES ('admin', ?, 'admin')", (hashed_password,))
conn.commit()
conn.close()
init_db()
# Set up static folders
app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'static', 'uploads')
app.config['PDF_FOLDER'] = os.path.join(app.root_path, 'static', 'pdf')
app.config['LOGO_FOLDER'] = os.path.join(app.root_path, 'static', 'logo')
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['PDF_FOLDER'], exist_ok=True)
os.makedirs(app.config['LOGO_FOLDER'], exist_ok=True)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
user = cursor.fetchone()
if user and check_password_hash(user[2], password):
session['user_id'] = user[0]
session['username'] = user[1]
session['role'] = user[3]
# Store logo path for current user in session
cursor.execute("SELECT logo_path FROM users WHERE username = ?", (username,))
logo_path = cursor.fetchone()[0] if cursor.fetchone() else None
session['user_logo_path'] = logo_path
return redirect(url_for('index'))
else:
flash('Invalid username or password')
return render_template('login.html')
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
role = request.form.get('role', 'user')
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
if cursor.fetchone():
flash('Username already exists')
return redirect(url_for('register'))
hashed_password = generate_password_hash(password)
cursor.execute("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)", (username, hashed_password, role))
conn.commit()
conn.close()
flash('Registration successful!')
return redirect(url_for('login'))
return render_template('register.html')
@app.route('/logout')
def logout():
session.pop('user_id', None)
session.pop('username', None)
session.pop('role', None)
session.pop('user_logo_path', None)
return redirect(url_for('index'))
@app.route('/admin')
def admin():
if 'user_id' not in session:
return redirect(url_for('login'))
return render_template('admin.html')
@app.route('/admin/users')
def admin_users():
if 'user_id' not in session or session['role'] != 'admin':
return redirect(url_for('index'))
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
users = cursor.fetchall()
conn.close()
return render_template('admin_users.html', users=users)
@app.route('/admin/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
if 'user_id' not in session or session['role'] != 'admin':
return redirect(url_for('index'))
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
conn.commit()
conn.close()
return redirect(url_for('admin_users'))
@app.route('/admin/logo', methods=['GET', 'POST'])
def admin_logo():
if 'user_id' not in session or session['role'] != 'admin':
return redirect(url_for('index'))
if request.method == 'POST':
if 'logo' in request.files:
file = request.files['logo']
if file.filename == '':
flash('No selected file')
return redirect(url_for('admin_users'))
filename = f'logo_{uuid.uuid4().hex}.png'
logo_path = os.path.join(app.config['LOGO_FOLDER'], filename)
file.save(logo_path)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("UPDATE users SET logo_path = ? WHERE username = 'admin'", (logo_path,))
conn.commit()
conn.close()
flash('Logo uploaded successfully')
return redirect(url_for('admin_users'))
return render_template('admin_logo.html')
@app.route('/')
def index():
conn = sqlite3.connect(DB_PATH)
inspections = conn.execute("SELECT * FROM inspections ORDER BY inspection_date DESC").fetchall()
conn.close()
return render_template('index.html', inspections=inspections)
@app.route('/new', methods=['GET', 'POST'])
def new_inspection():
if 'user_id' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
inspector_name = request.form['inspector_name']
location = request.form['location']
inspection_date = request.form['inspection_date']
installation_name = request.form['installation_name']
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("INSERT INTO inspections (inspector_name, location, inspection_date, installation_name) VALUES (?, ?, ?, ?)",
(inspector_name, location, inspection_date, installation_name))
conn.commit()
conn.close()
return redirect(url_for('view_inspection', id=cursor.lastrowid))
return render_template('new_inspection.html')
@app.route('/view/<int:id>')
def view_inspection(id):
if 'user_id' not in session:
return redirect(url_for('login'))
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT * FROM inspections WHERE id = ?", (id,))
inspection = cursor.fetchone()
cursor.execute("SELECT * FROM inspection_photos WHERE inspection_id = ?", (id,))
photos = cursor.fetchall()
cursor.execute("SELECT * FROM checklist_items")
checklist_items = cursor.fetchall()
conn.close()
return render_template('inspection.html', inspection=inspection, photos=photos, checklist_items=checklist_items)
@app.route('/upload/<int:id>', methods=['POST'])
def upload_photo(id):
if 'user_id' not in session:
return redirect(url_for('login'))
if 'photo' not in request.files:
flash('No file part')
return redirect(url_for('view_inspection', id=id))
file = request.files['photo']
if file.filename == '':
flash('No selected file')
return redirect(url_for('view_inspection', id=id))
filename = f'photo_{uuid.uuid4().hex}.jpg'
photo_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(photo_path)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("INSERT INTO inspection_photos (inspection_id, photo_path, comment) VALUES (?, ?, ?)",
(id, filename, request.form.get('comment', '')))
conn.commit()
conn.close()
flash('Photo uploaded successfully')
return redirect(url_for('view_inspection', id=id))
@app.route('/generate_pdf/<int:id>', methods=['GET'])
def generate_pdf(id):
if 'user_id' not in session:
return redirect(url_for('login'))
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT * FROM inspections WHERE id = ?", (id,))
inspection = cursor.fetchone()
cursor.execute("SELECT * FROM inspection_photos WHERE inspection_id = ?", (id,))
photos = cursor.fetchall()
cursor.execute("SELECT * FROM checklist_items")
checklist_items = cursor.fetchall()
conn.close()
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib import utils
from reportlab.lib.utils import ImageReader
pdf_path = os.path.join(app.config['PDF_FOLDER'], f'report_{id}.pdf')
doc = SimpleDocTemplate(pdf_path, pagesize=letter)
styles = getSampleStyleSheet()
elements = []
elements.append(Paragraph(f"Inspection Report - {inspection[1]}", styles['Heading1']))
elements.append(Spacer(1, 12))
elements.append(Paragraph(f"Inspector: {inspection[1]}", styles['Normal']))
elements.append(Paragraph(f"Location: {inspection[2]}", styles['Normal']))
elements.append(Paragraph(f"Date: {inspection[3]}", styles['Normal']))
elements.append(Paragraph(f"Installation: {inspection[4]}", styles['Normal']))
elements.append(Spacer(1, 12))
elements.append(Paragraph("Checklist", styles['Heading2']))
for item in checklist_items:
elements.append(Paragraph(f"- {item[1]}", styles['Normal']))
elements.append(Spacer(1, 12))
elements.append(Paragraph("Photos", styles['Heading2']))
for photo in photos:
elements.append(Paragraph(f"Photo {photo[0]}: {photo[2]}", styles['Normal']))
elements.append(Spacer(1, 4))
# Add logo if available
logo_path = session.get('user_logo_path')
if logo_path:
try:
logo = ImageReader(logo_path)
logo_width = 100
logo_height = 100
elements.append(Paragraph(f"Logo: {logo_path}", styles['Normal']))
# Add logo to PDF
doc.build(elements)
except Exception as e:
print(f"Error adding logo: {e}")
else:
doc.build(elements)
return send_from_directory(app.config['PDF_FOLDER'], f'report_{id}.pdf', as_attachment=True)
if __name__ == '__main__':
app.run(debug=True)

44
db_schema.sql Normal file
View file

@ -0,0 +1,44 @@
CREATE TABLE inspections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
inspector_name TEXT NOT NULL,
location TEXT NOT NULL,
inspection_date TEXT NOT NULL,
installation_name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE checklist_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
INSERT INTO checklist_items (name) VALUES
('Check electrical wiring'),
('Check plumbing'),
('Check fire safety'),
('Check electrical safety'),
('Check water pressure'),
('Check drainage'),
('Check structural integrity'),
('Check emergency exits'),
('Check fire extinguishers'),
('Check ventilation');
CREATE TABLE inspection_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
inspection_id INTEGER NOT NULL,
photo_path TEXT NOT NULL,
comment TEXT,
resolved INTEGER DEFAULT 0,
uploaded_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (inspection_id) REFERENCES inspections(id)
);
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
logo_path TEXT
);

44
templates/index.html Normal file
View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>Inspection Reports</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
a { text-decoration: none; color: #0066cc; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>Inspection Reports</h1>
<a href="{{ url_for('new_inspection') }}">Create New Inspection</a>
<table>
<thead>
<tr>
<th>ID</th>
<th>Inspector</th>
<th>Location</th>
<th>Date</th>
<th>Installation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for inspection in inspections %}
<tr>
<td>{{ inspection.id }}</td>
<td>{{ inspection.inspector_name }}</td>
<td>{{ inspection.location }}</td>
<td>{{ inspection.inspection_date }}</td>
<td>{{ inspection.installation_name }}</td>
<td>
<a href="{{ url_for('view_inspection', id=inspection.id) }}">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>

107
templates/inspection.html Normal file
View file

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<head>
<title>Inspection Report - {{ inspection.inspector_name }}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.header { border-bottom: 2px solid #000; padding-bottom: 10px; margin-bottom: 20px; }
.header h1 { color: #0066cc; }
.report-section { margin-bottom: 20px; }
.checklist { margin-left: 20px; }
.checklist-item { margin-bottom: 5px; }
.photo-section { margin-top: 20px; }
.photo-container { border: 1px solid #ddd; padding: 10px; margin-bottom: 10px; }
.photo { max-width: 300px; height: auto; }
.photo-comment { margin-top: 5px; font-size: 0.9em; }
.checkbox-container { display: flex; align-items: center; }
.checkbox { margin-right: 5px; }
.summary-section { margin-top: 20px; }
.summary-buttons { display: flex; gap: 10px; margin-top: 10px; }
.btn { background: #0066cc; color: white; padding: 5px 10px; border: none; cursor: pointer; }
.btn:hover { background: #0055aa; }
.download-btn { background: #333; color: white; padding: 5px 10px; border: none; cursor: pointer; }
.download-btn:hover { background: #555; }
.update-note { color: #888; margin-top: 5px; }
</style>
</head>
<body>
<div class="header">
<h1>Inspection Report</h1>
<p><strong>Inspector:</strong> {{ inspection.inspector_name }}<br>
<strong>Location:</strong> {{ inspection.location }}<br>
<strong>Date:</strong> {{ inspection.inspection_date }}<br>
<strong>Installation:</strong> {{ inspection.installation_name }}</p>
</div>
<div class="report-section">
<h2>Checklist</h2>
<ul class="checklist">
{% for item in checklist_items %}
<li class="checklist-item">
<input type="checkbox" class="checklist-item-checkbox" data-item-id="{{ item.id }}" data-inspection-id="{{ inspection.id }}"> {{ item.name }}
</li>
{% endfor %}
</ul>
</div>
<div class="report-section">
<h2>Photos</h2>
<div class="photo-section">
{% for photo in photos %}
<div class="photo-container">
<img src="{{ url_for('static', filename=photo.photo_path) }}" class="photo" alt="Photo {{ loop.index }}">
<div class="photo-comment">{{ photo.comment or 'No comment' }}</div>
<div class="checkbox-container">
<input type="checkbox" class="resolved-checkbox" data-photo-id="{{ photo.id }}" data-inspection-id="{{ inspection.id }}" data-resolved="{{ photo.resolved }}">
<label>Resolved</label>
</div>
<div class="update-note">Updated: {{ photo.uploaded_at }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="report-section">
<h2>Summary</h2>
<textarea name="summary" rows="4" cols="50" placeholder="Enter summary text here">{{ inspection.summary or '' }}</textarea>
<div class="summary-buttons">
<button class="btn" onclick="toggleStatus('ok')">Ok for operation</button>
<button class="btn" onclick="toggleStatus('minor')">Minor remarks</button>
<button class="btn" onclick="toggleStatus('major')">Major remarks</button>
</div>
</div>
<div class="report-section">
<h2>Download Report</h2>
<a href="{{ url_for('generate_pdf', id=inspection.id) }}" class="download-btn">Download as PDF</a>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const checklistItems = document.querySelectorAll('.checklist-item-checkbox');
const photoCheckboxes = document.querySelectorAll('.resolved-checkbox');
checklistItems.forEach(item => {
item.addEventListener('change', function() {
const inspectionId = this.getAttribute('data-inspection-id');
const itemId = this.getAttribute('data-item-id');
// Update the database here (implementation would go here)
});
});
photoCheckboxes.forEach(box => {
box.addEventListener('change', function() {
const photoId = this.getAttribute('data-photo-id');
const inspectionId = this.getAttribute('data-inspection-id');
const resolved = this.checked ? 1 : 0;
// Update the database here (implementation would go here)
});
});
function toggleStatus(status) {
// Implementation for toggling status
}
});
</script>
</body>
</html>

94
tests/test_app.py Normal file
View file

@ -0,0 +1,94 @@
import os
import unittest
from unittest.mock import patch
from flask import json
from app import app
import sqlite3
from io import BytesIO
class TestApp(unittest.TestCase):
def setUp(self):
# Use a temporary test database
self.test_db_path = os.path.join(os.getcwd(), 'test.db')
app.config['TESTING'] = True
app.config['DB_PATH'] = self.test_db_path
self.client = app.test_client()
# Initialize the database
conn = sqlite3.connect(self.test_db_path)
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS inspections (id INTEGER PRIMARY KEY AUTOINCREMENT, inspector_name TEXT NOT NULL, location TEXT NOT NULL, inspection_date TEXT NOT NULL, installation_name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP)")
cursor.execute("CREATE TABLE IF NOT EXISTS checklist_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)")
cursor.execute("INSERT OR IGNORE INTO checklist_items (name) VALUES ('Check electrical wiring'), ('Check plumbing'), ('Check fire safety'), ('Check electrical safety'), ('Check water pressure'), ('Check drainage'), ('Check structural integrity'), ('Check emergency exits'), ('Check fire extinguishers'), ('Check ventilation')")
cursor.execute("CREATE TABLE IF NOT EXISTS inspection_photos (id INTEGER PRIMARY KEY AUTOINCREMENT, inspection_id INTEGER NOT NULL, photo_path TEXT NOT NULL, comment TEXT, resolved INTEGER DEFAULT 0, uploaded_at TEXT DEFAULT CURRENT_TIMESTAMP)")
conn.commit()
conn.close()
def tearDown(self):
# Clean up the test database
if os.path.exists(self.test_db_path):
os.remove(self.test_db_path)
def test_create_inspection(self):
response = self.client.post('/new', data={
'inspector_name': 'Test Inspector',
'location': 'Test Location',
'inspection_date': '2026-02-23',
'installation_name': 'Test Installation'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertIn(b'Test Inspector', response.data)
self.assertIn(b'Test Location', response.data)
self.assertIn(b'Test Installation', response.data)
def test_view_inspection(self):
# Create an inspection
response = self.client.post('/new', data={
'inspector_name': 'Test Inspector',
'location': 'Test Location',
'inspection_date': '2026-02-23',
'installation_name': 'Test Installation'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
# View the inspection
response = self.client.get('/view/1')
self.assertEqual(response.status_code, 200)
self.assertIn(b'Inspection Report', response.data)
def test_upload_photo(self):
# Create an inspection
response = self.client.post('/new', data={
'inspector_name': 'Test Inspector',
'location': 'Test Location',
'inspection_date': '2026-02-23',
'installation_name': 'Test Installation'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
# Upload a photo
with BytesIO(b'test photo data') as photo:
response = self.client.post('/upload/1', data={
'photo': (photo, 'test.jpg'),
'comment': 'Test comment'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertIn(b'Photo uploaded successfully', response.data)
def test_generate_pdf(self):
# Create an inspection
response = self.client.post('/new', data={
'inspector_name': 'Test Inspector',
'location': 'Test Location',
'inspection_date': '2026-02-23',
'installation_name': 'Test Installation'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
# Generate PDF
response = self.client.get('/generate_pdf/1')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content_type, 'application/pdf')
if __name__ == '__main__':
unittest.main()