main.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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 click
  14. from werkzeug.utils import import_string, ImportStringError
  15. from flask import current_app
  16. from flask.cli import FlaskGroup, ScriptInfo, with_appcontext
  17. from sqlalchemy_utils.functions import database_exists, create_database, drop_database
  18. from flask_migrate import upgrade as upgrade_database
  19. from flaskbb import create_app
  20. from flaskbb._compat import iteritems
  21. from flaskbb.extensions import db, whooshee, celery
  22. from flaskbb.cli.utils import (get_version, save_user_prompt, FlaskBBCLIError,
  23. EmailType)
  24. from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
  25. create_default_groups,
  26. create_default_settings, insert_bulk_data,
  27. update_settings_from_fixture)
  28. from flaskbb.utils.translations import compile_translations
  29. def make_app(script_info):
  30. config_file = getattr(script_info, "config_file")
  31. if config_file is not None:
  32. # check if config file exists
  33. if os.path.exists(os.path.abspath(config_file)):
  34. click.secho("[+] Using config from: {}".format(
  35. os.path.abspath(config_file)), fg="cyan")
  36. # config file doesn't exist, maybe it's a module
  37. else:
  38. try:
  39. import_string(config_file)
  40. click.secho("[+] Using config from: {}".format(config_file),
  41. fg="cyan")
  42. except ImportStringError:
  43. click.secho("[~] Config '{}' doesn't exist. "
  44. "Using default config.".format(config_file),
  45. fg="red")
  46. config_file = None
  47. else:
  48. click.secho("[~] Using default config.", fg="yellow")
  49. return create_app(config_file)
  50. def set_config(ctx, param, value):
  51. """This will pass the config file to the create_app function."""
  52. ctx.ensure_object(ScriptInfo).config_file = value
  53. @click.group(cls=FlaskGroup, create_app=make_app)
  54. @click.option("--version", expose_value=False, callback=get_version,
  55. is_flag=True, is_eager=True, help="Show the FlaskBB version.")
  56. @click.option("--config", expose_value=False, callback=set_config,
  57. required=False, is_flag=False, is_eager=True,
  58. help="Specify the config to use in dotted module notation "
  59. "e.g. flaskbb.configs.default.DefaultConfig")
  60. def flaskbb():
  61. """This is the commandline interface for flaskbb."""
  62. pass
  63. @flaskbb.command()
  64. @click.option("--welcome", "-w", default=True, is_flag=True,
  65. help="Disable the welcome forum.")
  66. @click.option("--force", "-f", default=False, is_flag=True,
  67. help="Doesn't ask for confirmation.")
  68. @click.option("--username", "-u", help="The username of the user.")
  69. @click.option("--email", "-e", type=EmailType(),
  70. help="The email address of the user.")
  71. @click.option("--password", "-p", help="The password of the user.")
  72. @click.option("--group", "-g", help="The group of the user.",
  73. type=click.Choice(["admin", "super_mod", "mod", "member"]))
  74. def install(welcome, force, username, email, password, group):
  75. """Installs flaskbb. If no arguments are used, an interactive setup
  76. will be run.
  77. """
  78. click.secho("[+] Installing FlaskBB...", fg="cyan")
  79. if database_exists(db.engine.url):
  80. if force or click.confirm(click.style(
  81. "Existing database found. Do you want to delete the old one and "
  82. "create a new one?", fg="magenta")
  83. ):
  84. drop_database(db.engine.url)
  85. else:
  86. sys.exit(0)
  87. create_database(db.engine.url)
  88. upgrade_database()
  89. click.secho("[+] Creating default settings...", fg="cyan")
  90. create_default_groups()
  91. create_default_settings()
  92. click.secho("[+] Creating admin user...", fg="cyan")
  93. save_user_prompt(username, email, password, group)
  94. if welcome:
  95. click.secho("[+] Creating welcome forum...", fg="cyan")
  96. create_welcome_forum()
  97. click.secho("[+] Compiling translations...", fg="cyan")
  98. compile_translations()
  99. click.secho("[+] FlaskBB has been successfully installed!",
  100. fg="green", bold=True)
  101. @flaskbb.command()
  102. @click.option("--test-data", "-t", default=False, is_flag=True,
  103. help="Adds some test data.")
  104. @click.option("--bulk-data", "-b", default=False, is_flag=True,
  105. help="Adds a lot of data.")
  106. @click.option("--posts", default=100,
  107. help="Number of posts to create in each topic (default: 100).")
  108. @click.option("--topics", default=100,
  109. help="Number of topics to create (default: 100).")
  110. @click.option("--force", "-f", is_flag=True,
  111. help="Will delete the database before populating it.")
  112. @click.option("--initdb", "-i", is_flag=True,
  113. help="Initializes the database before populating it.")
  114. def populate(bulk_data, test_data, posts, topics, force, initdb):
  115. """Creates the necessary tables and groups for FlaskBB."""
  116. if force:
  117. click.secho("[+] Recreating database...", fg="cyan")
  118. drop_database(db.engine.url)
  119. # do not initialize the db if -i is passed
  120. if not initdb:
  121. upgrade_database()
  122. if initdb:
  123. click.secho("[+] Initializing database...", fg="cyan")
  124. upgrade_database()
  125. if test_data:
  126. click.secho("[+] Adding some test data...", fg="cyan")
  127. create_test_data()
  128. if bulk_data:
  129. timer = time.time()
  130. topic_count, post_count = insert_bulk_data(int(topics), int(posts))
  131. elapsed = time.time() - timer
  132. click.secho("[+] It took {} seconds to create {} topics and {} posts"
  133. .format(elapsed, topic_count, post_count), fg="cyan")
  134. # this just makes the most sense for the command name; use -i to
  135. # init the db as well
  136. if not test_data:
  137. click.secho("[+] Populating the database with some defaults...",
  138. fg="cyan")
  139. create_default_groups()
  140. create_default_settings()
  141. @flaskbb.command()
  142. def reindex():
  143. """Reindexes the search index."""
  144. click.secho("[+] Reindexing search index...", fg="cyan")
  145. whooshee.reindex()
  146. @flaskbb.command()
  147. @click.option("all_latest", "--all", "-a", default=False, is_flag=True,
  148. help="Upgrades migrations AND fixtures to the latest version.")
  149. @click.option("--fixture/", "-f", default=None,
  150. help="The fixture which should be upgraded or installed.")
  151. @click.option("--force", default=False, is_flag=True,
  152. help="Forcefully upgrades the fixtures.")
  153. def upgrade(all_latest, fixture, force):
  154. """Updates the migrations and fixtures."""
  155. if all_latest:
  156. click.secho("[+] Upgrading migrations to the latest version...",
  157. fg="cyan")
  158. upgrade_database()
  159. if fixture or all_latest:
  160. try:
  161. settings = import_string(
  162. "flaskbb.fixtures.{}".format(fixture)
  163. )
  164. settings = settings.fixture
  165. except ImportError:
  166. raise FlaskBBCLIError("{} fixture is not available"
  167. .format(fixture), fg="red")
  168. click.secho("[+] Updating fixtures...")
  169. count = update_settings_from_fixture(
  170. fixture=settings, overwrite_group=force, overwrite_setting=force
  171. )
  172. click.secho("[+] {} groups and {} settings updated.".format(
  173. len(count.keys()), len(count.values()), fg="green")
  174. )
  175. @flaskbb.command("download-emojis")
  176. @with_appcontext
  177. def download_emoji():
  178. """Downloads emojis from emoji-cheat-sheet.com.
  179. This command is probably going to be removed in future version.
  180. """
  181. click.secho("[+] Downloading emojis...", fg="cyan")
  182. HOSTNAME = "https://api.github.com"
  183. REPO = "/repos/arvida/emoji-cheat-sheet.com/contents/public/graphics/emojis"
  184. FULL_URL = "{}{}".format(HOSTNAME, REPO)
  185. DOWNLOAD_PATH = os.path.join(current_app.static_folder, "emoji")
  186. response = requests.get(FULL_URL)
  187. cached_count = 0
  188. count = 0
  189. for image in response.json():
  190. if not os.path.exists(os.path.abspath(DOWNLOAD_PATH)):
  191. raise FlaskBBCLIError(
  192. "{} does not exist.".format(os.path.abspath(DOWNLOAD_PATH)),
  193. fg="red")
  194. full_path = os.path.join(DOWNLOAD_PATH, image["name"])
  195. if not os.path.exists(full_path):
  196. count += 1
  197. f = open(full_path, 'wb')
  198. f.write(requests.get(image["download_url"]).content)
  199. f.close()
  200. if count == cached_count + 50:
  201. cached_count = count
  202. click.secho("[+] {} out of {} Emojis downloaded...".format(
  203. cached_count, len(response.json())), fg="cyan")
  204. click.secho("[+] Finished downloading {} Emojis.".format(count),
  205. fg="green")
  206. @flaskbb.command("celery", context_settings=dict(ignore_unknown_options=True,))
  207. @click.argument('celery_args', nargs=-1, type=click.UNPROCESSED)
  208. @click.option("show_help", "--help", "-h", is_flag=True,
  209. help="Shows this message and exits")
  210. @click.option("show_celery_help", "--help-celery", is_flag=True,
  211. help="Shows the celery help message")
  212. @click.pass_context
  213. @with_appcontext
  214. def start_celery(ctx, show_help, show_celery_help, celery_args):
  215. """Preconfigured wrapper around the 'celery' command.
  216. Additional CELERY_ARGS arguments are passed to celery."""
  217. if show_help:
  218. click.echo(ctx.get_help())
  219. sys.exit(0)
  220. if show_celery_help:
  221. click.echo(celery.start(argv=["--help"]))
  222. sys.exit(0)
  223. default_args = ['celery']
  224. default_args = default_args + list(celery_args)
  225. celery.start(argv=default_args)
  226. @flaskbb.command()
  227. @click.option("--server", "-s", default="gunicorn",
  228. type=click.Choice(["gunicorn", "gevent"]),
  229. help="The WSGI Server to run FlaskBB on.")
  230. @click.option("--host", "-h", default="127.0.0.1",
  231. help="The interface to bind FlaskBB to.")
  232. @click.option("--port", "-p", default="8000", type=int,
  233. help="The port to bind FlaskBB to.")
  234. @click.option("--workers", "-w", default=4,
  235. help="The number of worker processes for handling requests.")
  236. @click.option("--daemon", "-d", default=False, is_flag=True,
  237. help="Starts gunicorn as daemon.")
  238. @click.option("--config", "-c",
  239. help="The configuration file to use for FlaskBB.")
  240. def start(server, host, port, workers, config, daemon):
  241. """Starts a production ready wsgi server.
  242. TODO: Figure out a way how to forward additional args to gunicorn
  243. without causing any errors.
  244. """
  245. if server == "gunicorn":
  246. try:
  247. from gunicorn.app.base import Application
  248. class FlaskBBApplication(Application):
  249. def __init__(self, app, options=None):
  250. self.options = options or {}
  251. self.application = app
  252. super(FlaskBBApplication, self).__init__()
  253. def load_config(self):
  254. config = dict([
  255. (key, value) for key, value in iteritems(self.options)
  256. if key in self.cfg.settings and value is not None
  257. ])
  258. for key, value in iteritems(config):
  259. self.cfg.set(key.lower(), value)
  260. def load(self):
  261. return self.application
  262. options = {
  263. "bind": "{}:{}".format(host, port),
  264. "workers": workers,
  265. "daemon": daemon,
  266. }
  267. FlaskBBApplication(create_app(config=config), options).run()
  268. except ImportError:
  269. raise FlaskBBCLIError("Cannot import gunicorn. "
  270. "Make sure it is installed.", fg="red")
  271. elif server == "gevent":
  272. try:
  273. from gevent import __version__
  274. from gevent.pywsgi import WSGIServer
  275. click.secho("* Starting gevent {}".format(__version__))
  276. click.secho("* Listening on http://{}:{}/".format(host, port))
  277. http_server = WSGIServer((host, port), create_app(config=config))
  278. http_server.serve_forever()
  279. except ImportError:
  280. raise FlaskBBCLIError("Cannot import gevent. "
  281. "Make sure it is installed.", fg="red")
  282. @flaskbb.command("shell", short_help="Runs a shell in the app context.")
  283. @with_appcontext
  284. def shell_command():
  285. """Runs an interactive Python shell in the context of a given
  286. Flask application. The application will populate the default
  287. namespace of this shell according to it"s configuration.
  288. This is useful for executing small snippets of management code
  289. without having to manually configuring the application.
  290. This code snippet is taken from Flask"s cli module and modified to
  291. run IPython and falls back to the normal shell if IPython is not
  292. available.
  293. """
  294. import code
  295. banner = "Python %s on %s\nInstance Path: %s" % (
  296. sys.version,
  297. sys.platform,
  298. current_app.instance_path,
  299. )
  300. ctx = {"db": db}
  301. # Support the regular Python interpreter startup script if someone
  302. # is using it.
  303. startup = os.environ.get("PYTHONSTARTUP")
  304. if startup and os.path.isfile(startup):
  305. with open(startup, "r") as f:
  306. eval(compile(f.read(), startup, "exec"), ctx)
  307. ctx.update(current_app.make_shell_context())
  308. try:
  309. import IPython
  310. IPython.embed(banner1=banner, user_ns=ctx)
  311. except ImportError:
  312. code.interact(banner=banner, local=ctx)
  313. @flaskbb.command("urls", short_help="Show routes for the app.")
  314. @click.option("--route", "-r", "order_by", flag_value="rule", default=True,
  315. help="Order by route")
  316. @click.option("--endpoint", "-e", "order_by", flag_value="endpoint",
  317. help="Order by endpoint")
  318. @click.option("--methods", "-m", "order_by", flag_value="methods",
  319. help="Order by methods")
  320. @with_appcontext
  321. def list_urls(order_by):
  322. """Lists all available routes."""
  323. from flask import current_app
  324. rules = sorted(
  325. current_app.url_map.iter_rules(),
  326. key=lambda rule: getattr(rule, order_by)
  327. )
  328. max_rule_len = max(len(rule.rule) for rule in rules)
  329. max_rule_len = max(max_rule_len, len("Route"))
  330. max_endpoint_len = max(len(rule.endpoint) for rule in rules)
  331. max_endpoint_len = max(max_endpoint_len, len("Endpoint"))
  332. max_method_len = max(len(", ".join(rule.methods)) for rule in rules)
  333. max_method_len = max(max_method_len, len("Methods"))
  334. column_header_len = max_rule_len + max_endpoint_len + max_method_len + 4
  335. column_template = "{:<%s} {:<%s} {:<%s}" % (
  336. max_rule_len, max_endpoint_len, max_method_len
  337. )
  338. click.secho(column_template.format("Route", "Endpoint", "Methods"),
  339. fg="blue", bold=True)
  340. click.secho("=" * column_header_len, bold=True)
  341. for rule in rules:
  342. methods = ", ".join(rule.methods)
  343. click.echo(column_template.format(rule.rule, rule.endpoint, methods))