Files
TimeTracker/app/models/custom_field_definition.py
T
Dries Peeters a582e2af62 feat: improve error handling, performance logging, and PWA install UI
- Add session state clearing (expunge_all) after rollbacks in custom field
  definition error handlers to prevent stale session state
- Add graceful error handling for missing link_templates table with proper
  rollback and session cleanup, preventing app crashes when migrations
  haven't been run
- Add detailed performance logging to TaskService.list_tasks method to track
  timing of each query step for performance monitoring
- Improve PWA install prompt UI with better toast integration, dismiss button,
  and proper DOM manipulation using requestAnimationFrame
- Bump version to 4.5.0
2025-12-12 21:49:26 +01:00

191 lines
8.1 KiB
Python

"""Custom Field Definition model for global custom field management"""
from datetime import datetime
from app import db
from sqlalchemy.exc import ProgrammingError
class CustomFieldDefinition(db.Model):
"""Model for storing global custom field definitions that can be used across all clients"""
__tablename__ = "custom_field_definitions"
id = db.Column(db.Integer, primary_key=True)
field_key = db.Column(db.String(100), unique=True, nullable=False, index=True) # Unique key (e.g., 'debtor_number')
label = db.Column(db.String(200), nullable=False) # Display label (e.g., 'Debtor Number')
description = db.Column(db.Text, nullable=True) # Help text for the field
is_mandatory = db.Column(db.Boolean, default=False, nullable=False) # Whether field is required
is_active = db.Column(db.Boolean, default=True, nullable=False, index=True) # Whether field is active
order = db.Column(db.Integer, default=0, nullable=False) # Display order
created_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
creator = db.relationship("User", backref="custom_field_definitions", foreign_keys=[created_by])
def __repr__(self):
return f"<CustomFieldDefinition {self.field_key}>"
def to_dict(self):
"""Convert custom field definition to dictionary for JSON serialization"""
return {
"id": self.id,
"field_key": self.field_key,
"label": self.label,
"description": self.description,
"is_mandatory": self.is_mandatory,
"is_active": self.is_active,
"order": self.order,
"created_by": self.created_by,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
@classmethod
def get_active_definitions(cls):
"""Get all active custom field definitions ordered by order and label.
Returns empty list if table doesn't exist (migration not run yet).
"""
try:
return cls.query.filter_by(is_active=True).order_by(cls.order, cls.label).all()
except ProgrammingError as e:
# Handle case where custom_field_definitions table doesn't exist (migration not run)
if "does not exist" in str(e.orig) or "relation" in str(e.orig).lower():
try:
from flask import current_app
if current_app:
current_app.logger.warning(
"custom_field_definitions table does not exist. Run migration: flask db upgrade"
)
except RuntimeError:
pass # No application context
# Rollback the failed transaction and clear session state
try:
db.session.rollback()
db.session.expunge_all() # Clear all objects from session
except Exception:
pass
return []
raise
except Exception:
# For other database errors, return empty list to prevent breaking the app
try:
from flask import current_app
if current_app:
current_app.logger.warning(
"Could not query custom_field_definitions. Returning empty list."
)
except RuntimeError:
pass # No application context
# Rollback the failed transaction
try:
db.session.rollback()
except Exception:
pass
return []
@classmethod
def get_mandatory_definitions(cls):
"""Get all active mandatory custom field definitions.
Returns empty list if table doesn't exist (migration not run yet).
"""
try:
return cls.query.filter_by(is_active=True, is_mandatory=True).order_by(cls.order, cls.label).all()
except ProgrammingError as e:
# Handle case where custom_field_definitions table doesn't exist (migration not run)
if "does not exist" in str(e.orig) or "relation" in str(e.orig).lower():
try:
from flask import current_app
if current_app:
current_app.logger.warning(
"custom_field_definitions table does not exist. Run migration: flask db upgrade"
)
except RuntimeError:
pass # No application context
# Rollback the failed transaction and clear session state
try:
db.session.rollback()
db.session.expunge_all() # Clear all objects from session
except Exception:
pass
return []
raise
except Exception:
# For other database errors, return empty list to prevent breaking the app
try:
from flask import current_app
if current_app:
current_app.logger.warning(
"Could not query custom_field_definitions. Returning empty list."
)
except RuntimeError:
pass # No application context
# Rollback the failed transaction
try:
db.session.rollback()
except Exception:
pass
return []
@classmethod
def get_by_key(cls, field_key):
"""Get a custom field definition by its key.
Returns None if table doesn't exist (migration not run yet).
"""
try:
return cls.query.filter_by(field_key=field_key, is_active=True).first()
except ProgrammingError as e:
# Handle case where custom_field_definitions table doesn't exist (migration not run)
if "does not exist" in str(e.orig) or "relation" in str(e.orig).lower():
try:
from flask import current_app
if current_app:
current_app.logger.warning(
"custom_field_definitions table does not exist. Run migration: flask db upgrade"
)
except RuntimeError:
pass # No application context
# Rollback the failed transaction and clear session state
try:
db.session.rollback()
db.session.expunge_all() # Clear all objects from session
except Exception:
pass
return None
raise
except Exception:
# For other database errors, return None to prevent breaking the app
try:
from flask import current_app
if current_app:
current_app.logger.warning(
"Could not query custom_field_definitions. Returning None."
)
except RuntimeError:
pass # No application context
# Rollback the failed transaction
try:
db.session.rollback()
except Exception:
pass
return None
def count_clients_with_value(self):
"""Count how many clients have a value for this custom field"""
from app.models import Client
from sqlalchemy import func
# Query clients that have this field key in their custom_fields JSON
# This works for both SQLite and PostgreSQL
count = 0
for client in Client.query.all():
if client.custom_fields and self.field_key in client.custom_fields:
value = client.custom_fields.get(self.field_key)
# Count only if value is not empty
if value and str(value).strip():
count += 1
return count