2026-03-05 12:13:22 -06:00
|
|
|
import os
|
2026-03-06 00:03:06 -06:00
|
|
|
|
2026-03-12 10:23:22 -05:00
|
|
|
from flask import Flask, send_from_directory, jsonify
|
|
|
|
|
from werkzeug.exceptions import HTTPException, NotFound, BadRequest
|
2026-03-05 14:05:41 -06:00
|
|
|
from sqlalchemy import text
|
2026-03-06 00:03:06 -06:00
|
|
|
|
2026-03-05 12:13:22 -06:00
|
|
|
from .extensions import db, migrate, cors
|
|
|
|
|
from config import config
|
|
|
|
|
|
2026-03-06 00:03:06 -06:00
|
|
|
|
2026-03-05 12:13:22 -06:00
|
|
|
def create_app(config_name=None):
|
|
|
|
|
if config_name is None:
|
|
|
|
|
config_name = os.environ.get('FLASK_ENV', 'production')
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__, static_folder='static', static_url_path='')
|
|
|
|
|
app.config.from_object(config.get(config_name, config['default']))
|
|
|
|
|
|
|
|
|
|
db.init_app(app)
|
|
|
|
|
migrate.init_app(app, db)
|
|
|
|
|
cors.init_app(app, resources={r'/api/*': {'origins': '*'}})
|
|
|
|
|
|
|
|
|
|
from .routes.projects import projects_bp
|
|
|
|
|
from .routes.deliverables import deliverables_bp
|
2026-03-06 00:03:06 -06:00
|
|
|
|
2026-03-05 12:13:22 -06:00
|
|
|
app.register_blueprint(projects_bp, url_prefix='/api')
|
|
|
|
|
app.register_blueprint(deliverables_bp, url_prefix='/api')
|
|
|
|
|
|
|
|
|
|
with app.app_context():
|
|
|
|
|
db.create_all()
|
2026-03-05 14:05:41 -06:00
|
|
|
_run_migrations()
|
2026-03-05 12:13:22 -06:00
|
|
|
|
2026-03-12 10:36:02 -05:00
|
|
|
@app.route('/')
|
|
|
|
|
def index():
|
|
|
|
|
return send_from_directory(app.static_folder, 'index.html')
|
|
|
|
|
|
2026-03-12 10:23:22 -05:00
|
|
|
@app.errorhandler(404)
|
2026-03-12 10:36:02 -05:00
|
|
|
def handle_404(e):
|
|
|
|
|
if request.path.startswith('/api/'):
|
|
|
|
|
return jsonify({'error': 'Resource not found', 'message': str(e)}), 404
|
|
|
|
|
return send_from_directory(app.static_folder, 'index.html')
|
2026-03-12 10:23:22 -05:00
|
|
|
|
|
|
|
|
@app.errorhandler(400)
|
|
|
|
|
def bad_request(e):
|
|
|
|
|
return jsonify({'error': 'Bad request', 'message': str(e)}), 400
|
|
|
|
|
|
|
|
|
|
@app.errorhandler(Exception)
|
|
|
|
|
def handle_exception(e):
|
|
|
|
|
if isinstance(e, HTTPException):
|
|
|
|
|
return jsonify({'error': e.name, 'message': e.description}), e.code
|
|
|
|
|
app.logger.error(f"Unhandled exception: {str(e)}", exc_info=True)
|
|
|
|
|
return jsonify({'error': 'Internal server error', 'message': 'An unexpected error occurred'}), 500
|
2026-03-05 12:13:22 -06:00
|
|
|
|
|
|
|
|
return app
|
2026-03-05 14:05:41 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_migrations():
|
|
|
|
|
"""
|
|
|
|
|
Safe idempotent migrations for existing databases.
|
|
|
|
|
ALTER TABLE is a no-op if the column already exists (exception silently caught).
|
|
|
|
|
Add new columns here as the schema evolves.
|
|
|
|
|
"""
|
|
|
|
|
migrations = [
|
|
|
|
|
'ALTER TABLE projects ADD COLUMN drive_url VARCHAR(500)',
|
2026-03-06 00:03:06 -06:00
|
|
|
'ALTER TABLE projects ADD COLUMN archived_at DATETIME',
|
2026-03-05 14:05:41 -06:00
|
|
|
]
|
2026-03-06 00:03:06 -06:00
|
|
|
|
2026-03-05 14:05:41 -06:00
|
|
|
with db.engine.connect() as conn:
|
|
|
|
|
for stmt in migrations:
|
|
|
|
|
try:
|
|
|
|
|
conn.execute(text(stmt))
|
|
|
|
|
conn.commit()
|
|
|
|
|
except Exception:
|
|
|
|
|
# Column already exists — safe to ignore
|
|
|
|
|
pass
|