helpers.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.utils.helpers
  4. ~~~~~~~~~~~~~~~~~~~~~
  5. A few helpers that are used by flaskbb
  6. :copyright: (c) 2014 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import ast
  10. import itertools
  11. import logging
  12. import operator
  13. import os
  14. import re
  15. import time
  16. from datetime import datetime, timedelta
  17. from email import message_from_string
  18. from functools import wraps
  19. from pkg_resources import get_distribution
  20. import requests
  21. import unidecode
  22. from babel.core import get_locale_identifier
  23. from babel.dates import format_timedelta as babel_format_timedelta
  24. from werkzeug.utils import import_string, ImportStringError
  25. from flask import flash, g, redirect, request, session, url_for
  26. from flask_allows import Permission
  27. from flask_babelplus import lazy_gettext as _
  28. from flask_login import current_user
  29. from flask_themes2 import get_themes_list, render_theme_template
  30. from flaskbb._compat import (iteritems, range_method, text_type, string_types,
  31. to_bytes, to_unicode)
  32. from flaskbb.extensions import babel, redis_store
  33. from flaskbb.utils.markup import markdown
  34. from flaskbb.utils.settings import flaskbb_config
  35. from jinja2 import Markup
  36. from PIL import ImageFile
  37. from pytz import UTC
  38. from werkzeug.local import LocalProxy
  39. logger = logging.getLogger(__name__)
  40. _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
  41. def slugify(text, delim=u'-'):
  42. """Generates an slightly worse ASCII-only slug.
  43. Taken from the Flask Snippets page.
  44. :param text: The text which should be slugified
  45. :param delim: Default "-". The delimeter for whitespace
  46. """
  47. text = unidecode.unidecode(text)
  48. result = []
  49. for word in _punct_re.split(text.lower()):
  50. if word:
  51. result.append(word)
  52. return text_type(delim.join(result))
  53. def redirect_or_next(endpoint, **kwargs):
  54. """Redirects the user back to the page they were viewing or to a specified
  55. endpoint. Wraps Flasks :func:`Flask.redirect` function.
  56. :param endpoint: The fallback endpoint.
  57. """
  58. return redirect(
  59. request.args.get('next') or endpoint, **kwargs
  60. )
  61. def render_template(template, **context): # pragma: no cover
  62. """A helper function that uses the `render_theme_template` function
  63. without needing to edit all the views
  64. """
  65. if current_user.is_authenticated and current_user.theme:
  66. theme = current_user.theme
  67. else:
  68. theme = session.get('theme', flaskbb_config['DEFAULT_THEME'])
  69. return render_theme_template(theme, template, **context)
  70. def do_topic_action(topics, user, action, reverse):
  71. """Executes a specific action for topics. Returns a list with the modified
  72. topic objects.
  73. :param topics: A iterable with ``Topic`` objects.
  74. :param user: The user object which wants to perform the action.
  75. :param action: One of the following actions: locked, important and delete.
  76. :param reverse: If the action should be done in a reversed way.
  77. For example, to unlock a topic, ``reverse`` should be
  78. set to ``True``.
  79. """
  80. if not topics:
  81. return False
  82. from flaskbb.utils.requirements import (IsAtleastModeratorInForum,
  83. CanDeleteTopic, Has)
  84. if not Permission(IsAtleastModeratorInForum(forum=topics[0].forum)):
  85. flash(_("You do not have the permissions to execute this "
  86. "action."), "danger")
  87. return False
  88. modified_topics = 0
  89. if action not in {'delete', 'hide', 'unhide'}:
  90. for topic in topics:
  91. if getattr(topic, action) and not reverse:
  92. continue
  93. setattr(topic, action, not reverse)
  94. modified_topics += 1
  95. topic.save()
  96. elif action == "delete":
  97. if not Permission(CanDeleteTopic):
  98. flash(_("You do not have the permissions to delete these topics."), "danger")
  99. return False
  100. for topic in topics:
  101. modified_topics += 1
  102. topic.delete()
  103. elif action == 'hide':
  104. if not Permission(Has('makehidden')):
  105. flash(_("You do not have the permissions to hide these topics."), "danger")
  106. return False
  107. for topic in topics:
  108. if topic.hidden:
  109. continue
  110. modified_topics += 1
  111. topic.hide(user)
  112. elif action == 'unhide':
  113. if not Permission(Has('makehidden')):
  114. flash(_("You do not have the permissions to unhide these topics."), "danger")
  115. return False
  116. for topic in topics:
  117. if not topic.hidden:
  118. continue
  119. modified_topics += 1
  120. topic.unhide()
  121. return modified_topics
  122. def get_categories_and_forums(query_result, user):
  123. """Returns a list with categories. Every category has a list for all
  124. their associated forums.
  125. The structure looks like this::
  126. [(<Category 1>,
  127. [(<Forum 1>, None),
  128. (<Forum 2>, <flaskbb.forum.models.ForumsRead at 0x38fdb50>)]),
  129. (<Category 2>,
  130. [(<Forum 3>, None),
  131. (<Forum 4>, None)])]
  132. and to unpack the values you can do this::
  133. In [110]: for category, forums in x:
  134. .....: print category
  135. .....: for forum, forumsread in forums:
  136. .....: print "\t", forum, forumsread
  137. This will print something like this:
  138. <Category 1>
  139. <Forum 1> None
  140. <Forum 2> <flaskbb.forum.models.ForumsRead object at 0x38fdb50>
  141. <Category 2>
  142. <Forum 3> None
  143. <Forum 4> None
  144. :param query_result: A tuple (KeyedTuple) with all categories and forums
  145. :param user: The user object is needed because a signed out user does not
  146. have the ForumsRead relation joined.
  147. """
  148. it = itertools.groupby(query_result, operator.itemgetter(0))
  149. forums = []
  150. if user.is_authenticated:
  151. for key, value in it:
  152. forums.append((key, [(item[1], item[2]) for item in value]))
  153. else:
  154. for key, value in it:
  155. forums.append((key, [(item[1], None) for item in value]))
  156. return forums
  157. def get_forums(query_result, user):
  158. """Returns a tuple which contains the category and the forums as list.
  159. This is the counterpart for get_categories_and_forums and especially
  160. usefull when you just need the forums for one category.
  161. For example::
  162. (<Category 2>,
  163. [(<Forum 3>, None),
  164. (<Forum 4>, None)])
  165. :param query_result: A tuple (KeyedTuple) with all categories and forums
  166. :param user: The user object is needed because a signed out user does not
  167. have the ForumsRead relation joined.
  168. """
  169. it = itertools.groupby(query_result, operator.itemgetter(0))
  170. if user.is_authenticated:
  171. for key, value in it:
  172. forums = key, [(item[1], item[2]) for item in value]
  173. else:
  174. for key, value in it:
  175. forums = key, [(item[1], None) for item in value]
  176. return forums
  177. def forum_is_unread(forum, forumsread, user):
  178. """Checks if a forum is unread
  179. :param forum: The forum that should be checked if it is unread
  180. :param forumsread: The forumsread object for the forum
  181. :param user: The user who should be checked if he has read the forum
  182. """
  183. # If the user is not signed in, every forum is marked as read
  184. if not user.is_authenticated:
  185. return False
  186. read_cutoff = time_utcnow() - timedelta(
  187. days=flaskbb_config["TRACKER_LENGTH"])
  188. # disable tracker if TRACKER_LENGTH is set to 0
  189. if flaskbb_config["TRACKER_LENGTH"] == 0:
  190. return False
  191. # If there are no topics in the forum, mark it as read
  192. if forum and forum.topic_count == 0:
  193. return False
  194. # check if the last post is newer than the tracker length
  195. if forum.last_post_id is None or forum.last_post_created < read_cutoff:
  196. return False
  197. # If the user hasn't visited a topic in the forum - therefore,
  198. # forumsread is None and we need to check if it is still unread
  199. if forum and not forumsread:
  200. return forum.last_post_created > read_cutoff
  201. try:
  202. # check if the forum has been cleared and if there is a new post
  203. # since it have been cleared
  204. if forum.last_post_created > forumsread.cleared:
  205. if forum.last_post_created < forumsread.last_read:
  206. return False
  207. except TypeError:
  208. pass
  209. # else just check if the user has read the last post
  210. return forum.last_post_created > forumsread.last_read
  211. def topic_is_unread(topic, topicsread, user, forumsread=None):
  212. """Checks if a topic is unread.
  213. :param topic: The topic that should be checked if it is unread
  214. :param topicsread: The topicsread object for the topic
  215. :param user: The user who should be checked if he has read the last post
  216. in the topic
  217. :param forumsread: The forumsread object in which the topic is. If you
  218. also want to check if the user has marked the forum as
  219. read, than you will also need to pass an forumsread
  220. object.
  221. """
  222. if not user.is_authenticated:
  223. return False
  224. read_cutoff = time_utcnow() - timedelta(
  225. days=flaskbb_config["TRACKER_LENGTH"])
  226. # disable tracker if read_cutoff is set to 0
  227. if flaskbb_config["TRACKER_LENGTH"] == 0:
  228. return False
  229. # check read_cutoff
  230. if topic.last_post.date_created < read_cutoff:
  231. return False
  232. # topicsread is none if the user has marked the forum as read
  233. # or if he hasn't visited the topic yet
  234. if topicsread is None:
  235. # user has cleared the forum - check if there is a new post
  236. if forumsread and forumsread.cleared is not None:
  237. return forumsread.cleared < topic.last_post.date_created
  238. # user hasn't read the topic yet, or there is a new post since the user
  239. # has marked the forum as read
  240. return True
  241. # check if there is a new post since the user's last topic visit
  242. return topicsread.last_read < topic.last_post.date_created
  243. def mark_online(user_id, guest=False): # pragma: no cover
  244. """Marks a user as online
  245. :param user_id: The id from the user who should be marked as online
  246. :param guest: If set to True, it will add the user to the guest activity
  247. instead of the user activity.
  248. Ref: http://flask.pocoo.org/snippets/71/
  249. """
  250. user_id = to_bytes(user_id)
  251. now = int(time.time())
  252. expires = now + (flaskbb_config['ONLINE_LAST_MINUTES'] * 60) + 10
  253. if guest:
  254. all_users_key = 'online-guests/%d' % (now // 60)
  255. user_key = 'guest-activity/%s' % user_id
  256. else:
  257. all_users_key = 'online-users/%d' % (now // 60)
  258. user_key = 'user-activity/%s' % user_id
  259. p = redis_store.pipeline()
  260. p.sadd(all_users_key, user_id)
  261. p.set(user_key, now)
  262. p.expireat(all_users_key, expires)
  263. p.expireat(user_key, expires)
  264. p.execute()
  265. def get_online_users(guest=False): # pragma: no cover
  266. """Returns all online users within a specified time range
  267. :param guest: If True, it will return the online guests
  268. """
  269. current = int(time.time()) // 60
  270. minutes = range_method(flaskbb_config['ONLINE_LAST_MINUTES'])
  271. if guest:
  272. users = redis_store.sunion(['online-guests/%d' % (current - x)
  273. for x in minutes])
  274. else:
  275. users = redis_store.sunion(['online-users/%d' % (current - x)
  276. for x in minutes])
  277. return [to_unicode(u) for u in users]
  278. def crop_title(title, length=None, suffix="..."):
  279. """Crops the title to a specified length
  280. :param title: The title that should be cropped
  281. :param suffix: The suffix which should be appended at the
  282. end of the title.
  283. """
  284. length = flaskbb_config['TITLE_LENGTH'] if length is None else length
  285. if len(title) <= length:
  286. return title
  287. return title[:length].rsplit(' ', 1)[0] + suffix
  288. def render_markup(text):
  289. """Renders the given text as markdown
  290. :param text: The text that should be rendered as markdown
  291. """
  292. return Markup(markdown.render(text))
  293. def is_online(user):
  294. """A simple check to see if the user was online within a specified
  295. time range
  296. :param user: The user who needs to be checked
  297. """
  298. return user.lastseen >= time_diff()
  299. def time_utcnow():
  300. """Returns a timezone aware utc timestamp."""
  301. return datetime.now(UTC)
  302. def time_diff():
  303. """Calculates the time difference between now and the ONLINE_LAST_MINUTES
  304. variable from the configuration.
  305. """
  306. now = time_utcnow()
  307. diff = now - timedelta(minutes=flaskbb_config['ONLINE_LAST_MINUTES'])
  308. return diff
  309. def format_date(value, format='%Y-%m-%d'):
  310. """Returns a formatted time string
  311. :param value: The datetime object that should be formatted
  312. :param format: How the result should look like. A full list of available
  313. directives is here: http://goo.gl/gNxMHE
  314. """
  315. return value.strftime(format)
  316. def format_timedelta(delta, **kwargs):
  317. """Wrapper around babel's format_timedelta to make it user language
  318. aware.
  319. """
  320. locale = flaskbb_config.get("DEFAULT_LANGUAGE", "en")
  321. if current_user.is_authenticated and current_user.language is not None:
  322. locale = current_user.language
  323. return babel_format_timedelta(delta, locale=locale, **kwargs)
  324. def time_since(time): # pragma: no cover
  325. """Returns a string representing time since e.g.
  326. 3 days ago, 5 hours ago.
  327. :param time: A datetime object
  328. """
  329. delta = time - time_utcnow()
  330. return format_timedelta(delta, add_direction=True)
  331. def format_quote(username, content):
  332. """Returns a formatted quote depending on the markup language.
  333. :param username: The username of a user.
  334. :param content: The content of the quote
  335. """
  336. profile_url = url_for('user.profile', username=username)
  337. content = "\n> ".join(content.strip().split('\n'))
  338. quote = u"**[{username}]({profile_url}) wrote:**\n> {content}\n".\
  339. format(username=username, profile_url=profile_url, content=content)
  340. return quote
  341. def get_image_info(url):
  342. """Returns the content-type, image size (kb), height and width of
  343. an image without fully downloading it.
  344. :param url: The URL of the image.
  345. """
  346. r = requests.get(url, stream=True)
  347. image_size = r.headers.get("content-length")
  348. image_size = float(image_size) / 1000 # in kilobyte
  349. image_max_size = 10000
  350. image_data = {
  351. "content_type": "",
  352. "size": image_size,
  353. "width": 0,
  354. "height": 0
  355. }
  356. # lets set a hard limit of 10MB
  357. if image_size > image_max_size:
  358. return image_data
  359. data = None
  360. parser = ImageFile.Parser()
  361. while True:
  362. data = r.raw.read(1024)
  363. if not data:
  364. break
  365. parser.feed(data)
  366. if parser.image:
  367. image_data["content_type"] = parser.image.format
  368. image_data["width"] = parser.image.size[0]
  369. image_data["height"] = parser.image.size[1]
  370. break
  371. return image_data
  372. def check_image(url):
  373. """A little wrapper for the :func:`get_image_info` function.
  374. If the image doesn't match the ``flaskbb_config`` settings it will
  375. return a tuple with a the first value is the custom error message and
  376. the second value ``False`` for not passing the check.
  377. If the check is successful, it will return ``None`` for the error message
  378. and ``True`` for the passed check.
  379. :param url: The image url to be checked.
  380. """
  381. img_info = get_image_info(url)
  382. error = None
  383. if img_info["size"] > flaskbb_config["AVATAR_SIZE"]:
  384. error = "Image is too big! {}kb are allowed.".format(
  385. flaskbb_config["AVATAR_SIZE"]
  386. )
  387. return error, False
  388. if not img_info["content_type"] in flaskbb_config["AVATAR_TYPES"]:
  389. error = "Image type {} is not allowed. Allowed types are: {}".format(
  390. img_info["content_type"],
  391. ", ".join(flaskbb_config["AVATAR_TYPES"])
  392. )
  393. return error, False
  394. if img_info["width"] > flaskbb_config["AVATAR_WIDTH"]:
  395. error = "Image is too wide! {}px width is allowed.".format(
  396. flaskbb_config["AVATAR_WIDTH"]
  397. )
  398. return error, False
  399. if img_info["height"] > flaskbb_config["AVATAR_HEIGHT"]:
  400. error = "Image is too high! {}px height is allowed.".format(
  401. flaskbb_config["AVATAR_HEIGHT"]
  402. )
  403. return error, False
  404. return error, True
  405. def get_alembic_locations(plugin_dirs):
  406. """Returns a tuple with (branchname, plugin_dir) combinations.
  407. The branchname is the name of plugin directory which should also be
  408. the unique identifier of the plugin.
  409. """
  410. branches_dirs = [
  411. tuple([os.path.basename(os.path.dirname(p)), p])
  412. for p in plugin_dirs
  413. ]
  414. return branches_dirs
  415. def get_available_themes():
  416. """Returns a list that contains all available themes. The items in the
  417. list are tuples where the first item of the tuple is the identifier and
  418. the second one the name of the theme.
  419. For example::
  420. [('aurora_mod', 'Aurora Mod')]
  421. """
  422. return [(theme.identifier, theme.name) for theme in get_themes_list()]
  423. def get_available_languages():
  424. """Returns a list that contains all available languages. The items in the
  425. list are tuples where the first item of the tuple is the locale
  426. identifier (i.e. de_AT) and the second one the display name of the locale.
  427. For example::
  428. [('de_AT', 'Deutsch (Österreich)')]
  429. """
  430. return [
  431. (get_locale_identifier((l.language, l.territory)), l.display_name)
  432. for l in babel.list_translations()
  433. ]
  434. def app_config_from_env(app, prefix="FLASKBB_"):
  435. """Retrieves the configuration variables from the environment.
  436. Set your environment variables like this::
  437. export FLASKBB_SECRET_KEY="your-secret-key"
  438. and based on the prefix, it will set the actual config variable
  439. on the ``app.config`` object.
  440. :param app: The application object.
  441. :param prefix: The prefix of the environment variables.
  442. """
  443. for key, value in iteritems(os.environ):
  444. if key.startswith(prefix):
  445. key = key[len(prefix):]
  446. try:
  447. value = ast.literal_eval(value)
  448. except (ValueError, SyntaxError):
  449. pass
  450. app.config[key] = value
  451. return app
  452. def get_flaskbb_config(app, config_file):
  453. """Returns ``None`` or the config which is either the path to a config file
  454. or an object. They can be used by ``app.config.from_pyfile`` or
  455. by ``app.config.from_object``.
  456. :param app: The app instance.
  457. :param config_file: A string which is either a module that can be
  458. imported, a path to a config file or an object.
  459. If the provided config_file can't be found, it will
  460. search for a 'flaskbb.cfg' file in the instance
  461. directory and in the project's root directory.
  462. """
  463. if config_file is not None:
  464. # config is an object
  465. if not isinstance(config_file, string_types):
  466. return config_file
  467. # config is a file
  468. if (
  469. os.path.exists(os.path.join(app.instance_path, config_file)) or
  470. os.path.exists(os.path.abspath(config_file))
  471. ):
  472. return config_file
  473. # conifg is an importable string
  474. try:
  475. return import_string(config_file)
  476. except ImportStringError:
  477. return None
  478. else:
  479. # this would be so much nicer and cleaner if we wouldn't
  480. # support the root/project dir.
  481. # this walks back to flaskbb/ from flaskbb/flaskbb/cli/main.py
  482. project_dir = os.path.dirname(
  483. os.path.dirname(os.path.dirname(__file__))
  484. )
  485. project_config = os.path.join(project_dir, "flaskbb.cfg")
  486. # instance config doesn't exist
  487. instance_config = os.path.join(app.instance_path, "flaskbb.cfg")
  488. if os.path.exists(instance_config):
  489. return instance_config
  490. # config in root directory doesn't exist
  491. if os.path.exists(project_config):
  492. return project_config
  493. class ReverseProxyPathFix(object):
  494. """Wrap the application in this middleware and configure the
  495. front-end server to add these headers, to let you quietly bind
  496. this to a URL other than / and to an HTTP scheme that is
  497. different than what is used locally.
  498. http://flask.pocoo.org/snippets/35/
  499. In wsgi.py::
  500. from flaskbb.utils.helpers import ReverseProxyPathFix
  501. flaskbb = create_app(config="flaskbb.cfg")
  502. flaskbb.wsgi_app = ReverseProxyPathFix(flaskbb.wsgi_app)
  503. and in nginx::
  504. location /forums {
  505. proxy_pass http://127.0.0.1:8000;
  506. proxy_set_header Host $host;
  507. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  508. proxy_set_header X-Scheme $scheme;
  509. proxy_set_header X-Script-Name /forums; # this part here
  510. }
  511. :param app: the WSGI application
  512. :param force_https: Force HTTPS on all routes. Defaults to ``False``.
  513. """
  514. def __init__(self, app, force_https=False):
  515. self.app = app
  516. self.force_https = force_https
  517. def __call__(self, environ, start_response):
  518. script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
  519. if script_name:
  520. environ['SCRIPT_NAME'] = script_name
  521. path_info = environ.get('PATH_INFO', '')
  522. if path_info and path_info.startswith(script_name):
  523. environ['PATH_INFO'] = path_info[len(script_name):]
  524. server = environ.get('HTTP_X_FORWARDED_SERVER_CUSTOM',
  525. environ.get('HTTP_X_FORWARDED_SERVER', ''))
  526. if server:
  527. environ['HTTP_HOST'] = server
  528. scheme = environ.get('HTTP_X_SCHEME', '')
  529. if scheme:
  530. environ['wsgi.url_scheme'] = scheme
  531. if self.force_https:
  532. environ['wsgi.url_scheme'] = 'https'
  533. return self.app(environ, start_response)
  534. def real(obj):
  535. """Unwraps a werkzeug.local.LocalProxy object if given one,
  536. else returns the object.
  537. """
  538. if isinstance(obj, LocalProxy):
  539. return obj._get_current_object()
  540. return obj
  541. def parse_pkg_metadata(dist_name):
  542. try:
  543. raw_metadata = get_distribution(dist_name).get_metadata('METADATA')
  544. except FileNotFoundError:
  545. raw_metadata = get_distribution(dist_name).get_metadata('PKG-INFO')
  546. metadata = {}
  547. # lets use the Parser from email to parse our metadata :)
  548. for key, value in message_from_string(raw_metadata).items():
  549. metadata[key.replace('-', '_').lower()] = value
  550. return metadata
  551. def anonymous_required(f):
  552. @wraps(f)
  553. def wrapper(*a, **k):
  554. if current_user is not None and current_user.is_authenticated:
  555. return redirect_or_next(url_for('forum.index'))
  556. return f(*a, **k)
  557. return wrapper
  558. def enforce_recaptcha(limiter):
  559. current_limit = getattr(g, 'view_rate_limit', None)
  560. login_recaptcha = False
  561. if current_limit is not None:
  562. window_stats = limiter.limiter.get_window_stats(*current_limit)
  563. stats_diff = flaskbb_config["AUTH_REQUESTS"] - window_stats[1]
  564. login_recaptcha = stats_diff >= flaskbb_config["LOGIN_RECAPTCHA"]
  565. return login_recaptcha
  566. def registration_enabled(f):
  567. @wraps(f)
  568. def wrapper(*a, **k):
  569. if not flaskbb_config["REGISTRATION_ENABLED"]:
  570. flash(_("The registration has been disabled."), "info")
  571. return redirect_or_next(url_for("forum.index"))
  572. return f(*a, **k)
  573. return wrapper
  574. def requires_unactivated(f):
  575. @wraps(f)
  576. def wrapper(*a, **k):
  577. if current_user.is_active or not flaskbb_config["ACTIVATE_ACCOUNT"]:
  578. flash(_("This account is already activated."), "info")
  579. return redirect(url_for('forum.index'))
  580. return f(*a, **k)
  581. return wrapper
  582. def register_view(bp_or_app, routes, view_func, *args, **kwargs):
  583. for route in routes:
  584. bp_or_app.add_url_rule(route, view_func=view_func, *args, **kwargs)