Files
TimeTracker/app/models/integration.py
T
Dries Peeters eb4fb8296f feat: Add integration framework and major feature enhancements
This commit introduces a comprehensive integration framework and multiple new features to enhance the TimeTracker application's capabilities.

Major Features:

- Integration Framework: Extensible system for third-party integrations with support for Jira, Slack, GitHub, and calendar services

- Project Templates: Reusable project templates for faster project creation

- Invoice Approvals: Workflow for invoice approval before sending

- Payment Gateways: Online payment processing integration with Stripe support

- Scheduled Reports: Automated report generation and email delivery

- Custom Reports: Advanced report builder with saved views

- Gantt Chart: Visual project timeline and dependency management

- Calendar Integrations: External calendar synchronization with Google Calendar support

- Push Notifications: Enhanced notification system with PWA support

Bug Fixes:

- Fix None handling in analytics routes

- Fix dynamic relationship loading issues in ProjectRepository and ProjectService

- Fix parameter ordering in service methods

- Fix None duration_seconds handling in budget forecasting

UI/UX Improvements:

- Update logo references to timetracker-logo.svg

- Add favicon links to all templates

- Add navigation items for new features

- Enhance invoice view with approval status and payment gateway links

Database:

- Add Alembic migrations for new features (065, 066, 067)

Dependencies:

- Add stripe==7.0.0 for payment processing

- Add google-api-python-client libraries for calendar integration
2025-11-26 07:53:28 +01:00

90 lines
4.0 KiB
Python

"""
Integration models for third-party service connections.
"""
from datetime import datetime
from app import db
from sqlalchemy import JSON
class Integration(db.Model):
"""Integration model for third-party service connections."""
__tablename__ = 'integrations'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False) # e.g., 'Jira', 'Slack', 'GitHub'
provider = db.Column(db.String(50), nullable=False, index=True) # e.g., 'jira', 'slack', 'github'
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
is_active = db.Column(db.Boolean, default=False, nullable=False) # Only True when credentials are set up
config = db.Column(JSON, nullable=True) # Provider-specific configuration
last_sync_at = db.Column(db.DateTime, nullable=True)
last_sync_status = db.Column(db.String(20), nullable=True) # 'success', 'error', 'pending'
last_error = db.Column(db.Text, nullable=True)
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)
user = db.relationship('User', backref='integrations')
def __repr__(self):
return f"<Integration {self.provider} for User {self.user_id}>"
class IntegrationCredential(db.Model):
"""Stores OAuth tokens and credentials for integrations."""
__tablename__ = 'integration_credentials'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
integration_id = db.Column(db.Integer, db.ForeignKey('integrations.id', ondelete='CASCADE'), nullable=False, index=True)
access_token = db.Column(db.Text, nullable=True) # Encrypted in production
refresh_token = db.Column(db.Text, nullable=True) # Encrypted in production
token_type = db.Column(db.String(20), default='Bearer', nullable=False)
expires_at = db.Column(db.DateTime, nullable=True)
scope = db.Column(db.String(500), nullable=True) # OAuth scopes
extra_data = db.Column(JSON, nullable=True) # Additional provider-specific data
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)
integration = db.relationship('Integration', backref=db.backref('credentials', cascade='all, delete-orphan'))
def __repr__(self):
return f"<IntegrationCredential for Integration {self.integration_id}>"
@property
def is_expired(self):
"""Check if the access token is expired."""
if not self.expires_at:
return False
return datetime.utcnow() >= self.expires_at
def needs_refresh(self):
"""Check if token needs refresh (within 5 minutes of expiry)."""
if not self.expires_at or not self.refresh_token:
return False
from datetime import timedelta
return datetime.utcnow() >= (self.expires_at - timedelta(minutes=5))
class IntegrationEvent(db.Model):
"""Tracks integration events and sync history."""
__tablename__ = 'integration_events'
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
integration_id = db.Column(db.Integer, db.ForeignKey('integrations.id', ondelete='CASCADE'), nullable=False, index=True)
event_type = db.Column(db.String(50), nullable=False) # 'sync', 'webhook', 'error', etc.
status = db.Column(db.String(20), nullable=False) # 'success', 'error', 'pending'
message = db.Column(db.Text, nullable=True)
event_metadata = db.Column(JSON, nullable=True) # Event-specific data (renamed from 'metadata' to avoid SQLAlchemy conflict)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
integration = db.relationship('Integration', backref='events')
def __repr__(self):
return f"<IntegrationEvent {self.event_type} for Integration {self.integration_id}>"