commit 34daffbbee6c596bc90b1e723c48772f3660ba6d Author: pingud98 Date: Wed Feb 25 14:28:53 2026 +0000 first test with local claude code diff --git a/app.py b/app.py new file mode 100644 index 0000000..44bde7f --- /dev/null +++ b/app.py @@ -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/', 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/') +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/', 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/', 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) \ No newline at end of file diff --git a/db_schema.sql b/db_schema.sql new file mode 100644 index 0000000..7b3eaae --- /dev/null +++ b/db_schema.sql @@ -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 +); \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..f4c1318 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,44 @@ + + + + Inspection Reports + + + +

Inspection Reports

+ Create New Inspection + + + + + + + + + + + + + {% for inspection in inspections %} + + + + + + + + + {% endfor %} + +
IDInspectorLocationDateInstallationActions
{{ inspection.id }}{{ inspection.inspector_name }}{{ inspection.location }}{{ inspection.inspection_date }}{{ inspection.installation_name }} + View +
+ + \ No newline at end of file diff --git a/templates/inspection.html b/templates/inspection.html new file mode 100644 index 0000000..d368f0a --- /dev/null +++ b/templates/inspection.html @@ -0,0 +1,107 @@ + + + + Inspection Report - {{ inspection.inspector_name }} + + + +
+

Inspection Report

+

Inspector: {{ inspection.inspector_name }}
+ Location: {{ inspection.location }}
+ Date: {{ inspection.inspection_date }}
+ Installation: {{ inspection.installation_name }}

+
+ +
+

Checklist

+
    + {% for item in checklist_items %} +
  • + {{ item.name }} +
  • + {% endfor %} +
+
+ +
+

Photos

+
+ {% for photo in photos %} +
+ Photo {{ loop.index }} +
{{ photo.comment or 'No comment' }}
+
+ + +
+
Updated: {{ photo.uploaded_at }}
+
+ {% endfor %} +
+
+ +
+

Summary

+ +
+ + + +
+
+ +
+

Download Report

+ Download as PDF +
+ + + + \ No newline at end of file diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..1e7eb5d --- /dev/null +++ b/tests/test_app.py @@ -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() \ No newline at end of file