app.py 11 KB

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