app.py 11 KB

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