utils.py 5.2 KB

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