main.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  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. import traceback
  15. import logging
  16. from datetime import datetime
  17. import click
  18. import click_log
  19. from celery.bin.celery import CeleryCommand
  20. from werkzeug.utils import import_string
  21. from jinja2 import Environment, FileSystemLoader
  22. from flask import current_app
  23. from flask.cli import FlaskGroup, ScriptInfo, with_appcontext
  24. from sqlalchemy_utils.functions import database_exists
  25. from flask_alembic import alembic_click
  26. from flaskbb import create_app
  27. from flaskbb._compat import iteritems
  28. from flaskbb.extensions import db, whooshee, celery, alembic
  29. from flaskbb.cli.utils import (prompt_save_user, prompt_config_path,
  30. write_config, get_version, FlaskBBCLIError,
  31. EmailType)
  32. from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
  33. create_default_groups,
  34. create_default_settings, insert_bulk_data,
  35. update_settings_from_fixture,
  36. create_latest_db)
  37. from flaskbb.utils.translations import compile_translations
  38. logger = logging.getLogger(__name__)
  39. click_log.basic_config(logger)
  40. class FlaskBBGroup(FlaskGroup):
  41. def __init__(self, *args, **kwargs):
  42. super(FlaskBBGroup, self).__init__(*args, **kwargs)
  43. self._loaded_flaskbb_plugins = False
  44. def _load_flaskbb_plugins(self, ctx):
  45. if self._loaded_flaskbb_plugins:
  46. return
  47. try:
  48. app = ctx.ensure_object(ScriptInfo).load_app()
  49. app.pluggy.hook.flaskbb_cli(cli=self, app=app)
  50. self._loaded_flaskbb_plugins = True
  51. except Exception as exc:
  52. logger.error(
  53. "Error while loading CLI Plugins",
  54. exc_info=traceback.format_exc()
  55. )
  56. def get_command(self, ctx, name):
  57. self._load_flaskbb_plugins(ctx)
  58. return super(FlaskBBGroup, self).get_command(ctx, name)
  59. def list_commands(self, ctx):
  60. self._load_flaskbb_plugins(ctx)
  61. return super(FlaskBBGroup, self).list_commands(ctx)
  62. def make_app(script_info):
  63. config_file = getattr(script_info, "config_file", None)
  64. instance_path = getattr(script_info, "instance_path", None)
  65. return create_app(config_file, instance_path)
  66. def set_config(ctx, param, value):
  67. """This will pass the config file to the create_app function."""
  68. ctx.ensure_object(ScriptInfo).config_file = value
  69. def set_instance(ctx, param, value):
  70. """This will pass the instance path on the script info which can then
  71. be used in 'make_app'."""
  72. ctx.ensure_object(ScriptInfo).instance_path = value
  73. @click.group(cls=FlaskBBGroup, create_app=make_app, add_version_option=False,
  74. invoke_without_command=True)
  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 either in dotted module "
  78. "notation e.g. 'flaskbb.configs.default.DefaultConfig' "
  79. "or by using a path like '/path/to/flaskbb.cfg'")
  80. @click.option("--instance", expose_value=False, callback=set_instance,
  81. required=False, is_flag=False, is_eager=True, metavar="PATH",
  82. help="Specify the instance path to use. By default the folder "
  83. "'instance' next to the package or module is assumed to "
  84. "be the instance path.")
  85. @click.option("--version", expose_value=False, callback=get_version,
  86. is_flag=True, is_eager=True, help="Show the FlaskBB version.")
  87. @click.pass_context
  88. @click_log.simple_verbosity_option(logger)
  89. def flaskbb(ctx):
  90. """This is the commandline interface for flaskbb."""
  91. if ctx.invoked_subcommand is None:
  92. # show the help text instead of an error
  93. # when just '--config' option has been provided
  94. click.echo(ctx.get_help())
  95. flaskbb.add_command(alembic_click, "db")
  96. @flaskbb.command()
  97. @click.option("--welcome", "-w", default=True, is_flag=True,
  98. help="Disable the welcome forum.")
  99. @click.option("--force", "-f", default=False, is_flag=True,
  100. help="Doesn't ask for confirmation.")
  101. @click.option("--username", "-u", help="The username of the user.")
  102. @click.option("--email", "-e", type=EmailType(),
  103. help="The email address of the user.")
  104. @click.option("--password", "-p", help="The password of the user.")
  105. @with_appcontext
  106. def install(welcome, force, username, email, password):
  107. """Installs flaskbb. If no arguments are used, an interactive setup
  108. will be run.
  109. """
  110. click.secho("[+] Installing FlaskBB...", fg="cyan")
  111. if database_exists(db.engine.url):
  112. if force or click.confirm(click.style(
  113. "Existing database found. Do you want to delete the old one and "
  114. "create a new one?", fg="magenta")
  115. ):
  116. db.drop_all()
  117. else:
  118. sys.exit(0)
  119. # creating database from scratch and 'stamping it'
  120. create_latest_db()
  121. click.secho("[+] Creating default settings...", fg="cyan")
  122. create_default_groups()
  123. create_default_settings()
  124. click.secho("[+] Creating admin user...", fg="cyan")
  125. prompt_save_user(username, email, password, "admin")
  126. if welcome:
  127. click.secho("[+] Creating welcome forum...", fg="cyan")
  128. create_welcome_forum()
  129. click.secho("[+] Compiling translations...", fg="cyan")
  130. compile_translations()
  131. click.secho("[+] FlaskBB has been successfully installed!",
  132. fg="green", bold=True)
  133. @flaskbb.command()
  134. @click.option("--test-data", "-t", default=False, is_flag=True,
  135. help="Adds some test data.")
  136. @click.option("--bulk-data", "-b", default=False, is_flag=True,
  137. help="Adds a lot of data.")
  138. @click.option("--posts", default=100,
  139. help="Number of posts to create in each topic (default: 100).")
  140. @click.option("--topics", default=100,
  141. help="Number of topics to create (default: 100).")
  142. @click.option("--force", "-f", is_flag=True,
  143. help="Will delete the database before populating it.")
  144. @click.option("--initdb", "-i", is_flag=True,
  145. help="Initializes the database before populating it.")
  146. def populate(bulk_data, test_data, posts, topics, force, initdb):
  147. """Creates the necessary tables and groups for FlaskBB."""
  148. if force:
  149. click.secho("[+] Recreating database...", fg="cyan")
  150. db.drop_all()
  151. # do not initialize the db if -i is passed
  152. if not initdb:
  153. create_latest_db()
  154. if initdb:
  155. click.secho("[+] Initializing database...", fg="cyan")
  156. create_latest_db()
  157. if test_data:
  158. click.secho("[+] Adding some test data...", fg="cyan")
  159. create_test_data()
  160. if bulk_data:
  161. timer = time.time()
  162. topic_count, post_count = insert_bulk_data(int(topics), int(posts))
  163. elapsed = time.time() - timer
  164. click.secho("[+] It took {} seconds to create {} topics and {} posts"
  165. .format(elapsed, topic_count, post_count), fg="cyan")
  166. # this just makes the most sense for the command name; use -i to
  167. # init the db as well
  168. if not test_data:
  169. click.secho("[+] Populating the database with some defaults...",
  170. fg="cyan")
  171. create_default_groups()
  172. create_default_settings()
  173. @flaskbb.command()
  174. def reindex():
  175. """Reindexes the search index."""
  176. click.secho("[+] Reindexing search index...", fg="cyan")
  177. whooshee.reindex()
  178. @flaskbb.command()
  179. @click.option("all_latest", "--all", "-a", default=False, is_flag=True,
  180. help="Upgrades migrations AND fixtures to the latest version.")
  181. @click.option("--fixture/", "-f", default=None,
  182. help="The fixture which should be upgraded or installed.")
  183. @click.option("--force", default=False, is_flag=True,
  184. help="Forcefully upgrades the fixtures.")
  185. def upgrade(all_latest, fixture, force):
  186. """Updates the migrations and fixtures."""
  187. if all_latest:
  188. click.secho("[+] Upgrading migrations to the latest version...",
  189. fg="cyan")
  190. alembic.upgrade()
  191. if fixture or all_latest:
  192. try:
  193. settings = import_string(
  194. "flaskbb.fixtures.{}".format(fixture)
  195. )
  196. settings = settings.fixture
  197. except ImportError:
  198. raise FlaskBBCLIError("{} fixture is not available"
  199. .format(fixture), fg="red")
  200. click.secho("[+] Updating fixtures...", fg="cyan")
  201. count = update_settings_from_fixture(
  202. fixture=settings, overwrite_group=force, overwrite_setting=force
  203. )
  204. click.secho("[+] {settings} settings in {groups} setting groups updated.".format(
  205. groups=len(count), settings=sum(len(settings) for settings in count.values())), fg="green"
  206. )
  207. @flaskbb.command("download-emojis")
  208. @with_appcontext
  209. def download_emoji():
  210. """Downloads emojis from emoji-cheat-sheet.com.
  211. This command is probably going to be removed in future version.
  212. """
  213. click.secho("[+] Downloading emojis...", fg="cyan")
  214. HOSTNAME = "https://api.github.com"
  215. REPO = "/repos/arvida/emoji-cheat-sheet.com/contents/public/graphics/emojis" # noqa
  216. FULL_URL = "{}{}".format(HOSTNAME, REPO)
  217. DOWNLOAD_PATH = os.path.join(current_app.static_folder, "emoji")
  218. response = requests.get(FULL_URL)
  219. cached_count = 0
  220. count = 0
  221. for image in response.json():
  222. if not os.path.exists(os.path.abspath(DOWNLOAD_PATH)):
  223. raise FlaskBBCLIError(
  224. "{} does not exist.".format(os.path.abspath(DOWNLOAD_PATH)),
  225. fg="red")
  226. full_path = os.path.join(DOWNLOAD_PATH, image["name"])
  227. if not os.path.exists(full_path):
  228. count += 1
  229. f = open(full_path, 'wb')
  230. f.write(requests.get(image["download_url"]).content)
  231. f.close()
  232. if count == cached_count + 50:
  233. cached_count = count
  234. click.secho("[+] {} out of {} Emojis downloaded...".format(
  235. cached_count, len(response.json())), fg="cyan")
  236. click.secho("[+] Finished downloading {} Emojis.".format(count),
  237. fg="green")
  238. @flaskbb.command("celery", add_help_option=False,
  239. context_settings={"ignore_unknown_options": True,
  240. "allow_extra_args": True})
  241. @click.pass_context
  242. @with_appcontext
  243. def start_celery(ctx):
  244. """Preconfigured wrapper around the 'celery' command."""
  245. CeleryCommand(celery).execute_from_commandline(
  246. ["flaskbb celery"] + ctx.args
  247. )
  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. # An override to handle database location paths on Windows environments
  387. database_path = "sqlite:///" + os.path.join(os.path.dirname(current_app.instance_path), "flaskbb.sqlite")
  388. if os.name == 'nt':
  389. database_path = database_path.replace("\\", r"\\")
  390. default_conf = {
  391. "is_debug": True,
  392. "server_name": "localhost:5000",
  393. "url_scheme": "http",
  394. "database_uri": database_path,
  395. "redis_enabled": False,
  396. "redis_uri": "redis://localhost:6379",
  397. "mail_server": "localhost",
  398. "mail_port": 25,
  399. "mail_use_tls": False,
  400. "mail_use_ssl": False,
  401. "mail_username": "",
  402. "mail_password": "",
  403. "mail_sender_name": "FlaskBB Mailer",
  404. "mail_sender_address": "noreply@yourdomain",
  405. "mail_admin_address": "admin@yourdomain",
  406. "secret_key": binascii.hexlify(os.urandom(24)).decode(),
  407. "csrf_secret_key": binascii.hexlify(os.urandom(24)).decode(),
  408. "timestamp": datetime.utcnow().strftime("%A, %d. %B %Y at %H:%M"),
  409. "log_config_path": "",
  410. }
  411. if not force:
  412. config_path = prompt_config_path(config_path)
  413. if force and os.path.exists(config_path):
  414. click.secho("Overwriting existing config file: {}".format(config_path),
  415. fg="yellow")
  416. if development:
  417. write_config(default_conf, config_template, config_path)
  418. sys.exit(0)
  419. # SERVER_NAME
  420. click.secho("The name and port number of the server.\n"
  421. "This is needed to correctly generate URLs when no request "
  422. "context is available.", fg="cyan")
  423. default_conf["server_name"] = click.prompt(
  424. click.style("Server Name", fg="magenta"), type=str,
  425. default=default_conf.get("server_name"))
  426. # PREFERRED_URL_SCHEME
  427. click.secho("The URL Scheme is also needed in order to generate correct "
  428. "URLs when no request context is available.\n"
  429. "Choose either 'https' or 'http'.", fg="cyan")
  430. default_conf["url_scheme"] = click.prompt(
  431. click.style("URL Scheme", fg="magenta"),
  432. type=click.Choice(["https", "http"]),
  433. default=default_conf.get("url_scheme"))
  434. # SQLALCHEMY_DATABASE_URI
  435. click.secho("For Postgres use:\n"
  436. " postgresql://flaskbb@localhost:5432/flaskbb\n"
  437. "For more options see the SQLAlchemy docs:\n"
  438. " http://docs.sqlalchemy.org/en/latest/core/engines.html",
  439. fg="cyan")
  440. default_conf["database_uri"] = click.prompt(
  441. click.style("Database URI", fg="magenta"),
  442. default=default_conf.get("database_uri"))
  443. # REDIS_ENABLED
  444. click.secho("Redis will be used for things such as the task queue, "
  445. "caching and rate limiting.", fg="cyan")
  446. default_conf["redis_enabled"] = click.confirm(
  447. click.style("Would you like to use redis?", fg="magenta"),
  448. default=True) # default_conf.get("redis_enabled") is False
  449. # REDIS_URI
  450. if default_conf.get("redis_enabled", False):
  451. default_conf["redis_uri"] = click.prompt(
  452. click.style("Redis URI", fg="magenta"),
  453. default=default_conf.get("redis_uri"))
  454. else:
  455. default_conf["redis_uri"] = ""
  456. # MAIL_SERVER
  457. click.secho("To use 'localhost' make sure that you have sendmail or\n"
  458. "something similar installed. Gmail is also supprted.",
  459. fg="cyan")
  460. default_conf["mail_server"] = click.prompt(
  461. click.style("Mail Server", fg="magenta"),
  462. default=default_conf.get("mail_server"))
  463. # MAIL_PORT
  464. click.secho("The port on which the SMTP server is listening on.",
  465. fg="cyan")
  466. default_conf["mail_port"] = click.prompt(
  467. click.style("Mail Server SMTP Port", fg="magenta"),
  468. default=default_conf.get("mail_port"))
  469. # MAIL_USE_TLS
  470. click.secho("If you are using a local SMTP server like sendmail this is "
  471. "not needed. For external servers it is required.",
  472. fg="cyan")
  473. default_conf["mail_use_tls"] = click.confirm(
  474. click.style("Use TLS for sending mails?", fg="magenta"),
  475. default=default_conf.get("mail_use_tls"))
  476. # MAIL_USE_SSL
  477. click.secho("Same as above. TLS is the successor to SSL.", fg="cyan")
  478. default_conf["mail_use_ssl"] = click.confirm(
  479. click.style("Use SSL for sending mails?", fg="magenta"),
  480. default=default_conf.get("mail_use_ssl"))
  481. # MAIL_USERNAME
  482. click.secho("Not needed if you are using a local smtp server.\nFor gmail "
  483. "you have to put in your email address here.", fg="cyan")
  484. default_conf["mail_username"] = click.prompt(
  485. click.style("Mail Username", fg="magenta"),
  486. default=default_conf.get("mail_username"))
  487. # MAIL_PASSWORD
  488. click.secho("Not needed if you are using a local smtp server.\nFor gmail "
  489. "you have to put in your gmail password here.", fg="cyan")
  490. default_conf["mail_password"] = click.prompt(
  491. click.style("Mail Password", fg="magenta"),
  492. default=default_conf.get("mail_password"))
  493. # MAIL_DEFAULT_SENDER
  494. click.secho("The name of the sender. You probably want to change it to "
  495. "something like '<your_community> Mailer'.", fg="cyan")
  496. default_conf["mail_sender_name"] = click.prompt(
  497. click.style("Mail Sender Name", fg="magenta"),
  498. default=default_conf.get("mail_sender_name"))
  499. click.secho("On localhost you want to use a noreply address here. "
  500. "Use your email address for gmail here.", fg="cyan")
  501. default_conf["mail_sender_address"] = click.prompt(
  502. click.style("Mail Sender Address", fg="magenta"),
  503. default=default_conf.get("mail_sender_address"))
  504. # ADMINS
  505. click.secho("Logs and important system messages are sent to this address."
  506. "Use your email address for gmail here.", fg="cyan")
  507. default_conf["mail_admin_address"] = click.prompt(
  508. click.style("Mail Admin Email", fg="magenta"),
  509. default=default_conf.get("mail_admin_address"))
  510. click.secho("Optional filepath to load a logging configuration file from. "
  511. "See the Python logging documentation for more detail.\n"
  512. "\thttps://docs.python.org/library/logging.config.html#logging-config-fileformat",
  513. fg="cyan")
  514. default_conf["log_config_path"] = click.prompt(
  515. click.style("Logging Config Path", fg="magenta"),
  516. default=default_conf.get("log_config_path"))
  517. write_config(default_conf, config_template, config_path)
  518. # Finished
  519. click.secho("The configuration file has been saved to:\n{cfg}\n"
  520. "Feel free to adjust it as needed."
  521. .format(cfg=config_path), fg="blue", bold=True)
  522. click.secho("Usage: \nflaskbb --config {cfg} run"
  523. .format(cfg=config_path), fg="green")