Update probe related models, routes and templates

This commit is contained in:
Andrew Conlon
2025-07-29 10:51:42 -04:00
parent 7e62e61124
commit 2600a5aa61
7 changed files with 395 additions and 47 deletions
+49
View File
@@ -93,6 +93,23 @@ class Probe:
raise Exception('Failed to create probe')
return cls(**result.data[0])
def retire(self):
"""Mark probe as retired with current timestamp"""
supabase = get_supabase()
result = supabase.table('probes').update({
'retired_at': datetime.utcnow().isoformat()
}).eq('id', self.id).execute()
if not result.data:
raise Exception('Failed to retire probe')
self.retired_at = datetime.utcnow()
def delete(self):
"""Delete probe from database"""
supabase = get_supabase()
result = supabase.table('probes').delete().eq('id', self.id).execute()
if not result.data:
raise Exception('Failed to delete probe')
@dataclass
class Channel:
"""Channel model representing individual measurement channels"""
@@ -131,6 +148,19 @@ class Channel:
channels.append(cls(**row))
return channels
@classmethod
def get_by_serial(cls, serial_number: str):
"""Get channel by serial number if it exists (case-sensitive exact match)"""
serial_upper = serial_number.upper()
supabase = get_supabase()
result = supabase.table('channels').select("*").eq('serial_number', serial_upper).execute()
if not result.data:
return None
row = result.data[0]
# Parse datetime string
row['created_at'] = datetime.fromisoformat(row['created_at']) if row['created_at'] else None
return cls(**row)
@classmethod
def create(cls, probe_id: str, serial_number: str, parameter_id: str):
"""Create a new channel"""
@@ -259,6 +289,25 @@ class Calibration:
result = supabase.table('calibrations').select("*").eq('work_order_id', work_order_id).execute()
return [cls(**row) for row in result.data] if result.data else []
@classmethod
def get_by_channel(cls, channel_id: str):
"""Get all calibrations for a specific channel"""
supabase = get_supabase()
result = supabase.table('calibrations').select("*").eq('channel_id', channel_id).execute()
calibrations = []
for row in result.data if result.data else []:
try:
# Parse datetime strings
for date_field in ['std_cal_date', 'std_cal_due', 'date']:
if row.get(date_field):
row[date_field] = datetime.fromisoformat(row[date_field])
calibrations.append(cls(**row))
except Exception as e:
print(f"Error parsing calibration data: {e}")
continue
return calibrations
@dataclass
class Customer:
"""Customer model representing clients who request calibrations"""
+30 -5
View File
@@ -162,9 +162,14 @@ def review_calibration(calibration_id):
flash('Calibration not found', 'error')
return redirect(url_for('calibrations.index'))
user = {
'user_name': session.get('user_name'),
'can_calibrate': session.get('can_calibrate'),
'can_review': session.get('can_review')
}
return render_template('calibration_review.html',
calibration=calibration.data[0],
user=session)
user=user)
@calibrations_bp.route('/new')
def new_calibration():
@@ -189,13 +194,18 @@ def new_calibration():
# Get all parameters (using correct column name)
parameters = supabase.table('parameters').select('id, parameter_name').execute()
user = {
'user_name': session.get('user_name'),
'can_calibrate': session.get('can_calibrate'),
'can_review': session.get('can_review')
}
return render_template('calibration_form.html',
work_orders=work_orders.data,
standards=standards.data,
channels=channels.data,
probe_models=probe_models.data,
parameters=parameters.data,
user=session)
user=user)
@calibrations_bp.route('/filtered-channels')
def get_filtered_channels():
@@ -248,6 +258,11 @@ def index():
calibrated_by:calibrated_by(name)
''').order('date', desc=True).limit(10).execute()
user = {
'user_name': session.get('user_name'),
'can_calibrate': session.get('can_calibrate'),
'can_review': session.get('can_review')
}
return render_template('calibration_dashboard.html',
summary={
'total_calibrations': total,
@@ -255,7 +270,7 @@ def index():
'failed_calibrations': failed
},
recent_calibrations=recent_calibrations.data,
user=session)
user=user)
@calibrations_bp.route('/probe/<probe_id>')
def calibrations_for_probe(probe_id):
@@ -278,10 +293,15 @@ def calibrations_for_probe(probe_id):
# Get probe details
probe = supabase.table('probes').select('*').eq('id', probe_id).execute()
user = {
'user_name': session.get('user_name'),
'can_calibrate': session.get('can_calibrate'),
'can_review': session.get('can_review')
}
return render_template('calibration_history.html',
calibrations=calibrations.data,
probe=probe.data[0] if probe.data else None,
user=session)
user=user)
@calibrations_bp.route('/probe/<probe_id>/trends')
def calibration_trends(probe_id):
@@ -344,6 +364,11 @@ def view_calibration(calibration_id):
flash('Calibration not found', 'error')
return redirect(url_for('calibrations.index'))
user = {
'user_name': session.get('user_name'),
'can_calibrate': session.get('can_calibrate'),
'can_review': session.get('can_review')
}
return render_template('calibration_view.html',
calibration=calibration.data[0],
user=session)
user=user)
+8 -2
View File
@@ -1,4 +1,4 @@
from flask import Blueprint, render_template, redirect, url_for, flash
from flask import Blueprint, render_template, redirect, url_for, flash, session
from app.models import Channel, Calibration
from app.routes.auth import role_required
@@ -16,6 +16,12 @@ def view_channel(channel_id):
# Get calibration history for this channel
calibrations = Calibration.get_by_channel(channel_id)
user = {
'user_name': session.get('user_name'),
'can_calibrate': session.get('can_calibrate'),
'can_review': session.get('can_review')
}
return render_template('channel_view.html',
channel=channel,
calibrations=calibrations)
calibrations=calibrations,
user=user)
+154 -15
View File
@@ -1,20 +1,48 @@
import re
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
from app.models import Probe, ProbeModel, Channel, Parameter, ProbeLocation
from app.routes.auth import role_required
probes_bp = Blueprint('probes', __name__)
@probes_bp.route('/api/check-channel-serials', methods=['POST'], endpoint='check_channel_serials')
@role_required('review')
def check_channel_serials():
"""Check if channel serials already exist in database"""
data = request.get_json()
if not data or 'serials' not in data:
return jsonify({'error': 'Invalid request'}), 400
duplicates = []
for serial in data['serials']:
if Channel.get_by_serial(serial):
duplicates.append(serial)
return jsonify({'duplicates': duplicates})
@probes_bp.route('/')
@role_required('review')
def list_probes():
"""List all probes with optional active filter"""
active_only = request.args.get('active_only', 'true').lower() == 'true'
probes = Probe.get_all(active_only=active_only)
# Get channels for each probe
probes_with_channels = []
for probe in probes:
probe_dict = probe.__dict__
probe_dict['channels'] = Channel.get_by_probe(probe.id)
probes_with_channels.append(probe_dict)
user = {
'user_name': session.get('user_name'),
'can_calibrate': session.get('can_calibrate'),
'can_review': session.get('can_review')
}
return render_template('probe_list.html',
probes=probes,
probes=probes_with_channels,
active_only=active_only,
user=session)
user=user)
@probes_bp.route('/new')
@role_required('review')
@@ -22,36 +50,51 @@ def new_probe():
"""Display form to create new probe"""
probe_models = ProbeModel.get_all()
parameters = Parameter.get_all()
user = {
'user_name': session.get('user_name'),
'can_calibrate': session.get('can_calibrate'),
'can_review': session.get('can_review')
}
return render_template('probe_form.html',
probe_models=probe_models,
parameters=parameters,
user=session)
user=user)
@probes_bp.route('/', methods=['POST'])
@role_required('review')
def create_probe():
"""Handle new probe creation with channels"""
try:
# First create the probe
probe = Probe.create(
model_id=request.form['model_id'],
serial_number=request.form['serial_number'],
description='' # Empty description since field was removed
)
# Then create channels if provided
# First validate all channels
channel_serials = request.form.getlist('channel_serials[]')
parameter_ids = request.form.getlist('parameter_ids[]')
if channel_serials and parameter_ids and len(channel_serials) == len(parameter_ids):
for i in range(len(channel_serials)):
# Validate channel serial format [0-9A-F]{16}
if not re.fullmatch(r'^[0-9A-F]{16}$', channel_serials[i].upper()):
serial_upper = channel_serials[i].upper()
if not re.fullmatch(r'^[0-9A-F]{16}$', serial_upper):
raise ValueError(f'Invalid channel serial format: {channel_serials[i]}. Must be 16 hex characters (0-9, A-F)')
# Check for duplicate serial
existing_channel = Channel.get_by_serial(serial_upper)
if existing_channel:
raise ValueError(f'Channel serial already exists: {channel_serials[i]}')
# Only create probe if all channels are valid
probe = Probe.create(
model_id=request.form['model_id'],
serial_number='', # Default empty string since field was removed from form
description='' # Empty description since field was removed
)
# Create channels after successful probe creation
if channel_serials and parameter_ids and len(channel_serials) == len(parameter_ids):
for i in range(len(channel_serials)):
serial_upper = channel_serials[i].upper()
Channel.create(
probe_id=probe.id,
serial_number=channel_serials[i].upper(), # Store in uppercase
serial_number=serial_upper,
parameter_id=parameter_ids[i]
)
@@ -79,8 +122,104 @@ def view_probe(probe_id):
# Get location history
locations = ProbeLocation.get_by_probe(probe_id)
# Get parameters for channel form
parameters = Parameter.get_all()
user = {
'user_name': session.get('user_name'),
'can_calibrate': session.get('can_calibrate'),
'can_review': session.get('can_review')
}
return render_template('probe_view.html',
probe=probe,
channels=channels,
locations=locations,
user=session)
parameters=parameters,
user=user)
@probes_bp.route('/<probe_id>/channels', methods=['POST'])
@role_required('review')
def add_channel(probe_id):
"""Add a channel to an existing probe"""
try:
probe = Probe.get_by_id(probe_id)
if not probe:
flash('Probe not found', 'danger')
return redirect(url_for('probes.list_probes'))
if probe.retired_at:
flash('Cannot add channels to retired probe', 'danger')
return redirect(url_for('probes.view_probe', probe_id=probe_id))
serial = request.form.get('channel_serial', '').upper()
parameter_id = request.form.get('parameter_id')
# Validate channel serial format [0-9A-F]{16}
if not re.fullmatch(r'^[0-9A-F]{16}$', serial):
raise ValueError(f'Invalid channel serial format: {serial}. Must be 16 hex characters (0-9, A-F)')
# Validate parameter_id exists and is valid
if not parameter_id:
raise ValueError('Parameter ID is required')
if not Parameter.get_by_id(parameter_id):
raise ValueError('Invalid parameter ID')
# Check for duplicate serial
existing_channel = Channel.get_by_serial(serial)
if existing_channel:
raise ValueError(f'Channel serial already exists: {serial}')
# Create channel
Channel.create(
probe_id=probe_id,
serial_number=serial,
parameter_id=parameter_id
)
flash('Channel added successfully', 'success')
return redirect(url_for('probes.view_probe', probe_id=probe_id))
except ValueError as ve:
flash(f'Validation error: {str(ve)}', 'danger')
return redirect(url_for('probes.view_probe', probe_id=probe_id))
except Exception as e:
flash(f'Error adding channel: {str(e)}', 'danger')
return redirect(url_for('probes.view_probe', probe_id=probe_id))
@probes_bp.route('/<probe_id>/retire', methods=['POST'])
@role_required('review')
def retire_probe(probe_id):
"""Mark a probe as retired"""
try:
probe = Probe.get_by_id(probe_id)
if not probe:
flash('Probe not found', 'danger')
return redirect(url_for('probes.list_probes'))
if probe.retired_at:
flash('Probe is already retired', 'warning')
return redirect(url_for('probes.view_probe', probe_id=probe_id))
probe.retire()
flash('Probe retired successfully', 'success')
return redirect(url_for('probes.view_probe', probe_id=probe_id))
except Exception as e:
flash(f'Error retiring probe: {str(e)}', 'danger')
return redirect(url_for('probes.view_probe', probe_id=probe_id))
@probes_bp.route('/<probe_id>', methods=['DELETE'])
@role_required('review')
def delete_probe(probe_id):
"""Delete a probe (only if it has no channels)"""
try:
probe = Probe.get_by_id(probe_id)
if not probe:
return jsonify({'error': 'Probe not found'}), 404
channels = Channel.get_by_probe(probe_id)
if channels:
return jsonify({'error': 'Cannot delete probe with channels'}), 400
probe.delete()
return jsonify({'message': 'Probe deleted successfully'}), 200
except Exception as e:
return jsonify({'error': str(e)}), 500