push
This commit is contained in:
165
app/__init__.py
Normal file
165
app/__init__.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
from flask import Flask, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
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 _bootstrap_database(app):
|
||||
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
|
||||
Reference in New Issue
Block a user