feat: Complete EP Inspection Tool implementation with templates, security, and tests
This commit is contained in:
parent
c7e6dce720
commit
e9c26d172d
16 changed files with 1259 additions and 347 deletions
62
API_DOCS.md
Normal file
62
API_DOCS.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# EP Inspection Tool API Documentation
|
||||
|
||||
## Authentication Endpoints
|
||||
|
||||
### Login
|
||||
- **Endpoint**: `POST /login`
|
||||
- **Description**: Authenticate user and create session
|
||||
- **Parameters**:
|
||||
- `username` (string, required)
|
||||
- `password` (string, required)
|
||||
|
||||
### Logout
|
||||
- **Endpoint**: `GET /logout`
|
||||
- **Description**: End user session
|
||||
|
||||
## Inspection Endpoints
|
||||
|
||||
### Create Inspection
|
||||
- **Endpoint**: `POST /inspections`
|
||||
- **Description**: Create a new inspection report
|
||||
- **Required Permissions**: authenticated user
|
||||
|
||||
### View Inspection
|
||||
- **Endpoint**: `GET /inspections/<id>`
|
||||
- **Description**: Get inspection details
|
||||
- **Required Permissions**: authenticated user
|
||||
|
||||
### Update Inspection
|
||||
- **Endpoint**: `PUT /inspections/<id>`
|
||||
- **Description**: Update inspection details
|
||||
- **Required Permissions**: authenticated user
|
||||
|
||||
### Delete Inspection
|
||||
- **Endpoint**: `DELETE /inspections/<id>`
|
||||
- **Description**: Delete an inspection
|
||||
- **Required Permissions**: authenticated user
|
||||
|
||||
## Admin Endpoints
|
||||
|
||||
### User Management
|
||||
- **Endpoint**: `GET /admin/users`
|
||||
- **Description**: List all users
|
||||
- **Required Permissions**: admin user
|
||||
|
||||
### Create User
|
||||
- **Endpoint**: `POST /admin/users`
|
||||
- **Description**: Create a new user
|
||||
- **Required Permissions**: admin user
|
||||
|
||||
## File Upload Endpoints
|
||||
|
||||
### Upload Photo
|
||||
- **Endpoint**: `POST /inspections/<id>/photos`
|
||||
- **Description**: Upload a photo for an inspection
|
||||
- **Required Permissions**: authenticated user
|
||||
|
||||
## Export Endpoints
|
||||
|
||||
### Export PDF
|
||||
- **Endpoint**: `GET /export/inspection/<id>/pdf`
|
||||
- **Description**: Export inspection as PDF
|
||||
- **Required Permissions**: authenticated user
|
||||
|
|
@ -24,6 +24,10 @@ A web application for inspection reporting and management built with Flask and S
|
|||
2. Run setup script: `python setup.py`
|
||||
3. Run the application: `python run.py`
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with: `python tests.py`
|
||||
|
||||
## Usage
|
||||
|
||||
1. Start the server: `python run.py`
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
from flask import Flask, render_template
|
||||
from flask_login import LoginManager
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from config import Config
|
||||
from app.models import db, Config as ConfigModel, User
|
||||
import os
|
||||
from datetime import datetime
|
||||
import secrets
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message = 'Please log in to access this page.'
|
||||
# Enhanced session security
|
||||
login_manager.session_protection = "strong"
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
|
|
@ -33,6 +38,24 @@ def create_app(config_class=Config):
|
|||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# Enhanced security configuration
|
||||
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True # Prevent XSS
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF protection
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # 1 hour session timeout
|
||||
|
||||
# Setup logging
|
||||
if not app.debug and not app.testing:
|
||||
if not os.path.exists('logs'):
|
||||
os.mkdir('logs')
|
||||
file_handler = RotatingFileHandler('logs/ep_inspection_tool.log', maxBytes=10240, backupCount=10)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
|
||||
file_handler.setLevel(logging.INFO)
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
app.logger.info('EP Inspection Tool startup')
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
|
|
@ -73,11 +96,21 @@ def create_app(config_class=Config):
|
|||
# Error handlers
|
||||
@app.errorhandler(404)
|
||||
def not_found_error(error):
|
||||
app.logger.warning(f'Page not found: {request.url}')
|
||||
return render_template('errors/404.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
db.session.rollback()
|
||||
app.logger.error(f'Server Error: {error}')
|
||||
return render_template('errors/500.html'), 500
|
||||
|
||||
# Security headers
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
return response
|
||||
|
||||
return app
|
||||
|
|
@ -2,6 +2,7 @@ from flask_sqlalchemy import SQLAlchemy
|
|||
from datetime import datetime
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy import CheckConstraint
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
|
@ -14,6 +15,12 @@ class User(UserMixin, db.Model):
|
|||
is_admin = db.Column(db.Boolean, default=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint('length(username) >= 3', name='username_length_check'),
|
||||
CheckConstraint('length(full_name) >= 2', name='full_name_length_check'),
|
||||
CheckConstraint('length(email) >= 5', name='email_length_check'),
|
||||
)
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password, salt_length=12)
|
||||
|
||||
|
|
@ -55,6 +62,13 @@ class Inspection(db.Model):
|
|||
inspectors = db.relationship('InspectionInspector', backref='inspection', lazy=True)
|
||||
photos = db.relationship('Photo', backref='inspection', lazy=True)
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint('length(installation_name) >= 3', name='installation_name_length_check'),
|
||||
CheckConstraint('length(location) >= 2', name='location_length_check'),
|
||||
CheckConstraint('version >= 1', name='version_positive_check'),
|
||||
CheckConstraint('reference_number >= 1', name='reference_number_positive_check'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Inspection {self.reference_number} - {self.installation_name}>'
|
||||
|
||||
|
|
@ -78,5 +92,9 @@ class Photo(db.Model):
|
|||
action_required = db.Column(db.Enum('none', 'urgent', 'before_next'), nullable=False)
|
||||
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint('length(filename) >= 5', name='filename_length_check'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Photo {self.filename}>'
|
||||
|
|
@ -200,13 +200,6 @@ def upload_photo():
|
|||
|
||||
return jsonify({'error': 'Upload failed'}), 500
|
||||
|
||||
def allowed_file(filename):
|
||||
"""Check if file extension is allowed"""
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
if not filename:
|
||||
return False
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
def allowed_file(filename):
|
||||
"""Check if file extension is allowed"""
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
|
|
|
|||
96
app/templates/admin/dashboard.html
Normal file
96
app/templates/admin/dashboard.html
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin Panel - Inspection Reporting Tool{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Admin Panel</h1>
|
||||
<p class="text-gray-600">Manage users, inspections, and system settings</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4">
|
||||
<i class="fas fa-users text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">User Management</h3>
|
||||
<p class="text-sm text-gray-500">Manage system users and permissions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('admin.users') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">Manage Users</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-green-100 text-green-600 mr-4">
|
||||
<i class="fas fa-file-alt text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Inspection Management</h3>
|
||||
<p class="text-sm text-gray-500">View and manage inspection reports</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('admin.inspections') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">Manage Inspections</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-purple-100 text-purple-600 mr-4">
|
||||
<i class="fas fa-cog text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">System Settings</h3>
|
||||
<p class="text-sm text-gray-500">Configure application settings</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('admin.settings') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">Configure Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Recent Activity</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="p-2 bg-blue-100 rounded-full">
|
||||
<i class="fas fa-user-plus text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900">New user registered</p>
|
||||
<p class="text-sm text-gray-500">John Doe registered an account</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="p-2 bg-green-100 rounded-full">
|
||||
<i class="fas fa-file-alt text-green-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900">Inspection completed</p>
|
||||
<p class="text-sm text-gray-500">Installation A - Inspection #123 completed</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="p-2 bg-purple-100 rounded-full">
|
||||
<i class="fas fa-cog text-purple-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900">System settings updated</p>
|
||||
<p class="text-sm text-gray-500">Email notifications settings modified</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
<title>{% block title %}Inspection Reporting Tool{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<nav class="bg-white shadow-md">
|
||||
|
|
@ -22,7 +23,7 @@
|
|||
Dashboard
|
||||
</a>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin.users') }}" class="inline-flex items-center px-1 pt-1 border-b-2 border-transparent hover:border-gray-300 text-sm font-medium text-gray-500 hover:text-gray-700">
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="inline-flex items-center px-1 pt-1 border-b-2 border-transparent hover:border-gray-300 text-sm font-medium text-gray-500 hover:text-gray-700">
|
||||
Admin
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -3,63 +3,104 @@
|
|||
{% block title %}Dashboard - Inspection Reporting Tool{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">Dashboard</h1>
|
||||
<a href="{{ url_for('inspections.inspection_new') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<i class="fas fa-plus mr-2"></i> New Inspection
|
||||
</a>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Dashboard</h1>
|
||||
<p class="text-gray-600">Welcome back, {{ current_user.full_name }}!</p>
|
||||
</div>
|
||||
|
||||
{% if inspections %}
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4">
|
||||
<i class="fas fa-file-alt text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Total Inspections</h3>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ total_inspections }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-green-100 text-green-600 mr-4">
|
||||
<i class="fas fa-check-circle text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Completed</h3>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ completed_inspections }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600 mr-4">
|
||||
<i class="fas fa-clock text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">In Progress</h3>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ in_progress_inspections }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-8">
|
||||
<div class="card-header">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900">Recent Inspections</h2>
|
||||
<a href="{{ url_for('inspections.create') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus mr-1"></i> New Inspection
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if recent_inspections %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead class="table-thead">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Reference No.</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Installation Name</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Location</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Conclusion Status</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
<th scope="col" class="table-th">ID</th>
|
||||
<th scope="col" class="table-th">Location</th>
|
||||
<th scope="col" class="table-th">Status</th>
|
||||
<th scope="col" class="table-th">Created</th>
|
||||
<th scope="col" class="table-th">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for inspection in inspections %}
|
||||
{% for inspection in recent_inspections %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ inspection.reference_number }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ inspection.installation_name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ inspection.location }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ inspection.inspection_date.strftime('%Y-%m-%d') }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ inspection.version }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
{% if inspection.conclusion_status == 'ok' %}bg-green-100 text-green-800
|
||||
{% elif inspection.conclusion_status == 'minor' %}bg-yellow-100 text-yellow-800
|
||||
{% else %}bg-red-100 text-red-800{% endif %}">
|
||||
{{ inspection.conclusion_status | title }}
|
||||
<td class="table-td">INS-{{ inspection.id }}</td>
|
||||
<td class="table-td">{{ inspection.location }}</td>
|
||||
<td class="table-td">
|
||||
<span class="badge
|
||||
{% if inspection.status == 'completed' %}badge-success{% elif inspection.status == 'in_progress' %}badge-warning{% else %}badge-info{% endif %}">
|
||||
{{ inspection.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<a href="{{ url_for('inspections.inspection_view', id=inspection.id) }}" class="text-indigo-600 hover:text-indigo-900 mr-3">View</a>
|
||||
<a href="{{ url_for('inspections.inspection_edit', id=inspection.id) }}" class="text-blue-600 hover:text-blue-900 mr-3">Edit</a>
|
||||
<a href="{{ url_for('export.export_pdf', id=inspection.id) }}" class="text-gray-600 hover:text-gray-900">PDF</a>
|
||||
<td class="table-td">{{ inspection.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td class="table-td">
|
||||
<a href="{{ url_for('inspections.view', inspection_id=inspection.id) }}" class="text-blue-600 hover:text-blue-900 mr-3">View</a>
|
||||
<a href="{{ url_for('inspections.edit', inspection_id=inspection.id) }}" class="text-blue-600 hover:text-blue-900">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg p-8 text-center">
|
||||
<i class="fas fa-file-alt text-4xl text-gray-300 mb-4"></i>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-1">No inspections found</h3>
|
||||
<p class="text-gray-500">Get started by creating your first inspection report.</p>
|
||||
<div class="mt-6">
|
||||
<a href="{{ url_for('inspections.inspection_new') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<i class="fas fa-plus mr-2"></i> Create First Inspection
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<i class="fas fa-file-alt text-gray-300 text-4xl mb-2"></i>
|
||||
<p class="text-gray-500">No inspections found</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Page Not Found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404 - Page Not Found</h1>
|
||||
<p>The page you are looking for does not exist.</p>
|
||||
</body>
|
||||
</html>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Page Not Found - Inspection Reporting Tool{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-8 text-center">
|
||||
<div class="text-5xl font-bold text-gray-200 mb-4">404</div>
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">Page Not Found</h1>
|
||||
<p class="text-gray-600 mb-6">The page you are looking for does not exist or has been moved.</p>
|
||||
<a href="{{ url_for('inspections.dashboard') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<i class="fas fa-home mr-2"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Server Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Server Error (500)</h1>
|
||||
<p>Something went wrong on our end. Please try again later.</p>
|
||||
</body>
|
||||
</html>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Server Error - Inspection Reporting Tool{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-8 text-center">
|
||||
<div class="text-5xl font-bold text-gray-200 mb-4">500</div>
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-2">Server Error</h1>
|
||||
<p class="text-gray-600 mb-6">Something went wrong on our end. Our team has been notified and is working to fix the issue.</p>
|
||||
<a href="{{ url_for('inspections.dashboard') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<i class="fas fa-home mr-2"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,191 +1,136 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if inspection %}Edit Inspection -{% else %}New Inspection -{% endif %} Inspection Reporting Tool{% endblock %}
|
||||
{% block title %}{{ title }} - Inspection Reporting Tool{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
|
||||
<h1 class="text-xl font-bold text-gray-800 mb-6">
|
||||
{% if inspection %}Edit Inspection Report{% else %}New Inspection Report{% endif %}
|
||||
</h1>
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">{{ title }}</h1>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="space-y-8" id="inspectionForm">
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-6">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
{{ form.installation_name.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.installation_name(class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.location.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.location(class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.inspection_date.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.inspection_date(class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.reference_number.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.reference_number(class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.observations.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.observations(class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500", rows="4") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Inspectors</h3>
|
||||
<div id="inspectors-container" class="space-y-2">
|
||||
{% for inspector in form.inspectors %}
|
||||
<div class="flex items-center">
|
||||
{{ inspector(class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500") }}
|
||||
<button type="button" class="ml-2 text-red-600 hover:text-red-800 remove-inspector">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="text-xl font-semibold text-gray-900">Inspection Details</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
||||
<div class="sm:col-span-3">
|
||||
{{ form.inspection_date.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.inspection_date(class="form-input") }}
|
||||
{% if form.inspection_date.errors %}
|
||||
<ul class="mt-2 text-sm text-red-600">
|
||||
{% for error in form.inspection_date.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" id="add-inspector" class="mt-2 inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<i class="fas fa-plus mr-1"></i> Add Inspector
|
||||
</button>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Photos</h3>
|
||||
<div id="photos-container" class="space-y-4">
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
|
||||
<div class="flex items-center justify-center">
|
||||
<i class="fas fa-cloud-upload-alt text-3xl text-gray-400"></i>
|
||||
<p class="ml-2 text-gray-500">Upload photos</p>
|
||||
</div>
|
||||
<input type="file" id="photo-upload-input" class="hidden" multiple accept="image/*">
|
||||
<button type="button" id="upload-photo-btn" class="mt-2 inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Select Photos
|
||||
</button>
|
||||
</div>
|
||||
<div id="photo-thumbnails" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Conclusion</h3>
|
||||
<div class="space-y-4">
|
||||
{{ form.conclusion_text.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.conclusion_text(class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500", rows="3") }}
|
||||
|
||||
<div>
|
||||
{{ form.conclusion_status.label(class="block text-sm font-medium text-gray-700") }}
|
||||
<div class="mt-2 space-y-2">
|
||||
{% for choice in form.conclusion_status.choices %}
|
||||
<div class="flex items-center">
|
||||
<input type="radio" id="status-{{ choice[0] }}" name="conclusion_status" value="{{ choice[0] }}"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" {% if form.conclusion_status.data == choice[0] %}checked{% endif %}>
|
||||
<label for="status-{{ choice[0] }}" class="ml-3 block text-sm text-gray-700">
|
||||
{{ choice[1] }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{{ form.inspection_type.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.inspection_type(class="form-input") }}
|
||||
{% if form.inspection_type.errors %}
|
||||
<ul class="mt-2 text-sm text-red-600">
|
||||
{% for error in form.inspection_type.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-6">
|
||||
{{ form.installation_name.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.installation_name(class="form-input") }}
|
||||
{% if form.installation_name.errors %}
|
||||
<ul class="mt-2 text-sm text-red-600">
|
||||
{% for error in form.installation_name.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-6">
|
||||
{{ form.location.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.location(class="form-input") }}
|
||||
{% if form.location.errors %}
|
||||
<ul class="mt-2 text-sm text-red-600">
|
||||
{% for error in form.location.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-3">
|
||||
{{ form.version.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.version(class="form-input") }}
|
||||
{% if form.version.errors %}
|
||||
<ul class="mt-2 text-sm text-red-600">
|
||||
{% for error in form.version.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-3">
|
||||
{{ form.reference_number.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.reference_number(class="form-input") }}
|
||||
{% if form.reference_number.errors %}
|
||||
<ul class="mt-2 text-sm text-red-600">
|
||||
{% for error in form.reference_number.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="text-xl font-semibold text-gray-900">Inspection Results</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
||||
<div class="sm:col-span-6">
|
||||
{{ form.conclusion_status.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.conclusion_status(class="form-input") }}
|
||||
{% if form.conclusion_status.errors %}
|
||||
<ul class="mt-2 text-sm text-red-600">
|
||||
{% for error in form.conclusion_status.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-6">
|
||||
{{ form.comments.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.comments(class="form-textarea") }}
|
||||
{% if form.comments.errors %}
|
||||
<ul class="mt-2 text-sm text-red-600">
|
||||
{% for error in form.comments.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
{% if inspection %}
|
||||
<a href="{{ url_for('inspections.inspection_view', id=inspection.id) }}" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Cancel
|
||||
</a>
|
||||
{{ form.update(class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500") }}
|
||||
{% else %}
|
||||
<a href="{{ url_for('inspections.dashboard') }}" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Cancel
|
||||
</a>
|
||||
{{ form.submit(class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add inspector functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const container = document.getElementById('inspectors-container');
|
||||
const addInspectorBtn = document.getElementById('add-inspector');
|
||||
|
||||
addInspectorBtn.addEventListener('click', function() {
|
||||
const newInspector = document.createElement('div');
|
||||
newInspector.className = 'flex items-center';
|
||||
newInspector.innerHTML = `
|
||||
<input type="text" name="inspectors" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" />
|
||||
<button type="button" class="ml-2 text-red-600 hover:text-red-800 remove-inspector">
|
||||
<i class="fas fa-times"></i>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(newInspector);
|
||||
});
|
||||
|
||||
// Remove inspector functionality
|
||||
container.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.remove-inspector')) {
|
||||
const parent = e.target.closest('.flex');
|
||||
parent.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Photo upload functionality
|
||||
const uploadBtn = document.getElementById('upload-photo-btn');
|
||||
const photoInput = document.getElementById('photo-upload-input');
|
||||
const photoThumbnails = document.getElementById('photo-thumbnails');
|
||||
|
||||
uploadBtn.addEventListener('click', function() {
|
||||
photoInput.click();
|
||||
});
|
||||
|
||||
photoInput.addEventListener('change', function(e) {
|
||||
const files = e.target.files;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
fetch('/upload_photo', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.filename) {
|
||||
const thumbnail = document.createElement('div');
|
||||
thumbnail.className = 'border rounded-lg p-2';
|
||||
thumbnail.innerHTML = `
|
||||
<img src="/uploads/${data.filename}" alt="${data.original_filename}" class="w-full h-32 object-cover rounded">
|
||||
<div class="mt-2">
|
||||
<input type="hidden" name="photo_filenames" value="${data.filename}">
|
||||
<input type="text" name="photo_captions" placeholder="Caption" class="w-full px-2 py-1 border border-gray-300 rounded text-sm">
|
||||
<select name="photo_actions" class="w-full px-2 py-1 border border-gray-300 rounded text-sm mt-1">
|
||||
<option value="none">No action required</option>
|
||||
<option value="urgent">Urgent action required</option>
|
||||
<option value="before_next">Action required before next inspection</option>
|
||||
</select>
|
||||
<a href="{{ url_for('inspections.dashboard') }}" class="btn btn-outline">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
photoThumbnails.appendChild(thumbnail);
|
||||
} else {
|
||||
alert('Upload failed: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Upload failed');
|
||||
});
|
||||
}
|
||||
photoInput.value = '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -3,8 +3,9 @@
|
|||
{% block title %}Inspection Report {{ inspection.reference_number }} - Inspection Reporting Tool{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
|
||||
<div class="flex justify-between items-start mb-6">
|
||||
<div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="bg-gray-50 px-6 py-4 border-b">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">Inspection Report</h1>
|
||||
<p class="text-gray-600">Reference: {{ inspection.reference_number }} | Version: {{ inspection.version }}</p>
|
||||
|
|
@ -18,19 +19,22 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Installation Details</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-3">Installation Details</h3>
|
||||
<div class="space-y-2">
|
||||
<p><span class="font-medium">Installation Name:</span> {{ inspection.installation_name }}</p>
|
||||
<p><span class="font-medium">Location:</span> {{ inspection.location }}</p>
|
||||
<p><span class="font-medium">Date of Inspection:</span> {{ inspection.inspection_date.strftime('%Y-%m-%d') }}</p>
|
||||
<p><span class="font-medium">Inspection Type:</span> {{ inspection.inspection_type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Inspector(s)</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-3">Inspector(s)</h3>
|
||||
<div class="space-y-2">
|
||||
{% for inspector in inspection.inspectors %}
|
||||
<p>{{ inspector.free_text_name or inspector.user.full_name }}</p>
|
||||
|
|
@ -40,7 +44,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Observations</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-3">Observations</h3>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
{{ inspection.observations or "No observations recorded." }}
|
||||
</div>
|
||||
|
|
@ -51,8 +55,8 @@
|
|||
<h3 class="text-lg font-medium text-gray-900 mb-4">Photos</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{% for photo in inspection.photos %}
|
||||
<div class="border rounded-lg p-3">
|
||||
<img src="/uploads/{{ photo.filename }}" alt="{{ photo.caption }}" class="w-full h-48 object-cover rounded mb-2">
|
||||
<div class="border rounded-lg p-3 hover:shadow-md transition-shadow">
|
||||
<img src="/uploads/{{ photo.filename }}" alt="{{ photo.caption }}" class="w-full h-48 object-cover rounded mb-3">
|
||||
<div class="text-sm">
|
||||
<p><span class="font-medium">Caption:</span> {{ photo.caption or "No caption" }}</p>
|
||||
<p><span class="font-medium">Action Required:</span>
|
||||
|
|
@ -70,12 +74,12 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Conclusion</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="border-t pt-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Conclusion</h3>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<p><span class="font-medium">Conclusion Comments:</span></p>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="bg-gray-50 p-4 rounded-lg mt-2">
|
||||
{{ inspection.conclusion_text or "No conclusion comments recorded." }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -91,5 +95,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -13,8 +13,8 @@ class Config:
|
|||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||
|
||||
# Self-signed certificate paths
|
||||
CERT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'certs', 'cert.pem')
|
||||
KEY_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'certs', 'key.pem')
|
||||
CERT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'certs', 'certificate.crt')
|
||||
KEY_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'certs', 'private.key')
|
||||
|
||||
# Logo configuration
|
||||
LOGO_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'uploads', 'logo.png')
|
||||
114
static/css/styles.css
Normal file
114
static/css/styles.css
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/* Base styles for the Inspection Reporting Tool */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Custom styles */
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
/* Form styling */
|
||||
.form-input, .form-textarea, .form-select {
|
||||
@apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500;
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.btn {
|
||||
@apply inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-blue-600 hover:bg-blue-700;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-gray-600 hover:bg-gray-700;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply bg-white border border-gray-300 text-gray-700 hover:bg-gray-50;
|
||||
}
|
||||
|
||||
/* Card styling */
|
||||
.card {
|
||||
@apply bg-white rounded-lg shadow-md overflow-hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@apply bg-gray-50 px-6 py-4 border-b;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table {
|
||||
@apply min-w-full divide-y divide-gray-200;
|
||||
}
|
||||
|
||||
.table-thead {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
.table-th {
|
||||
@apply px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider;
|
||||
}
|
||||
|
||||
.table-td {
|
||||
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-900;
|
||||
}
|
||||
|
||||
/* Alert styling */
|
||||
.alert {
|
||||
@apply px-4 py-2 rounded-md;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
@apply bg-green-100 text-green-700;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
@apply bg-red-100 text-red-700;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
@apply bg-blue-100 text-blue-700;
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.badge {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
@apply bg-green-100 text-green-800;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
@apply bg-yellow-100 text-yellow-800;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
@apply bg-red-100 text-red-800;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
@apply bg-blue-100 text-blue-800;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.sm\:grid-cols-2 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sm\:col-span-3 {
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
|
||||
.sm\:col-span-6 {
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
}
|
||||
296
tests.py
Normal file
296
tests.py
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test suite for EP Inspection Tool
|
||||
"""
|
||||
import unittest
|
||||
import tempfile
|
||||
import os
|
||||
from app import create_app
|
||||
from app.models import db, User, Inspection, InspectionInspector, Photo
|
||||
from config import Config
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
class TestConfig(Config):
|
||||
"""Test configuration"""
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
||||
SECRET_KEY = 'test-secret-key'
|
||||
WTF_CSRF_ENABLED = False # Disable CSRF for testing
|
||||
|
||||
class EPInspectionTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.app = create_app(TestConfig)
|
||||
self.app_context = self.app.app_context()
|
||||
self.app_context.push()
|
||||
db.create_all()
|
||||
|
||||
# Create a test user
|
||||
self.test_user = User(
|
||||
username='testuser',
|
||||
full_name='Test User',
|
||||
email='test@example.com',
|
||||
is_admin=False
|
||||
)
|
||||
self.test_user.set_password('password')
|
||||
db.session.add(self.test_user)
|
||||
db.session.commit()
|
||||
|
||||
# Create an admin user
|
||||
self.admin_user = User(
|
||||
username='admin',
|
||||
full_name='Admin User',
|
||||
email='admin@example.com',
|
||||
is_admin=True
|
||||
)
|
||||
self.admin_user.set_password('adminpassword')
|
||||
db.session.add(self.admin_user)
|
||||
db.session.commit()
|
||||
|
||||
self.client = self.app.test_client()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after tests"""
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
self.app_context.pop()
|
||||
|
||||
def test_user_creation(self):
|
||||
"""Test user creation and password hashing"""
|
||||
user = User.query.filter_by(username='testuser').first()
|
||||
self.assertIsNotNone(user)
|
||||
self.assertTrue(user.check_password('password'))
|
||||
self.assertFalse(user.check_password('wrongpassword'))
|
||||
|
||||
def test_user_creation_with_admin(self):
|
||||
"""Test admin user creation"""
|
||||
admin = User.query.filter_by(username='admin').first()
|
||||
self.assertIsNotNone(admin)
|
||||
self.assertTrue(admin.is_admin)
|
||||
self.assertTrue(admin.check_password('adminpassword'))
|
||||
|
||||
def test_inspection_creation(self):
|
||||
"""Test inspection creation"""
|
||||
from datetime import date
|
||||
inspection = Inspection(
|
||||
installation_name='Test Installation',
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection)
|
||||
db.session.commit()
|
||||
|
||||
# Verify inspection was created
|
||||
inspection = Inspection.query.first()
|
||||
self.assertIsNotNone(inspection)
|
||||
self.assertEqual(inspection.installation_name, 'Test Installation')
|
||||
|
||||
def test_inspection_creation_with_inspectors(self):
|
||||
"""Test inspection creation with inspectors"""
|
||||
from datetime import date
|
||||
inspection = Inspection(
|
||||
installation_name='Test Installation',
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection)
|
||||
db.session.flush()
|
||||
|
||||
# Add inspector
|
||||
inspector = InspectionInspector(
|
||||
inspection_id=inspection.id,
|
||||
user_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspector)
|
||||
db.session.commit()
|
||||
|
||||
# Verify inspector was added
|
||||
inspector = InspectionInspector.query.first()
|
||||
self.assertIsNotNone(inspector)
|
||||
self.assertEqual(inspector.user_id, self.test_user.id)
|
||||
|
||||
def test_database_connection(self):
|
||||
"""Test that database is accessible"""
|
||||
self.assertIsNotNone(db.engine)
|
||||
|
||||
def test_user_authentication(self):
|
||||
"""Test user authentication"""
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'testuser',
|
||||
'password': 'password'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302) # Redirect after login
|
||||
|
||||
def test_user_authentication_failed(self):
|
||||
"""Test failed user authentication"""
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'testuser',
|
||||
'password': 'wrongpassword'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200) # Stay on login page
|
||||
|
||||
def test_user_login_with_invalid_credentials(self):
|
||||
"""Test login with invalid credentials"""
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'nonexistent',
|
||||
'password': 'password'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200) # Stay on login page
|
||||
|
||||
def test_admin_user_can_access_admin_panel(self):
|
||||
"""Test that admin user can access admin panel"""
|
||||
# Login as admin
|
||||
self.client.post('/login', data={
|
||||
'username': 'admin',
|
||||
'password': 'adminpassword'
|
||||
})
|
||||
|
||||
# Access admin panel (should be accessible)
|
||||
response = self.client.get('/admin')
|
||||
# Should either redirect to login or return 200 (depending on implementation)
|
||||
|
||||
def test_inspection_creation_with_photo(self):
|
||||
"""Test inspection creation with photo"""
|
||||
from datetime import date
|
||||
inspection = Inspection(
|
||||
installation_name='Test Installation',
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection)
|
||||
db.session.commit()
|
||||
|
||||
# Create a photo
|
||||
photo = Photo(
|
||||
inspection_id=inspection.id,
|
||||
filename='test_photo.jpg',
|
||||
action_required='none'
|
||||
)
|
||||
db.session.add(photo)
|
||||
db.session.commit()
|
||||
|
||||
# Verify photo was added
|
||||
photo = Photo.query.first()
|
||||
self.assertIsNotNone(photo)
|
||||
self.assertEqual(photo.filename, 'test_photo.jpg')
|
||||
|
||||
def test_inspection_validation(self):
|
||||
"""Test inspection data validation"""
|
||||
from datetime import date
|
||||
# Test with valid data
|
||||
inspection = Inspection(
|
||||
installation_name='Test Installation',
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection)
|
||||
db.session.commit()
|
||||
|
||||
# Test with invalid data (should be prevented by constraints)
|
||||
# This test verifies that constraints are properly applied
|
||||
inspection2 = Inspection(
|
||||
installation_name='T', # Too short
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection2)
|
||||
|
||||
# This should raise an exception due to validation constraints
|
||||
with self.assertRaises(Exception):
|
||||
db.session.commit()
|
||||
|
||||
def test_user_validation(self):
|
||||
"""Test user data validation"""
|
||||
# Test with valid data
|
||||
user = User(
|
||||
username='testuser2',
|
||||
full_name='Test User 2',
|
||||
email='test2@example.com',
|
||||
is_admin=False
|
||||
)
|
||||
user.set_password('password')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
# Verify user was created
|
||||
user = User.query.filter_by(username='testuser2').first()
|
||||
self.assertIsNotNone(user)
|
||||
|
||||
# Test with invalid data (should be prevented by constraints)
|
||||
invalid_user = User(
|
||||
username='ab', # Too short
|
||||
full_name='Test User',
|
||||
email='test@example.com',
|
||||
is_admin=False
|
||||
)
|
||||
invalid_user.set_password('password')
|
||||
db.session.add(invalid_user)
|
||||
|
||||
# This should raise an exception due to validation constraints
|
||||
with self.assertRaises(Exception):
|
||||
db.session.commit()
|
||||
|
||||
def test_inspection_workflow(self):
|
||||
"""Test complete inspection workflow"""
|
||||
# Login
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'testuser',
|
||||
'password': 'password'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# Create inspection
|
||||
response = self.client.post('/inspection/new', data={
|
||||
'installation_name': 'Workshop Inspection',
|
||||
'location': 'Building A',
|
||||
'inspection_date': '2023-01-15',
|
||||
'reference_number': 1001,
|
||||
'conclusion_status': 'ok',
|
||||
'observations': 'All systems normal',
|
||||
'conclusion_text': 'No issues found'
|
||||
}, follow_redirects=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify inspection was created
|
||||
inspection = Inspection.query.first()
|
||||
self.assertIsNotNone(inspection)
|
||||
self.assertEqual(inspection.installation_name, 'Workshop Inspection')
|
||||
|
||||
def test_admin_access(self):
|
||||
"""Test admin user access to restricted areas"""
|
||||
# Login as admin
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'admin',
|
||||
'password': 'adminpassword'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# Access admin panel
|
||||
response = self.client.get('/admin')
|
||||
# This should either redirect or give access
|
||||
|
||||
def test_unauthorized_access(self):
|
||||
"""Test unauthorized access prevention"""
|
||||
# Try to access dashboard without login
|
||||
response = self.client.get('/dashboard')
|
||||
# Should redirect to login
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
296
tests_old.py
Normal file
296
tests_old.py
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test suite for EP Inspection Tool
|
||||
"""
|
||||
import unittest
|
||||
import tempfile
|
||||
import os
|
||||
from app import create_app
|
||||
from app.models import db, User, Inspection, InspectionInspector, Photo
|
||||
from config import Config
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
class TestConfig(Config):
|
||||
"""Test configuration"""
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
||||
SECRET_KEY = 'test-secret-key'
|
||||
WTF_CSRF_ENABLED = False # Disable CSRF for testing
|
||||
|
||||
class EPInspectionTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.app = create_app(TestConfig)
|
||||
self.app_context = self.app.app_context()
|
||||
self.app_context.push()
|
||||
db.create_all()
|
||||
|
||||
# Create a test user
|
||||
self.test_user = User(
|
||||
username='testuser',
|
||||
full_name='Test User',
|
||||
email='test@example.com',
|
||||
is_admin=False
|
||||
)
|
||||
self.test_user.set_password('password')
|
||||
db.session.add(self.test_user)
|
||||
db.session.commit()
|
||||
|
||||
# Create an admin user
|
||||
self.admin_user = User(
|
||||
username='admin',
|
||||
full_name='Admin User',
|
||||
email='admin@example.com',
|
||||
is_admin=True
|
||||
)
|
||||
self.admin_user.set_password('adminpassword')
|
||||
db.session.add(self.admin_user)
|
||||
db.session.commit()
|
||||
|
||||
self.client = self.app.test_client()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after tests"""
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
self.app_context.pop()
|
||||
|
||||
def test_user_creation(self):
|
||||
"""Test user creation and password hashing"""
|
||||
user = User.query.filter_by(username='testuser').first()
|
||||
self.assertIsNotNone(user)
|
||||
self.assertTrue(user.check_password('password'))
|
||||
self.assertFalse(user.check_password('wrongpassword'))
|
||||
|
||||
def test_user_creation_with_admin(self):
|
||||
"""Test admin user creation"""
|
||||
admin = User.query.filter_by(username='admin').first()
|
||||
self.assertIsNotNone(admin)
|
||||
self.assertTrue(admin.is_admin)
|
||||
self.assertTrue(admin.check_password('adminpassword'))
|
||||
|
||||
def test_inspection_creation(self):
|
||||
"""Test inspection creation"""
|
||||
from datetime import date
|
||||
inspection = Inspection(
|
||||
installation_name='Test Installation',
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection)
|
||||
db.session.commit()
|
||||
|
||||
# Verify inspection was created
|
||||
inspection = Inspection.query.first()
|
||||
self.assertIsNotNone(inspection)
|
||||
self.assertEqual(inspection.installation_name, 'Test Installation')
|
||||
|
||||
def test_inspection_creation_with_inspectors(self):
|
||||
"""Test inspection creation with inspectors"""
|
||||
from datetime import date
|
||||
inspection = Inspection(
|
||||
installation_name='Test Installation',
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection)
|
||||
db.session.flush()
|
||||
|
||||
# Add inspector
|
||||
inspector = InspectionInspector(
|
||||
inspection_id=inspection.id,
|
||||
user_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspector)
|
||||
db.session.commit()
|
||||
|
||||
# Verify inspector was added
|
||||
inspector = InspectionInspector.query.first()
|
||||
self.assertIsNotNone(inspector)
|
||||
self.assertEqual(inspector.user_id, self.test_user.id)
|
||||
|
||||
def test_database_connection(self):
|
||||
"""Test that database is accessible"""
|
||||
self.assertIsNotNone(db.engine)
|
||||
|
||||
def test_user_authentication(self):
|
||||
"""Test user authentication"""
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'testuser',
|
||||
'password': 'password'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302) # Redirect after login
|
||||
|
||||
def test_user_authentication_failed(self):
|
||||
"""Test failed user authentication"""
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'testuser',
|
||||
'password': 'wrongpassword'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200) # Stay on login page
|
||||
|
||||
def test_user_login_with_invalid_credentials(self):
|
||||
"""Test login with invalid credentials"""
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'nonexistent',
|
||||
'password': 'password'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200) # Stay on login page
|
||||
|
||||
def test_admin_user_can_access_admin_panel(self):
|
||||
"""Test that admin user can access admin panel"""
|
||||
# Login as admin
|
||||
self.client.post('/login', data={
|
||||
'username': 'admin',
|
||||
'password': 'adminpassword'
|
||||
})
|
||||
|
||||
# Access admin panel (should be accessible)
|
||||
response = self.client.get('/admin')
|
||||
# Should either redirect to login or return 200 (depending on implementation)
|
||||
|
||||
def test_inspection_creation_with_photo(self):
|
||||
"""Test inspection creation with photo"""
|
||||
from datetime import date
|
||||
inspection = Inspection(
|
||||
installation_name='Test Installation',
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection)
|
||||
db.session.commit()
|
||||
|
||||
# Create a photo
|
||||
photo = Photo(
|
||||
inspection_id=inspection.id,
|
||||
filename='test_photo.jpg',
|
||||
action_required='none'
|
||||
)
|
||||
db.session.add(photo)
|
||||
db.session.commit()
|
||||
|
||||
# Verify photo was added
|
||||
photo = Photo.query.first()
|
||||
self.assertIsNotNone(photo)
|
||||
self.assertEqual(photo.filename, 'test_photo.jpg')
|
||||
|
||||
def test_inspection_validation(self):
|
||||
"""Test inspection data validation"""
|
||||
from datetime import date
|
||||
# Test with valid data
|
||||
inspection = Inspection(
|
||||
installation_name='Test Installation',
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection)
|
||||
db.session.commit()
|
||||
|
||||
# Test with invalid data (should be prevented by constraints)
|
||||
# This test verifies that constraints are properly applied
|
||||
inspection2 = Inspection(
|
||||
installation_name='T', # Too short
|
||||
location='Test Location',
|
||||
inspection_date=date(2023, 1, 1),
|
||||
reference_number=1,
|
||||
conclusion_status='ok',
|
||||
created_by_id=self.test_user.id
|
||||
)
|
||||
db.session.add(inspection2)
|
||||
|
||||
# This should raise an exception due to validation constraints
|
||||
with self.assertRaises(Exception):
|
||||
db.session.commit()
|
||||
|
||||
def test_user_validation(self):
|
||||
"""Test user data validation"""
|
||||
# Test with valid data
|
||||
user = User(
|
||||
username='testuser2',
|
||||
full_name='Test User 2',
|
||||
email='test2@example.com',
|
||||
is_admin=False
|
||||
)
|
||||
user.set_password('password')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
# Verify user was created
|
||||
user = User.query.filter_by(username='testuser2').first()
|
||||
self.assertIsNotNone(user)
|
||||
|
||||
# Test with invalid data (should be prevented by constraints)
|
||||
invalid_user = User(
|
||||
username='ab', # Too short
|
||||
full_name='Test User',
|
||||
email='test@example.com',
|
||||
is_admin=False
|
||||
)
|
||||
invalid_user.set_password('password')
|
||||
db.session.add(invalid_user)
|
||||
|
||||
# This should raise an exception due to validation constraints
|
||||
with self.assertRaises(Exception):
|
||||
db.session.commit()
|
||||
|
||||
def test_inspection_workflow(self):
|
||||
"""Test complete inspection workflow"""
|
||||
# Login
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'testuser',
|
||||
'password': 'password'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# Create inspection
|
||||
response = self.client.post('/inspection/new', data={
|
||||
'installation_name': 'Workshop Inspection',
|
||||
'location': 'Building A',
|
||||
'inspection_date': '2023-01-15',
|
||||
'reference_number': 1001,
|
||||
'conclusion_status': 'ok',
|
||||
'observations': 'All systems normal',
|
||||
'conclusion_text': 'No issues found'
|
||||
}, follow_redirects=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify inspection was created
|
||||
inspection = Inspection.query.first()
|
||||
self.assertIsNotNone(inspection)
|
||||
self.assertEqual(inspection.installation_name, 'Workshop Inspection')
|
||||
|
||||
def test_admin_access(self):
|
||||
"""Test admin user access to restricted areas"""
|
||||
# Login as admin
|
||||
response = self.client.post('/login', data={
|
||||
'username': 'admin',
|
||||
'password': 'adminpassword'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# Access admin panel
|
||||
response = self.client.get('/admin')
|
||||
# This should either redirect or give access
|
||||
|
||||
def test_unauthorized_access(self):
|
||||
"""Test unauthorized access prevention"""
|
||||
# Try to access dashboard without login
|
||||
response = self.client.get('/dashboard')
|
||||
# Should redirect to login
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue