04/07 updated timezone, report via email, etc
This commit is contained in:
+87
-1
@@ -117,6 +117,33 @@ def create_app(config_name=None):
|
||||
# object is already in session), then fall back to a SELECT by PK.
|
||||
return db.session.get(User, int(user_id))
|
||||
|
||||
# ── Timezone filter ──────────────────────────────────────────────────────
|
||||
# All datetimes in the DB are stored as UTC (naive). The localtime filter
|
||||
# converts them to the admin-configured display timezone for templates.
|
||||
# Python routes should call app_localtime(dt) when they need a local datetime.
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
def _get_tz(app_obj):
|
||||
"""Return the configured ZoneInfo, falling back to UTC on bad input."""
|
||||
from app.models import SystemSetting
|
||||
tz_name = SystemSetting.get('app_timezone', 'America/New_York') or 'America/New_York'
|
||||
try:
|
||||
return ZoneInfo(tz_name)
|
||||
except (ZoneInfoNotFoundError, KeyError):
|
||||
app_obj.logger.warning(f'[TZ] Unknown timezone {tz_name!r}, falling back to UTC')
|
||||
return ZoneInfo('UTC')
|
||||
|
||||
def localtime_filter(dt, fmt='%b %d, %Y %H:%M %Z'):
|
||||
"""Jinja2 filter: convert a naive UTC datetime to local display time."""
|
||||
if dt is None:
|
||||
return ''
|
||||
from datetime import timezone as _tz
|
||||
tz = _get_tz(app)
|
||||
aware = dt.replace(tzinfo=_tz.utc)
|
||||
return aware.astimezone(tz).strftime(fmt)
|
||||
|
||||
app.jinja_env.filters['localtime'] = localtime_filter
|
||||
|
||||
# ── Context processors ────────────────────────────────────────────────────
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
@@ -136,7 +163,17 @@ def create_app(config_name=None):
|
||||
'logo_initials' : SystemSetting.get('logo_initials', 'TD'),
|
||||
'primary_color' : SystemSetting.get('primary_color', '#2563eb'),
|
||||
}
|
||||
return dict(unread_notifications=unread, branding=branding)
|
||||
from datetime import datetime as _dt, timezone as _tz
|
||||
from zoneinfo import ZoneInfo as _ZI
|
||||
from app.models import SystemSetting as _SS
|
||||
_tz_name = _SS.get('app_timezone', 'America/New_York') or 'America/New_York'
|
||||
try:
|
||||
_zone = _ZI(_tz_name)
|
||||
except Exception:
|
||||
_zone = _ZI('UTC')
|
||||
now_local = _dt.now(_tz.utc).astimezone(_zone)
|
||||
return dict(unread_notifications=unread, branding=branding,
|
||||
now_local=now_local, app_tz_name=_tz_name)
|
||||
|
||||
# ── DB initialisation (first run) ─────────────────────────────────────────
|
||||
with app.app_context():
|
||||
@@ -144,6 +181,9 @@ def create_app(config_name=None):
|
||||
_seed_admin(app)
|
||||
_seed_settings()
|
||||
|
||||
# ── Email ingestion background scheduler ────────────────────────────────────
|
||||
_start_email_ingestion_scheduler(app)
|
||||
|
||||
# ── SLA background scheduler ──────────────────────────────────────────────
|
||||
# APScheduler runs inside the gunicorn worker process (single-worker
|
||||
# eventlet setup), so no cross-process coordination is needed.
|
||||
@@ -188,12 +228,58 @@ def _start_sla_scheduler(app):
|
||||
app.logger.error(f'[SLA] Failed to start APScheduler: {exc}')
|
||||
|
||||
|
||||
def _start_email_ingestion_scheduler(app):
|
||||
"""Start the APScheduler job that polls the inbound mailbox for new emails.
|
||||
|
||||
The interval is read from SystemSetting at job creation time (default 5 min).
|
||||
The job is a no-op when email_ingestion_enabled = '0', so it is safe to
|
||||
always register it — no credentials are required until the admin enables it.
|
||||
"""
|
||||
import os as _os
|
||||
if _os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
return # skip reloader child process
|
||||
|
||||
try:
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from app.services.email_ingestion_service import check_inbound_email
|
||||
from app.models import SystemSetting
|
||||
|
||||
with app.app_context():
|
||||
interval = int(SystemSetting.get('email_ingestion_interval', '5') or '5')
|
||||
|
||||
scheduler = BackgroundScheduler(daemon=True)
|
||||
scheduler.add_job(
|
||||
func = check_inbound_email,
|
||||
trigger = IntervalTrigger(minutes=interval),
|
||||
id = 'email_ingest',
|
||||
name = 'Inbound Email Ingestion',
|
||||
replace_existing = True,
|
||||
args = [app],
|
||||
)
|
||||
scheduler.start()
|
||||
app.logger.info(
|
||||
f'[EMAIL INGEST] APScheduler started — polling every {interval} minute(s)'
|
||||
)
|
||||
except Exception as exc:
|
||||
app.logger.error(f'[EMAIL INGEST] Failed to start scheduler: {exc}')
|
||||
|
||||
|
||||
def _seed_settings():
|
||||
"""Ensure all required system settings exist with safe defaults."""
|
||||
from app.models import SystemSetting
|
||||
defaults = [
|
||||
('registration_enabled', 'true', 'Allow new users to self-register via /auth/register'),
|
||||
('app_timezone', 'America/New_York', 'Display timezone for all dates and times in the UI'),
|
||||
('sla_critical_hours', '4', 'Hours before a CRITICAL ticket is considered overdue'),
|
||||
('email_ingestion_enabled', '0', 'Enable automatic ticket creation from inbound email (1=on, 0=off)'),
|
||||
('email_ingestion_host', '', 'IMAP server hostname (e.g. imap.gmail.com)'),
|
||||
('email_ingestion_port', '993', 'IMAP SSL port'),
|
||||
('email_ingestion_user', '', 'Mailbox username / email address'),
|
||||
('email_ingestion_password', '', 'Mailbox password (stored in plaintext — use a dedicated app password)'),
|
||||
('email_ingestion_folder', 'INBOX', 'IMAP folder to watch for new mail'),
|
||||
('email_ingestion_move_to', 'Processed', 'IMAP folder to move processed mail into'),
|
||||
('email_ingestion_interval', '5', 'Poll interval in minutes'),
|
||||
('sla_high_hours', '8', 'Hours before a HIGH ticket is considered overdue'),
|
||||
('sla_medium_hours', '48', 'Hours before a MEDIUM ticket is considered overdue'),
|
||||
('sla_low_hours', '120', 'Hours before a LOW ticket is considered overdue'),
|
||||
|
||||
Reference in New Issue
Block a user