utils.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.cli.utils
  4. ~~~~~~~~~~~~~~~~~
  5. This module contains some utility helpers that are used across
  6. commands.
  7. :copyright: (c) 2016 by the FlaskBB Team.
  8. :license: BSD, see LICENSE for more details.
  9. """
  10. import sys
  11. import os
  12. import re
  13. import click
  14. from flask import current_app, __version__ as flask_version
  15. from flask_themes2 import get_theme
  16. from flaskbb import __version__
  17. from flaskbb.utils.populate import create_user, update_user
  18. cookiecutter_available = False
  19. try:
  20. from cookiecutter.main import cookiecutter # noqa
  21. cookiecutter_available = True
  22. except ImportError:
  23. pass
  24. _email_regex = r"[^@]+@[^@]+\.[^@]+"
  25. class FlaskBBCLIError(click.ClickException):
  26. """An exception that signals a usage error including color support.
  27. This aborts any further handling.
  28. :param styles: The style kwargs which should be forwarded to click.secho.
  29. """
  30. def __init__(self, message, **styles):
  31. click.ClickException.__init__(self, message)
  32. self.styles = styles
  33. def show(self, file=None):
  34. if file is None:
  35. file = click._compat.get_text_stderr()
  36. click.secho("[-] Error: %s" % self.format_message(), file=file,
  37. **self.styles)
  38. class EmailType(click.ParamType):
  39. """The choice type allows a value to be checked against a fixed set of
  40. supported values. All of these values have to be strings.
  41. See :ref:`choice-opts` for an example.
  42. """
  43. name = "email"
  44. def convert(self, value, param, ctx):
  45. # Exact match
  46. if re.match(_email_regex, value):
  47. return value
  48. else:
  49. self.fail(("invalid email: %s" % value), param, ctx)
  50. def __repr__(self):
  51. return "email"
  52. def validate_plugin(plugin):
  53. """Checks if a plugin is installed.
  54. TODO: Figure out how to use this in a callback. Doesn't work because
  55. the appcontext can't be found and using with_appcontext doesn't
  56. help either.
  57. """
  58. if plugin not in current_app.pluggy.list_name():
  59. raise FlaskBBCLIError("Plugin {} not found.".format(plugin), fg="red")
  60. return True
  61. def validate_theme(theme):
  62. """Checks if a theme is installed."""
  63. try:
  64. get_theme(theme)
  65. except KeyError:
  66. raise FlaskBBCLIError("Theme {} not found.".format(theme), fg="red")
  67. def check_cookiecutter(ctx, param, value):
  68. if not cookiecutter_available:
  69. raise FlaskBBCLIError(
  70. "Can't create {} because cookiecutter is not installed. "
  71. "You can install it with 'pip install cookiecutter'.".
  72. format(value), fg="red"
  73. )
  74. return value
  75. def get_version(ctx, param, value):
  76. if not value or ctx.resilient_parsing:
  77. return
  78. message = ("FlaskBB %(version)s using Flask %(flask_version)s on "
  79. "Python %(python_version)s")
  80. click.echo(message % {
  81. 'version': __version__,
  82. 'flask_version': flask_version,
  83. 'python_version': sys.version.split("\n")[0]
  84. }, color=ctx.color)
  85. ctx.exit()
  86. def prompt_save_user(username, email, password, group, only_update=False):
  87. if not username:
  88. username = click.prompt(
  89. click.style("Username", fg="magenta"), type=str,
  90. default=os.environ.get("USER", "")
  91. )
  92. if not email:
  93. email = click.prompt(
  94. click.style("Email address", fg="magenta"), type=EmailType()
  95. )
  96. if not password:
  97. password = click.prompt(
  98. click.style("Password", fg="magenta"), hide_input=True,
  99. confirmation_prompt=True
  100. )
  101. if not group:
  102. group = click.prompt(
  103. click.style("Group", fg="magenta"),
  104. type=click.Choice(["admin", "super_mod", "mod", "member"]),
  105. default="admin"
  106. )
  107. if only_update:
  108. return update_user(username, password, email, group)
  109. return create_user(username, password, email, group)
  110. def prompt_config_path(config_path):
  111. """Asks for a config path. If the path exists it will ask the user
  112. for a new path until a he enters a path that doesn't exist.
  113. :param config_path: The path to the configuration.
  114. """
  115. click.secho("The path to save this configuration file.", fg="cyan")
  116. while True:
  117. if os.path.exists(config_path) and click.confirm(click.style(
  118. "Config {cfg} exists. Do you want to overwrite it?"
  119. .format(cfg=config_path), fg="magenta")
  120. ):
  121. break
  122. config_path = click.prompt(
  123. click.style("Save to", fg="magenta"),
  124. default=config_path)
  125. if not os.path.exists(config_path):
  126. break
  127. return config_path
  128. def write_config(config, config_template, config_path):
  129. """Writes a new config file based upon the config template.
  130. :param config: A dict containing all the key/value pairs which should be
  131. used for the new configuration file.
  132. :param config_template: The config (jinja2-)template.
  133. :param config_path: The place to write the new config file.
  134. """
  135. with open(config_path, 'wb') as cfg_file:
  136. cfg_file.write(
  137. config_template.render(**config).encode("utf-8")
  138. )