|
| 1 | +from celery.signals import after_task_publish |
| 2 | +import logging |
| 3 | +import os.path |
| 4 | +from envparse import env |
| 5 | + |
| 6 | +import sys |
| 7 | +from flask import Flask, json, make_response |
| 8 | +from flask_celeryext import FlaskCeleryExt |
| 9 | +from app.settings import get_settings, get_setts |
| 10 | +from flask_migrate import Migrate, MigrateCommand |
| 11 | +from flask_script import Manager |
| 12 | +from flask_login import current_user |
| 13 | +from flask_jwt_extended import JWTManager |
| 14 | +from flask_limiter import Limiter |
| 15 | +from datetime import timedelta |
| 16 | +from flask_cors import CORS |
| 17 | +from flask_rest_jsonapi.errors import jsonapi_errors |
| 18 | +from flask_rest_jsonapi.exceptions import JsonApiException |
| 19 | +from healthcheck import HealthCheck |
| 20 | +from apscheduler.schedulers.background import BackgroundScheduler |
| 21 | +from elasticsearch_dsl.connections import connections |
| 22 | +from pytz import utc |
| 23 | + |
| 24 | +import sqlalchemy as sa |
| 25 | + |
| 26 | +import stripe |
| 27 | +from app.settings import get_settings |
| 28 | +from app.models import db |
| 29 | +from app.api.helpers.jwt import jwt_user_loader |
| 30 | +from app.api.helpers.cache import cache |
| 31 | +from werkzeug.middleware.profiler import ProfilerMiddleware |
| 32 | +from app.views import BlueprintsManager |
| 33 | +from app.api.helpers.auth import AuthManager, is_token_blacklisted |
| 34 | +from app.api.helpers.scheduled_jobs import send_after_event_mail, send_event_fee_notification, \ |
| 35 | + send_event_fee_notification_followup, change_session_state_on_event_completion, \ |
| 36 | + expire_pending_tickets, send_monthly_event_invoice, event_invoices_mark_due |
| 37 | +from app.models.event import Event |
| 38 | +from app.models.role_invite import RoleInvite |
| 39 | +from app.views.healthcheck import health_check_celery, health_check_db, health_check_migrations, check_migrations |
| 40 | +from app.views.elastic_search import client |
| 41 | +from app.views.elastic_cron_helpers import sync_events_elasticsearch, cron_rebuild_events_elasticsearch |
| 42 | +from app.views.redis_store import redis_store |
| 43 | +from app.views.celery_ import celery |
| 44 | +from app.templates.flask_ext.jinja.filters import init_filters |
| 45 | +import sentry_sdk |
| 46 | +from sentry_sdk.integrations.flask import FlaskIntegration |
| 47 | + |
| 48 | + |
| 49 | +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 50 | + |
| 51 | +static_dir = os.path.dirname(os.path.dirname(__file__)) + "/static" |
| 52 | +template_dir = os.path.dirname(__file__) + "/templates" |
| 53 | +app = Flask(__name__, static_folder=static_dir, template_folder=template_dir) |
| 54 | +limiter = Limiter(app) |
| 55 | +env.read_envfile() |
| 56 | + |
| 57 | + |
| 58 | +class ReverseProxied(object): |
| 59 | + """ |
| 60 | + ReverseProxied flask wsgi app wrapper from http://stackoverflow.com/a/37842465/1562480 by aldel |
| 61 | + """ |
| 62 | + |
| 63 | + def __init__(self, app): |
| 64 | + self.app = app |
| 65 | + |
| 66 | + def __call__(self, environ, start_response): |
| 67 | + scheme = environ.get('HTTP_X_FORWARDED_PROTO') |
| 68 | + if scheme: |
| 69 | + environ['wsgi.url_scheme'] = scheme |
| 70 | + if os.getenv('FORCE_SSL', 'no') == 'yes': |
| 71 | + environ['wsgi.url_scheme'] = 'https' |
| 72 | + return self.app(environ, start_response) |
| 73 | + |
| 74 | + |
| 75 | +app.wsgi_app = ReverseProxied(app.wsgi_app) |
| 76 | + |
| 77 | +app_created = False |
| 78 | + |
| 79 | + |
| 80 | +def create_app(): |
| 81 | + global app_created |
| 82 | + if not app_created: |
| 83 | + BlueprintsManager.register(app) |
| 84 | + Migrate(app, db) |
| 85 | + |
| 86 | + app.config.from_object(env('APP_CONFIG', default='config.ProductionConfig')) |
| 87 | + db.init_app(app) |
| 88 | + _manager = Manager(app) |
| 89 | + _manager.add_command('db', MigrateCommand) |
| 90 | + |
| 91 | + if app.config['CACHING']: |
| 92 | + cache.init_app(app, config={'CACHE_TYPE': 'simple'}) |
| 93 | + else: |
| 94 | + cache.init_app(app, config={'CACHE_TYPE': 'null'}) |
| 95 | + |
| 96 | + stripe.api_key = 'SomeStripeKey' |
| 97 | + app.secret_key = 'super secret key' |
| 98 | + app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False |
| 99 | + app.config['FILE_SYSTEM_STORAGE_FILE_VIEW'] = 'static' |
| 100 | + |
| 101 | + app.logger.addHandler(logging.StreamHandler(sys.stdout)) |
| 102 | + app.logger.setLevel(logging.ERROR) |
| 103 | + |
| 104 | + # set up jwt |
| 105 | + app.config['JWT_HEADER_TYPE'] = 'JWT' |
| 106 | + app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(days=1) |
| 107 | + app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=365) |
| 108 | + app.config['JWT_ERROR_MESSAGE_KEY'] = 'error' |
| 109 | + app.config['JWT_TOKEN_LOCATION'] = ['cookies', 'headers'] |
| 110 | + app.config['JWT_REFRESH_COOKIE_PATH'] = '/v1/auth/token/refresh' |
| 111 | + app.config['JWT_SESSION_COOKIE'] = False |
| 112 | + app.config['JWT_BLACKLIST_ENABLED'] = True |
| 113 | + app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['refresh'] |
| 114 | + _jwt = JWTManager(app) |
| 115 | + _jwt.user_loader_callback_loader(jwt_user_loader) |
| 116 | + _jwt.token_in_blacklist_loader(is_token_blacklisted) |
| 117 | + |
| 118 | + # setup celery |
| 119 | + app.config['CELERY_BROKER_URL'] = app.config['REDIS_URL'] |
| 120 | + app.config['CELERY_RESULT_BACKEND'] = app.config['CELERY_BROKER_URL'] |
| 121 | + app.config['CELERY_ACCEPT_CONTENT'] = ['json', 'application/text'] |
| 122 | + |
| 123 | + CORS(app, resources={r"/*": {"origins": "*"}}) |
| 124 | + AuthManager.init_login(app) |
| 125 | + |
| 126 | + if app.config['TESTING'] and app.config['PROFILE']: |
| 127 | + # Profiling |
| 128 | + app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30]) |
| 129 | + |
| 130 | + # development api |
| 131 | + with app.app_context(): |
| 132 | + from app.api.admin_statistics_api.events import event_statistics |
| 133 | + from app.api.auth import auth_routes |
| 134 | + from app.api.attendees import attendee_misc_routes |
| 135 | + from app.api.bootstrap import api_v1 |
| 136 | + from app.api.celery_tasks import celery_routes |
| 137 | + from app.api.event_copy import event_copy |
| 138 | + from app.api.exports import export_routes |
| 139 | + from app.api.imports import import_routes |
| 140 | + from app.api.uploads import upload_routes |
| 141 | + from app.api.users import user_misc_routes |
| 142 | + from app.api.orders import order_misc_routes |
| 143 | + from app.api.role_invites import role_invites_misc_routes |
| 144 | + from app.api.auth import ticket_blueprint, authorised_blueprint |
| 145 | + from app.api.admin_translations import admin_blueprint |
| 146 | + from app.api.orders import alipay_blueprint |
| 147 | + from app.api.settings import admin_misc_routes |
| 148 | + |
| 149 | + app.register_blueprint(api_v1) |
| 150 | + app.register_blueprint(event_copy) |
| 151 | + app.register_blueprint(upload_routes) |
| 152 | + app.register_blueprint(export_routes) |
| 153 | + app.register_blueprint(import_routes) |
| 154 | + app.register_blueprint(celery_routes) |
| 155 | + app.register_blueprint(auth_routes) |
| 156 | + app.register_blueprint(event_statistics) |
| 157 | + app.register_blueprint(user_misc_routes) |
| 158 | + app.register_blueprint(attendee_misc_routes) |
| 159 | + app.register_blueprint(order_misc_routes) |
| 160 | + app.register_blueprint(role_invites_misc_routes) |
| 161 | + app.register_blueprint(ticket_blueprint) |
| 162 | + app.register_blueprint(authorised_blueprint) |
| 163 | + app.register_blueprint(admin_blueprint) |
| 164 | + app.register_blueprint(alipay_blueprint) |
| 165 | + app.register_blueprint(admin_misc_routes) |
| 166 | + |
| 167 | + sa.orm.configure_mappers() |
| 168 | + |
| 169 | + if app.config['SERVE_STATIC']: |
| 170 | + app.add_url_rule('/static/<path:filename>', |
| 171 | + endpoint='static', |
| 172 | + view_func=app.send_static_file) |
| 173 | + |
| 174 | + # sentry |
| 175 | + if not app_created and 'SENTRY_DSN' in app.config: |
| 176 | + sentry_sdk.init(app.config['SENTRY_DSN'], integrations=[FlaskIntegration()]) |
| 177 | + |
| 178 | + # redis |
| 179 | + redis_store.init_app(app) |
| 180 | + |
| 181 | + # elasticsearch |
| 182 | + if app.config['ENABLE_ELASTICSEARCH']: |
| 183 | + client.init_app(app) |
| 184 | + connections.add_connection('default', client.elasticsearch) |
| 185 | + with app.app_context(): |
| 186 | + try: |
| 187 | + cron_rebuild_events_elasticsearch.delay() |
| 188 | + except Exception: |
| 189 | + pass |
| 190 | + |
| 191 | + app_created = True |
| 192 | + return app, _manager, db, _jwt |
| 193 | + |
| 194 | + |
| 195 | +current_app, manager, database, jwt = create_app() |
| 196 | +init_filters(app) |
| 197 | + |
| 198 | + |
| 199 | +# http://stackoverflow.com/questions/26724623/ |
| 200 | +@app.before_request |
| 201 | +def track_user(): |
| 202 | + if current_user.is_authenticated: |
| 203 | + current_user.update_lat() |
| 204 | + |
| 205 | + |
| 206 | +def make_celery(app=None): |
| 207 | + app = app or create_app()[0] |
| 208 | + celery.conf.update(app.config) |
| 209 | + ext = FlaskCeleryExt(app) |
| 210 | + return ext.celery |
| 211 | + |
| 212 | + |
| 213 | +# Health-check |
| 214 | +health = HealthCheck(current_app, "/health-check") |
| 215 | +health.add_check(health_check_celery) |
| 216 | +health.add_check(health_check_db) |
| 217 | +with current_app.app_context(): |
| 218 | + current_app.config['MIGRATION_STATUS'] = check_migrations() |
| 219 | +health.add_check(health_check_migrations) |
| 220 | + |
| 221 | + |
| 222 | +# http://stackoverflow.com/questions/9824172/find-out-whether-celery-task-exists |
| 223 | +@after_task_publish.connect |
| 224 | +def update_sent_state(sender=None, headers=None, **kwargs): |
| 225 | + # the task may not exist if sent using `send_task` which |
| 226 | + # sends tasks by name, so fall back to the default result backend |
| 227 | + # if that is the case. |
| 228 | + task = celery.tasks.get(sender) |
| 229 | + backend = task.backend if task else celery.backend |
| 230 | + backend.store_result(headers['id'], None, 'WAITING') |
| 231 | + |
| 232 | + |
| 233 | +# register celery tasks. removing them will cause the tasks to not function. so don't remove them |
| 234 | +# it is important to register them after celery is defined to resolve circular imports |
| 235 | + |
| 236 | +from .api.helpers import tasks |
| 237 | + |
| 238 | +# import helpers.tasks |
| 239 | + |
| 240 | + |
| 241 | +scheduler = BackgroundScheduler(timezone=utc) |
| 242 | +# scheduler.add_job(send_mail_to_expired_orders, 'interval', hours=5) |
| 243 | +# scheduler.add_job(empty_trash, 'cron', hour=5, minute=30) |
| 244 | +if app.config['ENABLE_ELASTICSEARCH']: |
| 245 | + scheduler.add_job(sync_events_elasticsearch, 'interval', minutes=60) |
| 246 | + scheduler.add_job(cron_rebuild_events_elasticsearch, 'cron', day=7) |
| 247 | + |
| 248 | +scheduler.add_job(send_after_event_mail, 'cron', hour=5, minute=30) |
| 249 | +scheduler.add_job(send_event_fee_notification, 'cron', day=1) |
| 250 | +scheduler.add_job(send_event_fee_notification_followup, 'cron', day=1, month='1-12') |
| 251 | +scheduler.add_job(change_session_state_on_event_completion, 'cron', hour=5, minute=30) |
| 252 | +scheduler.add_job(expire_pending_tickets, 'cron', minute=45) |
| 253 | +scheduler.add_job(send_monthly_event_invoice, 'cron', day=1, month='1-12') |
| 254 | +scheduler.add_job(event_invoices_mark_due, 'cron', hour=5) |
| 255 | +scheduler.start() |
| 256 | + |
| 257 | + |
| 258 | +@app.errorhandler(500) |
| 259 | +def internal_server_error(error): |
| 260 | + if current_app.config['PROPOGATE_ERROR'] is True: |
| 261 | + exc = JsonApiException({'pointer': ''}, str(error)) |
| 262 | + else: |
| 263 | + exc = JsonApiException({'pointer': ''}, 'Unknown error') |
| 264 | + return make_response(json.dumps(jsonapi_errors([exc.to_dict()])), exc.status, |
| 265 | + {'Content-Type': 'application/vnd.api+json'}) |
| 266 | + |
| 267 | + |
| 268 | +if __name__ == '__main__': |
| 269 | + current_app.run() |
0 commit comments