Files
ksef_app/app/__init__.py
Mateusz Gruszczyński 51da117ab4 switch to debian in docker
2026-03-20 09:40:56 +01:00

185 lines
8.3 KiB
Python

import time
from pathlib import Path
from dotenv import load_dotenv
from flask import Flask, render_template, request, url_for
from flask_login import current_user
from sqlalchemy import inspect, text
from sqlalchemy.exc import OperationalError, SQLAlchemyError
from werkzeug.middleware.proxy_fix import ProxyFix
from config import Config
from redis.exceptions import RedisError
from app.cli import register_cli
from app.extensions import db, migrate, login_manager, csrf, limiter
from app.logging_config import configure_logging
from app.scheduler import init_scheduler
from app.utils.formatters import pln
import hashlib
def _ensure_storage(app):
Path(app.instance_path).mkdir(parents=True, exist_ok=True)
for key in ['ARCHIVE_PATH', 'PDF_PATH', 'BACKUP_PATH', 'CERTS_PATH']:
app.config[key].mkdir(parents=True, exist_ok=True)
def _ensure_column(table_name: str, column_name: str, ddl: str):
inspector = inspect(db.engine)
columns = {col['name'] for col in inspector.get_columns(table_name)} if inspector.has_table(table_name) else set()
if columns and column_name not in columns:
db.session.execute(text(f'ALTER TABLE {table_name} ADD COLUMN {ddl}'))
db.session.commit()
def _wait_for_database(app, attempts: int = 30, delay: float = 1.0) -> bool:
for attempt in range(1, attempts + 1):
try:
with db.engine.connect() as conn:
conn.execute(text('SELECT 1'))
if attempt > 1:
app.logger.info('Database became available after %s attempt(s).', attempt)
return True
except OperationalError:
app.logger.warning('Database not ready yet (%s/%s). Waiting...', attempt, attempts)
time.sleep(delay)
return False
def _bootstrap_database(app):
if not _wait_for_database(app):
app.logger.error('Database is still unavailable after waiting. Skipping bootstrap.')
return
try:
db.create_all()
patches = [
('user', 'theme_preference', "theme_preference VARCHAR(20) DEFAULT 'light' NOT NULL"),
('user', 'is_blocked', 'is_blocked BOOLEAN DEFAULT 0 NOT NULL'),
('user', 'force_password_change', 'force_password_change BOOLEAN DEFAULT 0 NOT NULL'),
('app_setting', 'is_encrypted', 'is_encrypted BOOLEAN DEFAULT 0 NOT NULL'),
('invoice', 'company_id', 'company_id INTEGER'),
('invoice', 'source', "source VARCHAR(32) DEFAULT 'ksef' NOT NULL"),
('invoice', 'customer_id', 'customer_id INTEGER'),
('invoice', 'issued_to_ksef_at', 'issued_to_ksef_at DATETIME'),
('invoice', 'issued_status', "issued_status VARCHAR(32) DEFAULT 'received' NOT NULL"),
('sync_log', 'company_id', 'company_id INTEGER'),
('sync_log', 'total', 'total INTEGER DEFAULT 0'),
('company', 'note', 'note TEXT'),
('company', 'regon', "regon VARCHAR(32) DEFAULT ''"),
('company', 'address', "address VARCHAR(255) DEFAULT ''"),
('customer', 'regon', "regon VARCHAR(32) DEFAULT ''"),
]
for table, col, ddl in patches:
_ensure_column(table, col, ddl)
app.logger.info('Database bootstrap checked.')
except SQLAlchemyError:
app.logger.exception('Automatic database bootstrap failed.')
def _asset_hash(app: Flask, filename: str) -> str:
static_file = Path(app.static_folder) / filename
if not static_file.exists() or not static_file.is_file():
return 'dev'
digest = hashlib.sha256(static_file.read_bytes()).hexdigest()
return digest[:12]
def create_app(config_class=Config):
load_dotenv()
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(config_class)
app.config['RATELIMIT_STORAGE_URI'] = app.config.get('REDIS_URL', 'memory://')
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
_ensure_storage(app)
configure_logging(app)
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
csrf.init_app(app)
from app.services.redis_service import RedisService
if app.config.get('RATELIMIT_STORAGE_URI', '').startswith('redis://') and not RedisService.available(app):
app.logger.warning('Redis niedostępny przy starcie - limiter przełączony na memory://')
app.config['RATELIMIT_STORAGE_URI'] = 'memory://'
limiter.init_app(app)
app.jinja_env.filters['pln'] = pln
from app.models import user, invoice, sync_log, notification, setting, audit_log, company, catalog # noqa
from app.auth.routes import bp as auth_bp
from app.dashboard.routes import bp as dashboard_bp
from app.invoices.routes import bp as invoices_bp
from app.settings.routes import bp as settings_bp
from app.notifications.routes import bp as notifications_bp
from app.api.routes import bp as api_bp
from app.admin.routes import bp as admin_bp
from app.nfz.routes import bp as nfz_bp
app.register_blueprint(auth_bp)
app.register_blueprint(dashboard_bp)
app.register_blueprint(invoices_bp)
app.register_blueprint(settings_bp)
app.register_blueprint(notifications_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(nfz_bp)
app.register_blueprint(api_bp, url_prefix='/api')
register_cli(app)
with app.app_context():
if not app.config.get('TESTING'):
_bootstrap_database(app)
init_scheduler(app)
@app.context_processor
def inject_globals():
from app.models.setting import AppSetting
from app.services.company_service import CompanyService
theme = request.cookies.get('theme', 'light')
if getattr(current_user, 'is_authenticated', False):
theme = getattr(current_user, 'theme_preference', theme) or 'light'
else:
theme = AppSetting.get('ui.theme', theme)
current_company = CompanyService.get_current_company() if getattr(current_user, 'is_authenticated', False) else None
available_companies = CompanyService.available_for_user() if getattr(current_user, 'is_authenticated', False) else []
nfz_enabled = False
if getattr(current_user, 'is_authenticated', False) and current_company:
from app.services.settings_service import SettingsService
nfz_enabled = SettingsService.get_effective('modules.nfz_enabled', 'false', company_id=current_company.id) == 'true'
status_map = {'sent': 'Wysłano', 'success': 'Sukces', 'error': 'Błąd', 'failed': 'Błąd', 'skipped': 'Pominięto', 'queued': 'Oczekuje'}
channel_map = {'email': 'E-mail', 'pushover': 'Pushover'}
return {
'app_name': 'KSeF Manager',
'theme': theme,
'read_only_mode': (__import__('app.services.settings_service', fromlist=['SettingsService']).SettingsService.read_only_enabled(company_id=current_company.id if current_company else None) if getattr(current_user, 'is_authenticated', False) else False),
'current_company': current_company,
'available_companies': available_companies,
'nfz_module_enabled': nfz_enabled,
'static_asset': lambda filename: url_for('static', filename=filename, v=_asset_hash(app, filename)),
'global_footer_text': app.config.get('APP_FOOTER_TEXT', ''),
'status_pl': lambda value: status_map.get((value or '').lower(), value or ''),
'channel_pl': lambda value: channel_map.get((value or '').lower(), (value or '').upper() if value else ''),
}
@app.after_request
def cleanup_static_headers(response):
if request.path.startswith('/static/'):
response.headers.pop('Content-Disposition', None)
return response
@app.errorhandler(403)
def error_403(err):
return render_template('errors/403.html'), 403
@app.errorhandler(404)
def error_404(err):
return render_template('errors/404.html'), 404
@app.errorhandler(RedisError)
@app.errorhandler(ConnectionError)
def error_redis(err):
db.session.rollback()
return render_template('errors/503.html', message='Usługa cache jest chwilowo niedostępna. Aplikacja korzysta z trybu awaryjnego.'), 503
@app.errorhandler(500)
def error_500(err):
db.session.rollback()
return render_template('errors/500.html'), 500
return app