helpers.py 24 KB

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