app.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.app
  4. ~~~~~~~~~~~
  5. manages the app creation and configuration process
  6. :copyright: (c) 2014 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import os
  10. import logging
  11. import time
  12. from functools import partial
  13. from sqlalchemy import event
  14. from sqlalchemy.engine import Engine
  15. from flask import Flask, request
  16. from flask_login import current_user
  17. # views
  18. from flaskbb.user.views import user
  19. from flaskbb.message.views import message
  20. from flaskbb.auth.views import auth
  21. from flaskbb.management.views import management
  22. from flaskbb.forum.views import forum
  23. # models
  24. from flaskbb.user.models import User, Guest
  25. # extensions
  26. from flaskbb.extensions import (db, login_manager, mail, cache, redis_store,
  27. debugtoolbar, migrate, themes, plugin_manager,
  28. babel, csrf, allows, limiter, celery, whooshee)
  29. # various helpers
  30. from flaskbb.utils.helpers import (time_utcnow, format_date, time_since,
  31. crop_title, is_online, mark_online,
  32. forum_is_unread, topic_is_unread,
  33. render_template, render_markup)
  34. from flaskbb.utils.translations import FlaskBBDomain
  35. # permission checks (here they are used for the jinja filters)
  36. from flaskbb.utils.requirements import (IsAdmin, IsAtleastModerator,
  37. CanBanUser, CanEditUser,
  38. TplCanModerate, TplCanDeletePost,
  39. TplCanDeleteTopic, TplCanEditPost,
  40. TplCanPostTopic, TplCanPostReply)
  41. # whooshees
  42. from flaskbb.utils.search import (PostWhoosheer, TopicWhoosheer,
  43. ForumWhoosheer, UserWhoosheer)
  44. # app specific configurations
  45. from flaskbb.utils.settings import flaskbb_config
  46. def create_app(config=None):
  47. """Creates the app.
  48. :param config: The configuration object.
  49. """
  50. # Initialize the app
  51. app = Flask("flaskbb")
  52. # Use the default config and override it afterwards
  53. app.config.from_object('flaskbb.configs.default.DefaultConfig')
  54. # Update the config
  55. app.config.from_object(config)
  56. # try to update the config via the environment variable
  57. app.config.from_envvar("FLASKBB_SETTINGS", silent=True)
  58. configure_celery_app(app, celery)
  59. configure_blueprints(app)
  60. configure_extensions(app)
  61. configure_template_filters(app)
  62. configure_context_processors(app)
  63. configure_before_handlers(app)
  64. configure_errorhandlers(app)
  65. configure_logging(app)
  66. return app
  67. def configure_celery_app(app, celery):
  68. """Configures the celery app."""
  69. app.config.update({'BROKER_URL': app.config["CELERY_BROKER_URL"]})
  70. celery.conf.update(app.config)
  71. TaskBase = celery.Task
  72. class ContextTask(TaskBase):
  73. abstract = True
  74. def __call__(self, *args, **kwargs):
  75. with app.app_context():
  76. return TaskBase.__call__(self, *args, **kwargs)
  77. celery.Task = ContextTask
  78. def configure_blueprints(app):
  79. app.register_blueprint(forum, url_prefix=app.config["FORUM_URL_PREFIX"])
  80. app.register_blueprint(user, url_prefix=app.config["USER_URL_PREFIX"])
  81. app.register_blueprint(auth, url_prefix=app.config["AUTH_URL_PREFIX"])
  82. app.register_blueprint(
  83. management, url_prefix=app.config["ADMIN_URL_PREFIX"]
  84. )
  85. app.register_blueprint(
  86. message, url_prefix=app.config["MESSAGE_URL_PREFIX"]
  87. )
  88. def configure_extensions(app):
  89. """Configures the extensions."""
  90. # Flask-WTF CSRF
  91. csrf.init_app(app)
  92. # Flask-Plugins
  93. plugin_manager.init_app(app)
  94. # Flask-SQLAlchemy
  95. db.init_app(app)
  96. # Flask-Migrate
  97. migrate.init_app(app, db)
  98. # Flask-Mail
  99. mail.init_app(app)
  100. # Flask-Cache
  101. cache.init_app(app)
  102. # Flask-Debugtoolbar
  103. debugtoolbar.init_app(app)
  104. # Flask-Themes
  105. themes.init_themes(app, app_identifier="flaskbb")
  106. # Flask-And-Redis
  107. redis_store.init_app(app)
  108. # Flask-Limiter
  109. limiter.init_app(app)
  110. # Flask-Whooshee
  111. whooshee.init_app(app)
  112. whooshee.register_whoosheer(PostWhoosheer)
  113. whooshee.register_whoosheer(TopicWhoosheer)
  114. whooshee.register_whoosheer(ForumWhoosheer)
  115. whooshee.register_whoosheer(UserWhoosheer)
  116. # Flask-Login
  117. login_manager.login_view = app.config["LOGIN_VIEW"]
  118. login_manager.refresh_view = app.config["REAUTH_VIEW"]
  119. login_manager.login_message_category = app.config["LOGIN_MESSAGE_CATEGORY"]
  120. login_manager.needs_refresh_message_category = \
  121. app.config["REFRESH_MESSAGE_CATEGORY"]
  122. login_manager.anonymous_user = Guest
  123. @login_manager.user_loader
  124. def load_user(user_id):
  125. """Loads the user. Required by the `login` extension."""
  126. user_instance = User.query.filter_by(id=user_id).first()
  127. if user_instance:
  128. return user_instance
  129. else:
  130. return None
  131. login_manager.init_app(app)
  132. # Flask-BabelEx
  133. babel.init_app(app=app, default_domain=FlaskBBDomain(app))
  134. @babel.localeselector
  135. def get_locale():
  136. # if a user is logged in, use the locale from the user settings
  137. if current_user and \
  138. current_user.is_authenticated and current_user.language:
  139. return current_user.language
  140. # otherwise we will just fallback to the default language
  141. return flaskbb_config["DEFAULT_LANGUAGE"]
  142. # Flask-Allows
  143. allows.init_app(app)
  144. allows.identity_loader(lambda: current_user)
  145. def configure_template_filters(app):
  146. """Configures the template filters."""
  147. filters = {}
  148. filters['markup'] = render_markup
  149. filters['format_date'] = format_date
  150. filters['time_since'] = time_since
  151. filters['is_online'] = is_online
  152. filters['crop_title'] = crop_title
  153. filters['forum_is_unread'] = forum_is_unread
  154. filters['topic_is_unread'] = topic_is_unread
  155. permissions = [
  156. ('is_admin', IsAdmin),
  157. ('is_moderator', IsAtleastModerator),
  158. ('is_admin_or_moderator', IsAtleastModerator),
  159. ('can_edit_user', CanEditUser),
  160. ('can_ban_user', CanBanUser),
  161. ]
  162. filters.update([
  163. (name, partial(perm, request=request)) for name, perm in permissions
  164. ])
  165. # these create closures
  166. filters['can_moderate'] = TplCanModerate(request)
  167. filters['post_reply'] = TplCanPostReply(request)
  168. filters['edit_post'] = TplCanEditPost(request)
  169. filters['delete_post'] = TplCanDeletePost(request)
  170. filters['post_topic'] = TplCanPostTopic(request)
  171. filters['delete_topic'] = TplCanDeleteTopic(request)
  172. app.jinja_env.filters.update(filters)
  173. def configure_context_processors(app):
  174. """Configures the context processors."""
  175. @app.context_processor
  176. def inject_flaskbb_config():
  177. """Injects the ``flaskbb_config`` config variable into the
  178. templates.
  179. """
  180. return dict(flaskbb_config=flaskbb_config)
  181. def configure_before_handlers(app):
  182. """Configures the before request handlers."""
  183. @app.before_request
  184. def update_lastseen():
  185. """Updates `lastseen` before every reguest if the user is
  186. authenticated."""
  187. if current_user.is_authenticated:
  188. current_user.lastseen = time_utcnow()
  189. db.session.add(current_user)
  190. db.session.commit()
  191. if app.config["REDIS_ENABLED"]:
  192. @app.before_request
  193. def mark_current_user_online():
  194. if current_user.is_authenticated:
  195. mark_online(current_user.username)
  196. else:
  197. mark_online(request.remote_addr, guest=True)
  198. def configure_errorhandlers(app):
  199. """Configures the error handlers."""
  200. @app.errorhandler(403)
  201. def forbidden_page(error):
  202. return render_template("errors/forbidden_page.html"), 403
  203. @app.errorhandler(404)
  204. def page_not_found(error):
  205. return render_template("errors/page_not_found.html"), 404
  206. @app.errorhandler(500)
  207. def server_error_page(error):
  208. return render_template("errors/server_error.html"), 500
  209. def configure_logging(app):
  210. """Configures logging."""
  211. logs_folder = os.path.join(app.root_path, os.pardir, "logs")
  212. from logging.handlers import SMTPHandler
  213. formatter = logging.Formatter(
  214. '%(asctime)s %(levelname)s: %(message)s '
  215. '[in %(pathname)s:%(lineno)d]')
  216. info_log = os.path.join(logs_folder, app.config['INFO_LOG'])
  217. info_file_handler = logging.handlers.RotatingFileHandler(
  218. info_log,
  219. maxBytes=100000,
  220. backupCount=10
  221. )
  222. info_file_handler.setLevel(logging.INFO)
  223. info_file_handler.setFormatter(formatter)
  224. app.logger.addHandler(info_file_handler)
  225. error_log = os.path.join(logs_folder, app.config['ERROR_LOG'])
  226. error_file_handler = logging.handlers.RotatingFileHandler(
  227. error_log,
  228. maxBytes=100000,
  229. backupCount=10
  230. )
  231. error_file_handler.setLevel(logging.ERROR)
  232. error_file_handler.setFormatter(formatter)
  233. app.logger.addHandler(error_file_handler)
  234. if app.config["SEND_LOGS"]:
  235. mail_handler = \
  236. SMTPHandler(
  237. app.config['MAIL_SERVER'],
  238. app.config['MAIL_DEFAULT_SENDER'],
  239. app.config['ADMINS'],
  240. 'application error, no admins specified',
  241. (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
  242. )
  243. mail_handler.setLevel(logging.ERROR)
  244. mail_handler.setFormatter(formatter)
  245. app.logger.addHandler(mail_handler)
  246. if app.config["SQLALCHEMY_ECHO"]:
  247. # Ref: http://stackoverflow.com/a/8428546
  248. @event.listens_for(Engine, "before_cursor_execute")
  249. def before_cursor_execute(conn, cursor, statement,
  250. parameters, context, executemany):
  251. conn.info.setdefault('query_start_time', []).append(time.time())
  252. @event.listens_for(Engine, "after_cursor_execute")
  253. def after_cursor_execute(conn, cursor, statement,
  254. parameters, context, executemany):
  255. total = time.time() - conn.info['query_start_time'].pop(-1)
  256. app.logger.debug("Total Time: %f", total)