mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-01 01:40:59 -05:00
a582e2af62
- 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
191 lines
8.1 KiB
Python
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 |