import json from datetime import datetime from flask import current_app, g, request, session, url_for from flask_babel import get_locale, gettext as _ from flask_login import current_user from app.models import Settings from app.utils.license_utils import is_license_activated from app.utils.timezone import ( get_resolved_date_format_key, get_resolved_time_format_key, get_resolved_week_start_day, get_timezone_offset_for_timezone, ) def register_context_processors(app): """Register context processors for the application""" # Register permission helpers for templates from app.utils.permissions import init_permission_helpers init_permission_helpers(app) @app.context_processor def inject_settings(): """Inject settings into all templates""" try: from app import db # Check if we have an active database session if db.session.is_active: settings = Settings.get_settings() resolved_date = get_resolved_date_format_key() resolved_time = get_resolved_time_format_key() resolved_week_start = get_resolved_week_start_day() return { "settings": settings, "currency": settings.currency, "timezone": settings.timezone, "resolved_date_format_key": resolved_date, "resolved_time_format_key": resolved_time, "resolved_week_start_day": resolved_week_start, "is_license_activated": is_license_activated(settings), } except Exception as e: # Log the error but continue with defaults print(f"Warning: Could not inject settings: {e}") # Rollback the failed transaction try: from app import db db.session.rollback() except Exception: pass pass # Return defaults if settings not available (resolved keys still work without db) try: resolved_date = get_resolved_date_format_key() resolved_time = get_resolved_time_format_key() resolved_week_start = get_resolved_week_start_day() except Exception: resolved_date = "YYYY-MM-DD" resolved_time = "24h" resolved_week_start = 1 return { "settings": None, "currency": "EUR", "timezone": "Europe/Rome", "resolved_date_format_key": resolved_date, "resolved_time_format_key": resolved_time, "resolved_week_start_day": resolved_week_start, "is_license_activated": False, } @app.context_processor def inject_globals(): """Inject global variables into all templates""" try: from app import db # Check if we have an active database session if db.session.is_active: settings = Settings.get_settings() timezone_name = settings.timezone if settings else "Europe/Rome" else: timezone_name = "Europe/Rome" except Exception as e: # Log the error but continue with defaults print(f"Warning: Could not inject globals: {e}") # Rollback the failed transaction try: from app import db db.session.rollback() except Exception: pass timezone_name = "Europe/Rome" # Resolve user-specific timezone, falling back to application timezone user_timezone = timezone_name try: if ( current_user and getattr(current_user, "is_authenticated", False) and getattr(current_user, "timezone", None) ): user_timezone = current_user.timezone except Exception: pass # Determine app version from setup.py (single source of truth) try: import os from app.config.analytics_defaults import get_version_from_setup # Get version from setup.py version_value = get_version_from_setup() # If version is "unknown", fall back to environment variable for dev mode if version_value == "unknown": env_version = os.getenv("APP_VERSION") if env_version: version_value = env_version else: # Last resort: use "dev-0" for development version_value = "dev-0" # Strip any leading 'v' prefix to avoid double 'v' in template (e.g., vv3.5.0) if version_value and version_value.startswith("v"): version_value = version_value[1:] except Exception: # Fallback if anything goes wrong version_value = "dev-0" # Current locale code (e.g., 'en', 'de') try: current_locale = str(get_locale()) except Exception: current_locale = "en" # Normalize to short code for comparisons (e.g., 'en' from 'en_US') short_locale = current_locale.split("_", 1)[0] if current_locale else "en" # Reverse-map normalized locale codes back to config keys for label lookup # 'nb' (used by Flask-Babel) should map back to 'no' (used in LANGUAGES config) display_locale = short_locale if short_locale == "nb": display_locale = "no" available_languages = current_app.config.get("LANGUAGES", {}) or {} current_language_label = available_languages.get(display_locale, short_locale.upper()) # Check if current language is RTL rtl_languages = current_app.config.get("RTL_LANGUAGES", set()) is_rtl = short_locale in rtl_languages support_purchase_url = current_app.config.get( "SUPPORT_PURCHASE_URL", "https://timetracker.drytrix.com/support.html" ) # User stats and support banner suppression for smart prompts (authenticated users only) user_stats = None support_banner_suppressed = False support_ab_variant = "control" if getattr(current_user, "is_authenticated", False): try: from app.models import DonationInteraction user_stats = DonationInteraction.get_user_engagement_metrics(current_user.id) support_banner_suppressed = DonationInteraction.has_recent_donation_click(current_user.id, days=30) # Stable A/B variant per user for support CTA experiments (control | key_first | cta_alt) support_ab_variant = ("control", "key_first", "cta_alt")[current_user.id % 3] except Exception: user_stats = {} support_banner_suppressed = False is_admin_user = bool( getattr(current_user, "is_authenticated", False) and getattr(current_user, "is_admin", False) ) support_ui_json = None layout_support_prompt = None support_usage_stats_modal = None if getattr(current_user, "is_authenticated", False): try: from app.config.support_ui import ( build_support_checkout_urls, get_long_session_minutes, get_social_proof_text, ) from app.models import Settings from app.services.support_prompt_service import SupportPromptService from app.services.usage_stats_service import UsageStatsService from app.utils.license_utils import is_license_activated settings_obj = Settings.get_settings() is_supporter_instance = bool(settings_obj and is_license_activated(settings_obj)) ui_show_donate = bool(getattr(current_user, "ui_show_donate", True)) layout_support_prompt = SupportPromptService.consume_layout_prompt( session, ui_show_donate=ui_show_donate, is_supporter=is_supporter_instance, support_banner_suppressed=support_banner_suppressed, ) usage_stats = UsageStatsService.get_for_user(current_user.id) support_usage_stats_modal = usage_stats checkout_urls = build_support_checkout_urls(current_app.config) social_line = get_social_proof_text(current_app.config) long_session_minutes = get_long_session_minutes() if not session.get("support_session_started_at"): session["support_session_started_at"] = ( datetime.utcnow().replace(microsecond=0).isoformat() + "Z" ) lp_message = "" lp_action = _("Support") if layout_support_prompt: v = layout_support_prompt.get("variant") if v == SupportPromptService.VARIANT_AFTER_REPORT: lp_message = _( "That report was quick to generate. If TimeTracker saves you time, " "consider supporting its development." ) support_ui_json = json.dumps( { "urls": checkout_urls, "stats": usage_stats, "socialProofLine": social_line, "longSessionMinutes": long_session_minutes, "isSupporter": is_supporter_instance, "sessionStartedAt": session.get("support_session_started_at"), "shareUrl": url_for("main.about", _external=True), "trackUrl": url_for("main.track_support_event"), "softPromptUrl": url_for("main.request_soft_support_prompt"), "layoutPrompt": ( { "variant": layout_support_prompt.get("variant"), "message": lp_message, "actionLabel": lp_action, } if layout_support_prompt else None ), "i18n": { "offlineNote": _( "You appear to be offline. Reconnect to open donation or checkout links." ), "shareSuccess": _("Link copied to clipboard"), "shareFail": _("Could not copy link"), "supportAction": _("Support"), "longSessionToast": _( "You have been using TimeTracker actively for a while. " "If it helps your work, consider supporting its development." ), }, }, ensure_ascii=False, ) except Exception: support_ui_json = None layout_support_prompt = None support_usage_stats_modal = None return { "app_name": "Time Tracker", "app_version": version_value, "is_admin_user": is_admin_user, "timezone": timezone_name, "timezone_offset": get_timezone_offset_for_timezone(timezone_name), "user_timezone": user_timezone, "current_locale": current_locale, "current_language_code": display_locale, # Use display locale (e.g., 'no' not 'nb') "current_language_label": current_language_label, "is_rtl": is_rtl, "available_languages": available_languages, "config": current_app.config, "support_purchase_url": support_purchase_url, "user_stats": user_stats, "support_banner_suppressed": support_banner_suppressed, "support_ab_variant": support_ab_variant, "support_ui_json": support_ui_json, "layout_support_prompt": layout_support_prompt, "support_usage_stats_modal": support_usage_stats_modal, } @app.context_processor def inject_keyboard_shortcuts_config(): """Inject keyboard shortcut config for logged-in users (for keyboard-shortcuts-advanced.js).""" try: if getattr(current_user, "is_authenticated", False): from app.utils.keyboard_shortcuts_defaults import merge_overrides overrides = getattr(current_user, "keyboard_shortcuts_overrides", None) or {} shortcuts = merge_overrides(overrides) return {"keyboard_shortcuts_config": {"shortcuts": shortcuts, "overrides": overrides}} except Exception: pass return {"keyboard_shortcuts_config": None} @app.before_request def before_request(): """Set up request-specific data""" g.request_start_time = request.start_time if hasattr(request, "start_time") else None