spec.py 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.plugins.spec
  4. ~~~~~~~~~~~~~~~~~~~~~~~
  5. This module provides the core FlaskBB plugin hook definitions
  6. :copyright: (c) 2017 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. from pluggy import HookspecMarker
  10. spec = HookspecMarker("flaskbb")
  11. # Setup Hooks
  12. @spec
  13. def flaskbb_extensions(app):
  14. """Hook for initializing any plugin loaded extensions."""
  15. @spec
  16. def flaskbb_load_translations():
  17. """Hook for registering translation folders."""
  18. @spec
  19. def flaskbb_load_migrations():
  20. """Hook for registering additional migrations."""
  21. @spec
  22. def flaskbb_load_blueprints(app):
  23. """Hook for registering blueprints.
  24. :param app: The application object.
  25. """
  26. @spec
  27. def flaskbb_request_processors(app):
  28. """Hook for registering pre/post request processors.
  29. :param app: The application object.
  30. """
  31. @spec
  32. def flaskbb_errorhandlers(app):
  33. """Hook for registering error handlers.
  34. :param app: The application object.
  35. """
  36. @spec
  37. def flaskbb_jinja_directives(app):
  38. """Hook for registering jinja filters, context processors, etc.
  39. :param app: The application object.
  40. """
  41. @spec
  42. def flaskbb_additional_setup(app, pluggy):
  43. """Hook for any additional setup a plugin wants to do after all other
  44. application setup has finished.
  45. For example, you could apply a WSGI middleware::
  46. @impl
  47. def flaskbb_additional_setup(app):
  48. app.wsgi_app = ProxyFix(app.wsgi_app)
  49. :param app: The application object.
  50. :param pluggy: The pluggy object.
  51. """
  52. @spec
  53. def flaskbb_load_post_markdown_class(app):
  54. """
  55. Hook for loading a mistune renderer child class in order to render
  56. markdown on posts and user signatures. All classes returned by this hook
  57. will be composed into a single class to render markdown for posts.
  58. Since all classes will be composed together, child classes should call
  59. super as appropriate and not add any new arguments to `__init__` since the
  60. class will be insantiated with predetermined arguments.
  61. Example::
  62. class YellingRenderer(mistune.Renderer):
  63. def paragraph(self, text):
  64. return super(YellingRenderer, self).paragraph(text.upper())
  65. @impl
  66. def flaskbb_load_post_markdown_class():
  67. return YellingRenderer
  68. :param app: The application object associated with the class if needed
  69. :type app: Flask
  70. """
  71. @spec
  72. def flaskbb_load_nonpost_markdown_class(app):
  73. """
  74. Hook for loading a mistune renderer child class in order to render
  75. markdown in locations other than posts, for example in category or
  76. forum descriptions. All classes returned by this hook will be composed into
  77. a single class to render markdown for nonpost content (e.g. forum and
  78. category descriptions).
  79. Since all classes will be composed together, child classes should call
  80. super as appropriate and not add any new arguments to `__init__` since the
  81. class will be insantiated with predetermined arguments.
  82. Example::
  83. class YellingRenderer(mistune.Renderer):
  84. def paragraph(self, text):
  85. return super(YellingRenderer, self).paragraph(text.upper())
  86. @impl
  87. def flaskbb_load_nonpost_markdown_class():
  88. return YellingRenderer
  89. :param app: The application object associated with the class if needed
  90. :type app: Flask
  91. """
  92. @spec
  93. def flaskbb_load_post_markdown_plugins(plugins, app):
  94. """
  95. Hook for loading mistune renderer plugins used when rendering markdown on
  96. posts and user signatures. Implementations should modify the `plugins`
  97. list directly.
  98. Example of adding plugins::
  99. from mistune.plugins import plugin_abbr, plugin_table
  100. @impl
  101. def flaskbb_load_post_markdown_plugins(plugins):
  102. # add the built-in mistune table and abbr plugins
  103. plugins.extend([plugin_abbr, plugin_table])
  104. Example of removing plugins::
  105. from flaskbb.markup import plugin_userify
  106. @impl
  107. def flaskbb_load_post_markdown_plugins(plugins):
  108. try:
  109. # remove the FlaskBB user mention link plugin
  110. plugins.remove(plugin_userify)
  111. except ValueError:
  112. # other FlaskBB plugins might beat you to removing a plugin,
  113. # which is not an error. You should not raise an exception in
  114. # this case.
  115. pass
  116. :param plugins: List of mistune plugins to load.
  117. :type plugins: list
  118. :param app: The application object.
  119. :type app: Flask
  120. .. seealso::
  121. https://mistune.readthedocs.io/en/v2.0.2/advanced.html#create-plugins
  122. Mistune plugin documentation.
  123. :data:`~flaskbb.markup.plugin_userify`
  124. FlaskBB-provided plugin that links user mentions to their profiles.
  125. :data:`~flaskbb.markup.DEFAULT_PLUGINS`
  126. List of plugins loaded by default.
  127. :func:`flaskbb_load_nonpost_markdown_plugins`
  128. Hook to modify the list of plugins for markdown rendering in
  129. non-post areas.
  130. """
  131. @spec
  132. def flaskbb_load_nonpost_markdown_plugins(plugins, app):
  133. """
  134. Hook for loading mistune renderer plugins used when rendering markdown in
  135. locations other than posts, for example in category or forum
  136. descriptions. Implementations should modify the `plugins` list directly.
  137. See :func:`flaskbb_load_post_markdown_plugins` for more details.
  138. :param plugins: List of mistune plugins to load.
  139. :type plugins: list
  140. :param app: The application object.
  141. :type app: Flask
  142. """
  143. @spec
  144. def flaskbb_cli(cli, app):
  145. """Hook for registering CLI commands.
  146. For example::
  147. @impl
  148. def flaskbb_cli(cli):
  149. @cli.command()
  150. def testplugin():
  151. click.echo("Hello Testplugin")
  152. return testplugin
  153. :param app: The application object.
  154. :param cli: The FlaskBBGroup CLI object.
  155. """
  156. @spec
  157. def flaskbb_shell_context():
  158. """Hook for registering shell context handlers
  159. Expected to return a single callable function that returns a dictionary or
  160. iterable of key value pairs.
  161. """
  162. # Event hooks
  163. @spec
  164. def flaskbb_event_post_save_before(post):
  165. """Hook for handling a post before it has been saved.
  166. :param flaskbb.forum.models.Post post: The post which triggered the event.
  167. """
  168. @spec
  169. def flaskbb_event_post_save_after(post, is_new):
  170. """Hook for handling a post after it has been saved.
  171. :param flaskbb.forum.models.Post post: The post which triggered the event.
  172. :param bool is_new: True if the post is new, False if it is an edit.
  173. """
  174. @spec
  175. def flaskbb_event_topic_save_before(topic):
  176. """Hook for handling a topic before it has been saved.
  177. :param flaskbb.forum.models.Topic topic: The topic which triggered the
  178. event.
  179. """
  180. @spec
  181. def flaskbb_event_topic_save_after(topic, is_new):
  182. """Hook for handling a topic after it has been saved.
  183. :param flaskbb.forum.models.Topic topic: The topic which triggered the
  184. event.
  185. :param bool is_new: True if the topic is new, False if it is an edit.
  186. """
  187. # TODO(anr): When pluggy 1.0 is released, mark this spec deprecated
  188. @spec
  189. def flaskbb_event_user_registered(username):
  190. """Hook for handling events after a user is registered
  191. .. warning::
  192. This hook is deprecated in favor of
  193. :func:`~flaskbb.plugins.spec.flaskbb_registration_post_processor`
  194. :param username: The username of the newly registered user.
  195. """
  196. @spec
  197. def flaskbb_gather_registration_validators():
  198. """
  199. Hook for gathering user registration validators, implementers must return
  200. a callable that accepts a
  201. :class:`~flaskbb.core.auth.registration.UserRegistrationInfo` and raises
  202. a :class:`~flaskbb.core.exceptions.ValidationError` if the registration
  203. is invalid or :class:`~flaskbb.core.exceptions.StopValidation` if
  204. validation of the registration should end immediatey.
  205. Example::
  206. def cannot_be_named_fred(user_info):
  207. if user_info.username.lower() == 'fred':
  208. raise ValidationError(('username', 'Cannot name user fred'))
  209. @impl
  210. def flaskbb_gather_registration_validators():
  211. return [cannot_be_named_fred]
  212. .. note::
  213. This is implemented as a hook that returns callables since the
  214. callables are designed to raise exceptions that are aggregated to
  215. form the failure message for the registration response.
  216. See Also: :class:`~flaskbb.core.auth.registration.UserValidator`
  217. """
  218. @spec
  219. def flaskbb_registration_failure_handler(user_info, failures):
  220. """
  221. Hook for dealing with user registration failures, receives the info
  222. that user attempted to register with as well as the errors that failed
  223. the registration.
  224. Example::
  225. from .utils import fuzz_username
  226. def has_already_registered(failures):
  227. return any(
  228. attr = "username" and "already registered" in msg
  229. for (attr, msg) in failures
  230. )
  231. def suggest_alternate_usernames(user_info, failures):
  232. if has_already_registered(failures):
  233. suggestions = fuzz_username(user_info.username)
  234. failures.append(("username", "Try: {}".format(suggestions)))
  235. @impl
  236. def flaskbb_registration_failure_handler(user_info, failures):
  237. suggest_alternate_usernames(user_info, failures)
  238. See Also: :class:`~flaskbb.core.auth.registration.RegistrationFailureHandler`
  239. """ # noqa
  240. @spec
  241. def flaskbb_registration_post_processor(user):
  242. """
  243. Hook for handling actions after a user has successfully registered. This
  244. spec receives the user object after it has been successfully persisted
  245. to the database.
  246. Example::
  247. def greet_user(user):
  248. flash(_("Thanks for registering {}".format(user.username)))
  249. @impl
  250. def flaskbb_registration_post_processor(user):
  251. greet_user(user)
  252. See Also: :class:`~flaskbb.core.auth.registration.RegistrationPostProcessor`
  253. """ # noqa
  254. @spec(firstresult=True)
  255. def flaskbb_authenticate(identifier, secret):
  256. """Hook for authenticating users in FlaskBB.
  257. This hook should return either an instance of
  258. :class:`flaskbb.user.models.User` or None.
  259. If a hook decides that all attempts for authentication
  260. should end, it may raise a
  261. :class:`flaskbb.core.exceptions.StopAuthentication`
  262. and include a reason why authentication was stopped.
  263. Only the first User result will used and the default FlaskBB
  264. authentication is tried last to give others an attempt to
  265. authenticate the user instead.
  266. See also:
  267. :class:`AuthenticationProvider<flaskbb.core.auth.AuthenticationProvider>`
  268. Example of alternative auth::
  269. def ldap_auth(identifier, secret):
  270. "basic ldap example with imaginary ldap library"
  271. user_dn = "uid={},ou=flaskbb,dc=flaskbb,dc=org"
  272. try:
  273. ldap.bind(user_dn, secret)
  274. return User.query.join(
  275. UserLDAP
  276. ).filter(
  277. UserLDAP.dn==user_dn
  278. ).with_entities(User).one()
  279. except:
  280. return None
  281. @impl
  282. def flaskbb_authenticate(identifier, secret):
  283. return ldap_auth(identifier, secret)
  284. Example of ending authentication::
  285. def prevent_login_with_too_many_failed_attempts(identifier):
  286. user = User.query.filter(
  287. db.or_(
  288. User.username == identifier,
  289. User.email == identifier
  290. )
  291. ).first()
  292. if user is not None:
  293. if has_too_many_failed_logins(user):
  294. raise StopAuthentication(_(
  295. "Your account is temporarily locked due to too many"
  296. " login attempts"
  297. ))
  298. @impl(tryfirst=True)
  299. def flaskbb_authenticate(user, identifier):
  300. prevent_login_with_too_many_failed_attempts(identifier)
  301. """
  302. @spec
  303. def flaskbb_post_authenticate(user):
  304. """Hook for handling actions that occur after a user is
  305. authenticated but before setting them as the current user.
  306. This could be used to handle MFA. However, these calls will
  307. be blocking and should be taken into account.
  308. Responses from this hook are not considered at all. If a hook
  309. should need to prevent the user from logging in, it should
  310. register itself as tryfirst and raise a
  311. :class:`flaskbb.core.exceptions.StopAuthentication`
  312. and include why the login was prevented.
  313. See also:
  314. :class:`PostAuthenticationHandler<flaskbb.core.auth.PostAuthenticationHandler>`
  315. Example::
  316. def post_auth(user):
  317. today = utcnow()
  318. if is_anniversary(today, user.date_joined):
  319. flash(_("Happy registerversary!"))
  320. @impl
  321. def flaskbb_post_authenticate(user):
  322. post_auth(user)
  323. """
  324. @spec
  325. def flaskbb_authentication_failed(identifier):
  326. """Hook for handling authentication failure events.
  327. This hook will only be called when no authentication
  328. providers successfully return a user or a
  329. :class:`flaskbb.core.exceptions.StopAuthentication`
  330. is raised during the login process.
  331. See also:
  332. :class:`AuthenticationFailureHandler<flaskbb.core.auth.AuthenticationFailureHandler>`
  333. Example::
  334. def mark_failed_logins(identifier):
  335. user = User.query.filter(
  336. db.or_(
  337. User.username == identifier,
  338. User.email == identifier
  339. )
  340. ).first()
  341. if user is not None:
  342. if user.login_attempts is None:
  343. user.login_attempts = 1
  344. else:
  345. user.login_attempts += 1
  346. user.last_failed_login = utcnow()
  347. """
  348. @spec(firstresult=True)
  349. def flaskbb_reauth_attempt(user, secret):
  350. """Hook for handling reauth in FlaskBB
  351. These hooks receive the currently authenticated user
  352. and the entered secret. Only the first response from
  353. this hook is considered -- similar to the authenticate
  354. hooks. A successful attempt should return True, otherwise
  355. None for an unsuccessful or untried reauth from an
  356. implementation. Reauth will be considered a failure if
  357. no implementation return True.
  358. If a hook decides that a reauthenticate attempt should
  359. cease, it may raise StopAuthentication.
  360. See also:
  361. :class:`ReauthenticateProvider<flaskbb.core.auth.ReauthenticateProvider>`
  362. Example of checking secret or passing to the next implementer::
  363. @impl
  364. def flaskbb_reauth_attempt(user, secret):
  365. if check_password(user.password, secret):
  366. return True
  367. Example of forcefully ending reauth::
  368. @impl
  369. def flaskbb_reauth_attempt(user, secret):
  370. if user.login_attempts > 5:
  371. raise StopAuthentication(
  372. _("Too many failed authentication attempts")
  373. )
  374. """
  375. @spec
  376. def flaskbb_post_reauth(user):
  377. """Hook called after successfully reauthenticating.
  378. These hooks are called a user has passed the flaskbb_reauth_attempt
  379. hooks but before their reauth is confirmed so a post reauth implementer
  380. may still force a reauth to fail by raising StopAuthentication.
  381. Results from these hooks are not considered.
  382. See also:
  383. :class:`PostReauthenticateHandler<flaskbb.core.auth.PostAuthenticationHandler>`
  384. """
  385. @spec
  386. def flaskbb_reauth_failed(user):
  387. """Hook called if a reauth fails.
  388. These hooks will only be called if no implementation
  389. for flaskbb_reauth_attempt returns a True result or if
  390. an implementation raises StopAuthentication.
  391. If an implementation raises ForceLogout it should register
  392. itself as trylast to give other reauth failed handlers an
  393. opprotunity to run first.
  394. See also:
  395. :class:`ReauthenticateFailureHandler<flaskbb.core.auth.ReauthenticateFailureHandler>`
  396. """
  397. # Form hooks
  398. @spec
  399. def flaskbb_form_post(form):
  400. """Hook for modifying the :class:`~flaskbb.forum.forms.ReplyForm`.
  401. For example::
  402. @impl
  403. def flaskbb_form_post(form):
  404. form.example = TextField("Example Field", validators=[
  405. DataRequired(message="This field is required"),
  406. Length(min=3, max=50)])
  407. :param form: The :class:`~flaskbb.forum.forms.ReplyForm` class.
  408. """
  409. @spec
  410. def flaskbb_form_post_save(form, post):
  411. """Hook for modifying the :class:`~flaskbb.forum.forms.ReplyForm`.
  412. This hook is called while populating the post object with
  413. the data from the form. The post object will be saved after the hook
  414. call.
  415. :param form: The form object.
  416. :param post: The post object.
  417. """
  418. @spec
  419. def flaskbb_form_topic(form):
  420. """Hook for modifying the :class:`~flaskbb.forum.forms.NewTopicForm`
  421. For example::
  422. @impl
  423. def flaskbb_form_topic(form):
  424. form.example = TextField("Example Field", validators=[
  425. DataRequired(message="This field is required"),
  426. Length(min=3, max=50)])
  427. :param form: The :class:`~flaskbb.forum.forms.NewTopicForm` class.
  428. """
  429. @spec
  430. def flaskbb_form_topic_save(form, topic):
  431. """Hook for modifying the :class:`~flaskbb.forum.forms.NewTopicForm`.
  432. This hook is called while populating the topic object with
  433. the data from the form. The topic object will be saved after the hook
  434. call.
  435. :param form: The form object.
  436. :param topic: The topic object.
  437. """
  438. @spec
  439. def flaskbb_form_registration(form):
  440. """
  441. Hook for modifying the :class:`~flaskbb.auth.forms.RegisterForm`.
  442. :param form: The form class
  443. """
  444. @spec
  445. def flaskbb_gather_password_validators(app):
  446. """
  447. Hook for gathering :class:`~flaskbb.core.changesets.ChangeSetValidator`
  448. instances specialized for handling :class:`~flaskbb.core.user.update.PasswordUpdate`
  449. This hook should return an iterable::
  450. class NotLongEnough(ChangeSetValidator):
  451. def __init__(self, min_length):
  452. self._min_length = min_length
  453. def validate(self, model, changeset):
  454. if len(changeset.new_password) < self._min_length:
  455. raise ValidationError(
  456. "new_password",
  457. "Password must be at least {} characters ".format(
  458. self._min_length
  459. )
  460. )
  461. @impl
  462. def flaskbb_gather_password_validators(app):
  463. return [NotLongEnough(app.config['MIN_PASSWORD_LENGTH'])]
  464. :param app: The current application
  465. """
  466. @spec
  467. def flaskbb_gather_email_validators(app):
  468. """
  469. Hook for gathering :class:`~flaskbb.core.changesets.ChangeSetValidator`
  470. instances specialized for :class:`~flaskbb.core.user.update.EmailUpdate`.
  471. This hook should return an iterable::
  472. class BlackListedEmailProviders(ChangeSetValidator):
  473. def __init__(self, black_list):
  474. self._black_list = black_list
  475. def validate(self, model, changeset):
  476. provider = changeset.new_email.split('@')[1]
  477. if provider in self._black_list:
  478. raise ValidationError(
  479. "new_email",
  480. "{} is a black listed email provider".format(provider)
  481. )
  482. @impl
  483. def flaskbb_gather_email_validators(app):
  484. return [BlackListedEmailProviders(app.config["EMAIL_PROVIDER_BLACK_LIST"])]
  485. :param app: The current application
  486. """
  487. @spec
  488. def flaskbb_gather_details_update_validators(app):
  489. """
  490. Hook for gathering :class:`~flaskbb.core.changesets.ChangeSetValidator`
  491. instances specialized for :class:`~flaskbb.core.user.update.UserDetailsChange`.
  492. This hook should return an iterable::
  493. class DontAllowImageSignatures(ChangeSetValidator):
  494. def __init__(self, renderer):
  495. self._renderer = renderer
  496. def validate(self, model, changeset):
  497. rendered = self._renderer.render(changeset.signature)
  498. if '<img' in rendered:
  499. raise ValidationError("signature", "No images allowed in signature")
  500. @impl
  501. def flaskbb_gather_details_update_validators(app):
  502. renderer = app.pluggy.hook.flaskbb_load_nonpost_markdown_class()
  503. return [DontAllowImageSignatures(renderer())]
  504. :param app: The current application
  505. """
  506. @spec
  507. def flaskbb_details_updated(user, details_update):
  508. """
  509. Hook for responding to a user updating their details. This hook is called
  510. after the details update has been persisted.
  511. See also :class:`~flaskbb.core.changesets.ChangeSetPostProcessor`
  512. :param user: The user whose details have been updated.
  513. :param details_update: The details change set applied to the user.
  514. """
  515. @spec
  516. def flaskbb_password_updated(user):
  517. """
  518. Hook for responding to a user updating their password. This hook is called
  519. after the password change has been persisted::
  520. @impl
  521. def flaskbb_password_updated(app, user):
  522. send_email(
  523. "Password changed",
  524. [user.email],
  525. text_body=...,
  526. html_body=...
  527. )
  528. See also :class:`~flaskbb.core.changesets.ChangeSetPostProcessor`
  529. :param user: The user that updated their password.
  530. """
  531. @spec
  532. def flaskbb_email_updated(user, email_update):
  533. """
  534. Hook for responding to a user updating their email. This hook is called after
  535. the email change has been persisted::
  536. @impl
  537. def flaskbb_email_updated(app):
  538. send_email(
  539. "Email changed",
  540. [email_change.old_email],
  541. text_body=...,
  542. html_body=...
  543. )
  544. See also :class:`~flaskbb.core.changesets.ChangeSetPostProcessor`.
  545. :param user: The user whose email was updated.
  546. :param email_update: The change set applied to the user.
  547. """
  548. @spec
  549. def flaskbb_settings_updated(user, settings_update):
  550. """
  551. Hook for responding to a user updating their settings. This hook is called after
  552. the settings change has been persisted.
  553. See also :class:`~flaskbb.core.changesets.ChangeSetPostProcessor`
  554. :param user: The user whose settings have been updated.
  555. :param settings: The settings change set applied to the user.
  556. """
  557. # Template Hooks
  558. @spec
  559. def flaskbb_tpl_navigation_before():
  560. """Hook for registering additional navigation items.
  561. in :file:`templates/layout.html`.
  562. """
  563. @spec
  564. def flaskbb_tpl_navigation_after():
  565. """Hook for registering additional navigation items.
  566. in :file:`templates/layout.html`.
  567. """
  568. @spec
  569. def flaskbb_tpl_user_nav_loggedin_before():
  570. """Hook for registering additional user navigational items
  571. which are only shown when a user is logged in.
  572. in :file:`templates/layout.html`.
  573. """
  574. @spec
  575. def flaskbb_tpl_user_nav_loggedin_after():
  576. """Hook for registering additional user navigational items
  577. which are only shown when a user is logged in.
  578. in :file:`templates/layout.html`.
  579. """
  580. @spec
  581. def flaskbb_tpl_form_registration_before(form):
  582. """This hook is emitted in the Registration form **before** the first
  583. input field but after the hidden CSRF token field.
  584. in :file:`templates/auth/register.html`.
  585. :param form: The form object.
  586. """
  587. @spec
  588. def flaskbb_tpl_form_registration_after(form):
  589. """This hook is emitted in the Registration form **after** the last
  590. input field but before the submit field.
  591. in :file:`templates/auth/register.html`.
  592. :param form: The form object.
  593. """
  594. @spec
  595. def flaskbb_tpl_form_user_details_before(form):
  596. """This hook is emitted in the Change User Details form **before** an
  597. input field is rendered.
  598. in :file:`templates/user/change_user_details.html`.
  599. :param form: The form object.
  600. """
  601. @spec
  602. def flaskbb_tpl_form_user_details_after(form):
  603. """This hook is emitted in the Change User Details form **after** the last
  604. input field has been rendered but before the submit field.
  605. in :file:`templates/user/change_user_details.html`.
  606. :param form: The form object.
  607. """
  608. @spec
  609. def flaskbb_tpl_profile_settings_menu(user):
  610. """This hook is emitted on the user settings page in order to populate the
  611. side bar menu. Implementations of this hook should return a list of tuples
  612. that are view name and display text. The display text will be provided to
  613. the translation service so it is unnecessary to supply translated text.
  614. A plugin can declare a new block by setting the view to None. If this is
  615. done, consider marking the hook implementation with `trylast=True` to
  616. avoid capturing plugins that do not create new blocks.
  617. For example::
  618. @impl(trylast=True)
  619. def flaskbb_tpl_profile_settings_menu():
  620. return [
  621. (None, 'Account Settings'),
  622. ('user.settings', 'General Settings'),
  623. ('user.change_user_details', 'Change User Details'),
  624. ('user.change_email', 'Change E-Mail Address'),
  625. ('user.change_password', 'Change Password')
  626. ]
  627. Hookwrappers for this spec should not be registered as FlaskBB
  628. supplies its own hookwrapper to flatten all the lists into a single list.
  629. in :file:`templates/user/settings_layout.html`
  630. .. versionchanged:: 2.1.0
  631. The user param. Typically this will be the current user but might not
  632. always be the current user.
  633. :param user: The user the settings menu is being rendered for.
  634. """
  635. @spec
  636. def flaskbb_tpl_profile_sidebar_links(user):
  637. """
  638. This hook is emitted on the user profile page in order to populate the
  639. sidebar menu. Implementations of this hook should return an iterable of
  640. :class:`~flaskbb.display.navigation.NavigationItem` instances::
  641. @impl
  642. def flaskbb_tpl_profile_sidebar_links(user):
  643. return [
  644. NavigationLink(
  645. endpoint="user.profile",
  646. name=_("Overview"),
  647. icon="fa fa-home",
  648. urlforkwargs={"username": user.username},
  649. ),
  650. NavigationLink(
  651. endpoint="user.view_all_topics",
  652. name=_("Topics"),
  653. icon="fa fa-comments",
  654. urlforkwargs={"username": user.username},
  655. ),
  656. NavigationLink(
  657. endpoint="user.view_all_posts",
  658. name=_("Posts"),
  659. icon="fa fa-comment",
  660. urlforkwargs={"username": user.username},
  661. ),
  662. ]
  663. .. warning::
  664. Hookwrappers for this spec should not be registered as FlaskBB registers
  665. its own hook wrapper to flatten all the results into a single list.
  666. .. versionadded:: 2.1
  667. :param user: The user the profile page belongs to.
  668. """
  669. @spec
  670. def flaskbb_tpl_admin_settings_menu(user):
  671. """This hook is emitted in the admin panel and used to add additional
  672. navigation links to the admin menu.
  673. Implementations of this hook should return a list of tuples
  674. that are view name, display text and optionally an icon.
  675. The display text will be provided to the translation service so it
  676. is unnecessary to supply translated text.
  677. For example::
  678. @impl(trylast=True)
  679. def flaskbb_tpl_admin_settings_menu():
  680. # only add this item if the user is an admin
  681. if Permission(IsAdmin, identity=current_user):
  682. return [
  683. ("myplugin.foobar", "Foobar", "fa fa-foobar")
  684. ]
  685. Hookwrappers for this spec should not be registered as FlaskBB
  686. supplies its own hookwrapper to flatten all the lists into a single list.
  687. in :file:`templates/management/management_layout.html`
  688. :param user: The current user object.
  689. """
  690. @spec
  691. def flaskbb_tpl_admin_settings_sidebar(user):
  692. """This hook is emitted in the admin panels setting tab and used
  693. to add additional navigation links to the sidebar settings menu.
  694. Implementations of this hook should return a list of tuples
  695. that are view name and display text.
  696. The display text will be provided to the translation service so it
  697. is unnecessary to supply translated text.
  698. For example::
  699. @impl(trylast=True)
  700. def flaskbb_tpl_admin_settings_menu():
  701. return [
  702. ("myplugin.foobar", "Foobar")
  703. ]
  704. Only admins can view the Settings tab.
  705. Hookwrappers for this spec should not be registered as FlaskBB
  706. supplies its own hookwrapper to flatten all the lists into a single list.
  707. in :file:`templates/management/settings.html`
  708. :param user: The current user object.
  709. """
  710. @spec
  711. def flaskbb_tpl_profile_sidebar_stats(user):
  712. """This hook is emitted on the users profile page below the standard
  713. information. For example, it can be used to add additional items
  714. such as a link to the profile.
  715. in :file:`templates/user/profile_layout.html`
  716. :param user: The user object for whom the profile is currently visited.
  717. """
  718. @spec
  719. def flaskbb_tpl_post_author_info_before(user, post):
  720. """This hook is emitted before the information about the
  721. author of a post is displayed (but after the username).
  722. in :file:`templates/forum/topic.html`
  723. :param user: The user object of the post's author.
  724. :param post: The post object.
  725. """
  726. @spec
  727. def flaskbb_tpl_post_author_info_after(user, post):
  728. """This hook is emitted after the information about the
  729. author of a post is displayed (but after the username).
  730. in :file:`templates/forum/topic.html`
  731. :param user: The user object of the post's author.
  732. :param post: The post object.
  733. """
  734. @spec
  735. def flaskbb_tpl_post_content_before(post):
  736. """Hook to do some stuff before the post content is rendered.
  737. in :file:`templates/forum/topic.html`
  738. :param post: The current post object.
  739. """
  740. @spec
  741. def flaskbb_tpl_post_content_after(post):
  742. """Hook to do some stuff after the post content is rendered.
  743. in :file:`templates/forum/topic.html`
  744. :param post: The current post object.
  745. """
  746. @spec
  747. def flaskbb_tpl_post_menu_before(post):
  748. """Hook for inserting a new item at the beginning of the post menu.
  749. in :file:`templates/forum/topic.html`
  750. :param post: The current post object.
  751. """
  752. @spec
  753. def flaskbb_tpl_post_menu_after(post):
  754. """Hook for inserting a new item at the end of the post menu.
  755. in :file:`templates/forum/topic.html`
  756. :param post: The current post object.
  757. """
  758. @spec
  759. def flaskbb_tpl_topic_controls(topic):
  760. """Hook for inserting additional topic moderation controls.
  761. in :file:`templates/forum/topic_controls.html`
  762. :param topic: The current topic object.
  763. """
  764. @spec
  765. def flaskbb_tpl_form_new_post_before(form):
  766. """Hook for inserting a new form field before the first field is
  767. rendered.
  768. For example::
  769. @impl
  770. def flaskbb_tpl_form_new_post_after(form):
  771. return render_template_string(
  772. \"""
  773. <div class="form-group">
  774. <div class="col-md-12 col-sm-12 col-xs-12">
  775. <label>{{ form.example.label.text }}</label>
  776. {{ form.example(class="form-control",
  777. placeholder=form.example.label.text) }}
  778. {%- for error in form.example.errors -%}
  779. <span class="help-block">{{error}}</span>
  780. {%- endfor -%}
  781. </div>
  782. </div>
  783. \"""
  784. in :file:`templates/forum/new_post.html`
  785. :param form: The form object.
  786. """
  787. @spec
  788. def flaskbb_tpl_form_new_post_after(form):
  789. """Hook for inserting a new form field after the last field is
  790. rendered (but before the submit field).
  791. in :file:`templates/forum/new_post.html`
  792. :param form: The form object.
  793. """
  794. @spec
  795. def flaskbb_tpl_form_new_topic_before(form):
  796. """Hook for inserting a new form field before the first field is
  797. rendered (but before the CSRF token).
  798. in :file:`templates/forum/new_topic.html`
  799. :param form: The form object.
  800. """
  801. @spec
  802. def flaskbb_tpl_form_new_topic_after(form):
  803. """Hook for inserting a new form field after the last field is
  804. rendered (but before the submit button).
  805. in :file:`templates/forum/new_topic.html`
  806. :param form: The form object.
  807. """