main.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.cli.commands
  4. ~~~~~~~~~~~~~~~~~~~~
  5. This module contains the main commands.
  6. :copyright: (c) 2016 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import sys
  10. import os
  11. import time
  12. import requests
  13. import binascii
  14. from datetime import datetime
  15. import click
  16. from werkzeug.utils import import_string, ImportStringError
  17. from jinja2 import Environment, FileSystemLoader
  18. from flask import current_app
  19. from flask.cli import FlaskGroup, ScriptInfo, with_appcontext
  20. from sqlalchemy_utils.functions import (database_exists, create_database,
  21. drop_database)
  22. from flask_alembic import alembic_click
  23. from flaskbb import create_app
  24. from flaskbb._compat import iteritems
  25. from flaskbb.extensions import db, whooshee, celery, alembic
  26. from flaskbb.cli.utils import (prompt_save_user, prompt_config_path,
  27. write_config, get_version, FlaskBBCLIError,
  28. EmailType)
  29. from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
  30. create_default_groups,
  31. create_default_settings, insert_bulk_data,
  32. update_settings_from_fixture)
  33. from flaskbb.utils.translations import compile_translations
  34. def make_app(script_info):
  35. config_file = getattr(script_info, "config_file")
  36. if config_file is not None:
  37. # check if config file exists
  38. if os.path.exists(os.path.abspath(config_file)):
  39. click.secho("[+] Using config from: {}".format(
  40. os.path.abspath(config_file)), fg="cyan")
  41. # config file doesn't exist, maybe it's a module
  42. else:
  43. try:
  44. import_string(config_file)
  45. click.secho("[+] Using config from: {}".format(config_file),
  46. fg="cyan")
  47. except ImportStringError:
  48. click.secho("[~] Config '{}' doesn't exist. "
  49. "Using default config.".format(config_file),
  50. fg="red")
  51. config_file = None
  52. else:
  53. # lets look for a config file in flaskbb's root folder
  54. # TODO: are there any other places we should look for the config?
  55. # Like somewhere in /etc/?
  56. # this walks back to flaskbb/ from flaskbb/flaskbb/cli/main.py
  57. # can't use current_app.root_path because it's not (yet) available
  58. config_dir = os.path.dirname(
  59. os.path.dirname(os.path.dirname(__file__))
  60. )
  61. config_file = os.path.join(config_dir, "flaskbb.cfg")
  62. if os.path.exists(config_file):
  63. click.secho("[+] Found config file 'flaskbb.cfg' in {}"
  64. .format(config_dir), fg="yellow")
  65. click.secho("[+] Using config from: {}".format(config_file),
  66. fg="cyan")
  67. else:
  68. config_file = None
  69. click.secho("[~] Using default config.", fg="yellow")
  70. return create_app(config_file)
  71. def set_config(ctx, param, value):
  72. """This will pass the config file to the create_app function."""
  73. ctx.ensure_object(ScriptInfo).config_file = value
  74. @click.group(cls=FlaskGroup, create_app=make_app, add_version_option=False)
  75. @click.option("--config", expose_value=False, callback=set_config,
  76. required=False, is_flag=False, is_eager=True, metavar="CONFIG",
  77. help="Specify the config to use in dotted module notation "
  78. "e.g. flaskbb.configs.default.DefaultConfig")
  79. @click.option("--version", expose_value=False, callback=get_version,
  80. is_flag=True, is_eager=True, help="Show the FlaskBB version.")
  81. def flaskbb():
  82. """This is the commandline interface for flaskbb."""
  83. pass
  84. flaskbb.add_command(alembic_click, "db")
  85. @flaskbb.command()
  86. @click.option("--welcome", "-w", default=True, is_flag=True,
  87. help="Disable the welcome forum.")
  88. @click.option("--force", "-f", default=False, is_flag=True,
  89. help="Doesn't ask for confirmation.")
  90. @click.option("--username", "-u", help="The username of the user.")
  91. @click.option("--email", "-e", type=EmailType(),
  92. help="The email address of the user.")
  93. @click.option("--password", "-p", help="The password of the user.")
  94. @click.option("--group", "-g", help="The group of the user.",
  95. type=click.Choice(["admin", "super_mod", "mod", "member"]))
  96. def install(welcome, force, username, email, password, group):
  97. """Installs flaskbb. If no arguments are used, an interactive setup
  98. will be run.
  99. """
  100. click.secho("[+] Installing FlaskBB...", fg="cyan")
  101. if database_exists(db.engine.url):
  102. if force or click.confirm(click.style(
  103. "Existing database found. Do you want to delete the old one and "
  104. "create a new one?", fg="magenta")
  105. ):
  106. drop_database(db.engine.url)
  107. else:
  108. sys.exit(0)
  109. create_database(db.engine.url)
  110. alembic.upgrade()
  111. click.secho("[+] Creating default settings...", fg="cyan")
  112. create_default_groups()
  113. create_default_settings()
  114. click.secho("[+] Creating admin user...", fg="cyan")
  115. prompt_save_user(username, email, password, group)
  116. if welcome:
  117. click.secho("[+] Creating welcome forum...", fg="cyan")
  118. create_welcome_forum()
  119. click.secho("[+] Compiling translations...", fg="cyan")
  120. compile_translations()
  121. click.secho("[+] FlaskBB has been successfully installed!",
  122. fg="green", bold=True)
  123. @flaskbb.command()
  124. @click.option("--test-data", "-t", default=False, is_flag=True,
  125. help="Adds some test data.")
  126. @click.option("--bulk-data", "-b", default=False, is_flag=True,
  127. help="Adds a lot of data.")
  128. @click.option("--posts", default=100,
  129. help="Number of posts to create in each topic (default: 100).")
  130. @click.option("--topics", default=100,
  131. help="Number of topics to create (default: 100).")
  132. @click.option("--force", "-f", is_flag=True,
  133. help="Will delete the database before populating it.")
  134. @click.option("--initdb", "-i", is_flag=True,
  135. help="Initializes the database before populating it.")
  136. def populate(bulk_data, test_data, posts, topics, force, initdb):
  137. """Creates the necessary tables and groups for FlaskBB."""
  138. if force:
  139. click.secho("[+] Recreating database...", fg="cyan")
  140. drop_database(db.engine.url)
  141. # do not initialize the db if -i is passed
  142. if not initdb:
  143. alembic.upgrade()
  144. if initdb:
  145. click.secho("[+] Initializing database...", fg="cyan")
  146. alembic.upgrade()
  147. if test_data:
  148. click.secho("[+] Adding some test data...", fg="cyan")
  149. create_test_data()
  150. if bulk_data:
  151. timer = time.time()
  152. topic_count, post_count = insert_bulk_data(int(topics), int(posts))
  153. elapsed = time.time() - timer
  154. click.secho("[+] It took {} seconds to create {} topics and {} posts"
  155. .format(elapsed, topic_count, post_count), fg="cyan")
  156. # this just makes the most sense for the command name; use -i to
  157. # init the db as well
  158. if not test_data:
  159. click.secho("[+] Populating the database with some defaults...",
  160. fg="cyan")
  161. create_default_groups()
  162. create_default_settings()
  163. @flaskbb.command()
  164. def reindex():
  165. """Reindexes the search index."""
  166. click.secho("[+] Reindexing search index...", fg="cyan")
  167. whooshee.reindex()
  168. @flaskbb.command()
  169. @click.option("all_latest", "--all", "-a", default=False, is_flag=True,
  170. help="Upgrades migrations AND fixtures to the latest version.")
  171. @click.option("--fixture/", "-f", default=None,
  172. help="The fixture which should be upgraded or installed.")
  173. @click.option("--force", default=False, is_flag=True,
  174. help="Forcefully upgrades the fixtures.")
  175. def upgrade(all_latest, fixture, force):
  176. """Updates the migrations and fixtures."""
  177. if all_latest:
  178. click.secho("[+] Upgrading migrations to the latest version...",
  179. fg="cyan")
  180. alembic.upgrade()
  181. if fixture or all_latest:
  182. try:
  183. settings = import_string(
  184. "flaskbb.fixtures.{}".format(fixture)
  185. )
  186. settings = settings.fixture
  187. except ImportError:
  188. raise FlaskBBCLIError("{} fixture is not available"
  189. .format(fixture), fg="red")
  190. click.secho("[+] Updating fixtures...", fg="cyan")
  191. count = update_settings_from_fixture(
  192. fixture=settings, overwrite_group=force, overwrite_setting=force
  193. )
  194. click.secho("[+] {} groups and {} settings updated.".format(
  195. len(count.keys()), len(count.values())), fg="green"
  196. )
  197. @flaskbb.command("download-emojis")
  198. @with_appcontext
  199. def download_emoji():
  200. """Downloads emojis from emoji-cheat-sheet.com.
  201. This command is probably going to be removed in future version.
  202. """
  203. click.secho("[+] Downloading emojis...", fg="cyan")
  204. HOSTNAME = "https://api.github.com"
  205. REPO = "/repos/arvida/emoji-cheat-sheet.com/contents/public/graphics/emojis" # noqa
  206. FULL_URL = "{}{}".format(HOSTNAME, REPO)
  207. DOWNLOAD_PATH = os.path.join(current_app.static_folder, "emoji")
  208. response = requests.get(FULL_URL)
  209. cached_count = 0
  210. count = 0
  211. for image in response.json():
  212. if not os.path.exists(os.path.abspath(DOWNLOAD_PATH)):
  213. raise FlaskBBCLIError(
  214. "{} does not exist.".format(os.path.abspath(DOWNLOAD_PATH)),
  215. fg="red")
  216. full_path = os.path.join(DOWNLOAD_PATH, image["name"])
  217. if not os.path.exists(full_path):
  218. count += 1
  219. f = open(full_path, 'wb')
  220. f.write(requests.get(image["download_url"]).content)
  221. f.close()
  222. if count == cached_count + 50:
  223. cached_count = count
  224. click.secho("[+] {} out of {} Emojis downloaded...".format(
  225. cached_count, len(response.json())), fg="cyan")
  226. click.secho("[+] Finished downloading {} Emojis.".format(count),
  227. fg="green")
  228. @flaskbb.command("celery", context_settings=dict(ignore_unknown_options=True,))
  229. @click.argument('celery_args', nargs=-1, type=click.UNPROCESSED)
  230. @click.option("show_help", "--help", "-h", is_flag=True,
  231. help="Shows this message and exits")
  232. @click.option("show_celery_help", "--help-celery", is_flag=True,
  233. help="Shows the celery help message")
  234. @click.pass_context
  235. @with_appcontext
  236. def start_celery(ctx, show_help, show_celery_help, celery_args):
  237. """Preconfigured wrapper around the 'celery' command.
  238. Additional CELERY_ARGS arguments are passed to celery."""
  239. if show_help:
  240. click.echo(ctx.get_help())
  241. sys.exit(0)
  242. if show_celery_help:
  243. click.echo(celery.start(argv=["--help"]))
  244. sys.exit(0)
  245. default_args = ['celery']
  246. default_args = default_args + list(celery_args)
  247. celery.start(argv=default_args)
  248. @flaskbb.command()
  249. @click.option("--server", "-s", default="gunicorn",
  250. type=click.Choice(["gunicorn", "gevent"]),
  251. help="The WSGI Server to run FlaskBB on.")
  252. @click.option("--host", "-h", default="127.0.0.1",
  253. help="The interface to bind FlaskBB to.")
  254. @click.option("--port", "-p", default="8000", type=int,
  255. help="The port to bind FlaskBB to.")
  256. @click.option("--workers", "-w", default=4,
  257. help="The number of worker processes for handling requests.")
  258. @click.option("--daemon", "-d", default=False, is_flag=True,
  259. help="Starts gunicorn as daemon.")
  260. @click.option("--config", "-c",
  261. help="The configuration file to use for FlaskBB.")
  262. def start(server, host, port, workers, config, daemon):
  263. """Starts a production ready wsgi server.
  264. TODO: Figure out a way how to forward additional args to gunicorn
  265. without causing any errors.
  266. """
  267. if server == "gunicorn":
  268. try:
  269. from gunicorn.app.base import Application
  270. class FlaskBBApplication(Application):
  271. def __init__(self, app, options=None):
  272. self.options = options or {}
  273. self.application = app
  274. super(FlaskBBApplication, self).__init__()
  275. def load_config(self):
  276. config = dict([
  277. (key, value) for key, value in iteritems(self.options)
  278. if key in self.cfg.settings and value is not None
  279. ])
  280. for key, value in iteritems(config):
  281. self.cfg.set(key.lower(), value)
  282. def load(self):
  283. return self.application
  284. options = {
  285. "bind": "{}:{}".format(host, port),
  286. "workers": workers,
  287. "daemon": daemon,
  288. }
  289. FlaskBBApplication(create_app(config=config), options).run()
  290. except ImportError:
  291. raise FlaskBBCLIError("Cannot import gunicorn. "
  292. "Make sure it is installed.", fg="red")
  293. elif server == "gevent":
  294. try:
  295. from gevent import __version__
  296. from gevent.pywsgi import WSGIServer
  297. click.secho("* Starting gevent {}".format(__version__))
  298. click.secho("* Listening on http://{}:{}/".format(host, port))
  299. http_server = WSGIServer((host, port), create_app(config=config))
  300. http_server.serve_forever()
  301. except ImportError:
  302. raise FlaskBBCLIError("Cannot import gevent. "
  303. "Make sure it is installed.", fg="red")
  304. @flaskbb.command("shell", short_help="Runs a shell in the app context.")
  305. @with_appcontext
  306. def shell_command():
  307. """Runs an interactive Python shell in the context of a given
  308. Flask application. The application will populate the default
  309. namespace of this shell according to it"s configuration.
  310. This is useful for executing small snippets of management code
  311. without having to manually configuring the application.
  312. This code snippet is taken from Flask"s cli module and modified to
  313. run IPython and falls back to the normal shell if IPython is not
  314. available.
  315. """
  316. import code
  317. banner = "Python %s on %s\nInstance Path: %s" % (
  318. sys.version,
  319. sys.platform,
  320. current_app.instance_path,
  321. )
  322. ctx = {"db": db}
  323. # Support the regular Python interpreter startup script if someone
  324. # is using it.
  325. startup = os.environ.get("PYTHONSTARTUP")
  326. if startup and os.path.isfile(startup):
  327. with open(startup, "r") as f:
  328. eval(compile(f.read(), startup, "exec"), ctx)
  329. ctx.update(current_app.make_shell_context())
  330. try:
  331. import IPython
  332. IPython.embed(banner1=banner, user_ns=ctx)
  333. except ImportError:
  334. code.interact(banner=banner, local=ctx)
  335. @flaskbb.command("urls", short_help="Show routes for the app.")
  336. @click.option("--route", "-r", "order_by", flag_value="rule", default=True,
  337. help="Order by route")
  338. @click.option("--endpoint", "-e", "order_by", flag_value="endpoint",
  339. help="Order by endpoint")
  340. @click.option("--methods", "-m", "order_by", flag_value="methods",
  341. help="Order by methods")
  342. @with_appcontext
  343. def list_urls(order_by):
  344. """Lists all available routes."""
  345. from flask import current_app
  346. rules = sorted(
  347. current_app.url_map.iter_rules(),
  348. key=lambda rule: getattr(rule, order_by)
  349. )
  350. max_rule_len = max(len(rule.rule) for rule in rules)
  351. max_rule_len = max(max_rule_len, len("Route"))
  352. max_endpoint_len = max(len(rule.endpoint) for rule in rules)
  353. max_endpoint_len = max(max_endpoint_len, len("Endpoint"))
  354. max_method_len = max(len(", ".join(rule.methods)) for rule in rules)
  355. max_method_len = max(max_method_len, len("Methods"))
  356. column_header_len = max_rule_len + max_endpoint_len + max_method_len + 4
  357. column_template = "{:<%s} {:<%s} {:<%s}" % (
  358. max_rule_len, max_endpoint_len, max_method_len
  359. )
  360. click.secho(column_template.format("Route", "Endpoint", "Methods"),
  361. fg="blue", bold=True)
  362. click.secho("=" * column_header_len, bold=True)
  363. for rule in rules:
  364. methods = ", ".join(rule.methods)
  365. click.echo(column_template.format(rule.rule, rule.endpoint, methods))
  366. @flaskbb.command("makeconfig")
  367. @click.option("--development", "-d", default=False, is_flag=True,
  368. help="Creates a development config with DEBUG set to True.")
  369. @click.option("--output", "-o", required=False,
  370. help="The path where the config file will be saved at. "
  371. "Defaults to the flaskbb's root folder.")
  372. @click.option("--force", "-f", default=False, is_flag=True,
  373. help="Overwrite any existing config file if one exists.")
  374. def generate_config(development, output, force):
  375. """Generates a FlaskBB configuration file."""
  376. config_env = Environment(
  377. loader=FileSystemLoader(os.path.join(current_app.root_path, "configs"))
  378. )
  379. config_template = config_env.get_template('config.cfg.template')
  380. if output:
  381. config_path = os.path.abspath(output)
  382. else:
  383. config_path = os.path.dirname(current_app.root_path)
  384. if os.path.exists(config_path) and not os.path.isfile(config_path):
  385. config_path = os.path.join(config_path, "flaskbb.cfg")
  386. default_conf = {
  387. "is_debug": True,
  388. "server_name": "localhost:5000",
  389. "url_scheme": "http",
  390. "database_uri": "sqlite:///" + os.path.join(
  391. os.path.dirname(current_app.root_path), "flaskbb.sqlite"),
  392. "redis_enabled": False,
  393. "redis_uri": "redis://localhost:6379",
  394. "mail_server": "localhost",
  395. "mail_port": 25,
  396. "mail_use_tls": False,
  397. "mail_use_ssl": False,
  398. "mail_username": "",
  399. "mail_password": "",
  400. "mail_sender_name": "FlaskBB Mailer",
  401. "mail_sender_address": "noreply@yourdomain",
  402. "mail_admin_address": "admin@yourdomain",
  403. "secret_key": binascii.hexlify(os.urandom(24)).decode(),
  404. "csrf_secret_key": binascii.hexlify(os.urandom(24)).decode(),
  405. "timestamp": datetime.utcnow().strftime("%A, %d. %B %Y at %H:%M")
  406. }
  407. if not force:
  408. config_path = prompt_config_path(config_path)
  409. if force and os.path.exists(config_path):
  410. click.secho("Overwriting existing config file: {}".format(config_path),
  411. fg="yellow")
  412. if development:
  413. write_config(default_conf, config_template, config_path)
  414. sys.exit(0)
  415. # SERVER_NAME
  416. click.secho("The name and port number of the server.\n"
  417. "This is needed to correctly generate URLs when no request "
  418. "context is available.", fg="cyan")
  419. default_conf["server_name"] = click.prompt(
  420. click.style("Server Name", fg="magenta"), type=str,
  421. default=default_conf.get("server_name"))
  422. # PREFERRED_URL_SCHEME
  423. click.secho("The URL Scheme is also needed in order to generate correct "
  424. "URLs when no request context is available.\n"
  425. "Choose either 'https' or 'http'.", fg="cyan")
  426. default_conf["url_scheme"] = click.prompt(
  427. click.style("URL Scheme", fg="magenta"),
  428. type=click.Choice(["https", "http"]),
  429. default=default_conf.get("url_scheme"))
  430. # SQLALCHEMY_DATABASE_URI
  431. click.secho("For Postgres use:\n"
  432. " postgresql://flaskbb@localhost:5432/flaskbb\n"
  433. "For more options see the SQLAlchemy docs:\n"
  434. " http://docs.sqlalchemy.org/en/latest/core/engines.html",
  435. fg="cyan")
  436. default_conf["database_uri"] = click.prompt(
  437. click.style("Database URI", fg="magenta"),
  438. default=default_conf.get("database_uri"))
  439. # REDIS_ENABLED
  440. click.secho("Redis will be used for things such as the task queue, "
  441. "caching and rate limiting.", fg="cyan")
  442. default_conf["redis_enabled"] = click.confirm(
  443. click.style("Would you like to use redis?", fg="magenta"),
  444. default=True) # default_conf.get("redis_enabled") is False
  445. # REDIS_URI
  446. if default_conf.get("redis_enabled", False):
  447. default_conf["redis_uri"] = click.prompt(
  448. click.style("Redis URI", fg="magenta"),
  449. default=default_conf.get("redis_uri"))
  450. else:
  451. default_conf["redis_uri"] = ""
  452. # MAIL_SERVER
  453. click.secho("To use 'localhost' make sure that you have sendmail or\n"
  454. "something similar installed. Gmail is also supprted.",
  455. fg="cyan")
  456. default_conf["mail_server"] = click.prompt(
  457. click.style("Mail Server", fg="magenta"),
  458. default=default_conf.get("mail_server"))
  459. # MAIL_PORT
  460. click.secho("The port on which the SMTP server is listening on.",
  461. fg="cyan")
  462. default_conf["mail_port"] = click.prompt(
  463. click.style("Mail Server SMTP Port", fg="magenta"),
  464. default=default_conf.get("mail_port"))
  465. # MAIL_USE_TLS
  466. click.secho("If you are using a local SMTP server like sendmail this is "
  467. "not needed. For external servers it is required.",
  468. fg="cyan")
  469. default_conf["mail_use_tls"] = click.confirm(
  470. click.style("Use TLS for sending mails?", fg="magenta"),
  471. default=default_conf.get("mail_use_tls"))
  472. # MAIL_USE_SSL
  473. click.secho("Same as above. TLS is the successor to SSL.", fg="cyan")
  474. default_conf["mail_use_ssl"] = click.confirm(
  475. click.style("Use SSL for sending mails?", fg="magenta"),
  476. default=default_conf.get("mail_use_ssl"))
  477. # MAIL_USERNAME
  478. click.secho("Not needed if you are using a local smtp server.\nFor gmail "
  479. "you have to put in your email address here.", fg="cyan")
  480. default_conf["mail_username"] = click.prompt(
  481. click.style("Mail Username", fg="magenta"),
  482. default=default_conf.get("mail_username"))
  483. # MAIL_PASSWORD
  484. click.secho("Not needed if you are using a local smtp server.\nFor gmail "
  485. "you have to put in your gmail password here.", fg="cyan")
  486. default_conf["mail_password"] = click.prompt(
  487. click.style("Mail Password", fg="magenta"),
  488. default=default_conf.get("mail_password"))
  489. # MAIL_DEFAULT_SENDER
  490. click.secho("The name of the sender. You probably want to change it to "
  491. "something like '<your_community> Mailer'.", fg="cyan")
  492. default_conf["mail_sender_name"] = click.prompt(
  493. click.style("Mail Sender Name", fg="magenta"),
  494. default=default_conf.get("mail_sender_name"))
  495. click.secho("On localhost you want to use a noreply address here. "
  496. "Use your email address for gmail here.", fg="cyan")
  497. default_conf["mail_sender_address"] = click.prompt(
  498. click.style("Mail Sender Address", fg="magenta"),
  499. default=default_conf.get("mail_sender_address"))
  500. # ADMINS
  501. click.secho("Logs and important system messages are sent to this address."
  502. "Use your email address for gmail here.", fg="cyan")
  503. default_conf["mail_admin_address"] = click.prompt(
  504. click.style("Mail Admin Email", fg="magenta"),
  505. default=default_conf.get("mail_admin_address"))
  506. write_config(default_conf, config_template, config_path)
  507. # Finished
  508. click.secho("The configuration file has been saved to:\n{cfg}\n"
  509. "Feel free to adjust it as needed."
  510. .format(cfg=config_path), fg="blue", bold=True)
  511. click.secho("Usage: \nflaskbb --config {cfg} run"
  512. .format(cfg=config_path), fg="green")