commands.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.cli.commands
  4. ~~~~~~~~~~~~~~~~~~~~
  5. This module contains all commands.
  6. Plugin and Theme templates are generated via cookiecutter.
  7. In order to generate those project templates you have to
  8. cookiecutter first::
  9. pip install cookiecutter
  10. :copyright: (c) 2016 by the FlaskBB Team.
  11. :license: BSD, see LICENSE for more details.
  12. """
  13. import sys
  14. import os
  15. import re
  16. import time
  17. import shutil
  18. import requests
  19. import click
  20. from werkzeug.utils import import_string, ImportStringError
  21. from flask import current_app, __version__ as flask_version
  22. from flask.cli import FlaskGroup, ScriptInfo, with_appcontext
  23. from sqlalchemy.exc import IntegrityError
  24. from sqlalchemy_utils.functions import database_exists, drop_database
  25. from flask_migrate import upgrade as upgrade_database
  26. from flask_plugins import (get_all_plugins, get_enabled_plugins,
  27. get_plugin_from_all)
  28. from flask_themes2 import get_themes_list, get_theme
  29. from flaskbb import create_app, __version__
  30. from flaskbb._compat import iteritems
  31. from flaskbb.extensions import db, whooshee, plugin_manager, celery
  32. from flaskbb.user.models import User
  33. from flaskbb.utils.settings import flaskbb_config
  34. from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
  35. create_user, update_user,
  36. create_default_groups,
  37. create_default_settings, insert_bulk_data,
  38. update_settings_from_fixture)
  39. from flaskbb.utils.translations import (add_translations, compile_translations,
  40. update_translations,
  41. add_plugin_translations,
  42. compile_plugin_translations,
  43. update_plugin_translations)
  44. # Not optimal and subject to change
  45. try:
  46. from flaskbb.configs.production import ProductionConfig as Config
  47. except ImportError:
  48. try:
  49. from flaskbb.configs.development import DevelopmentConfig as Config
  50. except ImportError:
  51. from flaskbb.configs.default import DefaultConfig as Config
  52. cookiecutter_available = False
  53. try:
  54. from cookiecutter.main import cookiecutter
  55. cookiecutter_available = True
  56. except ImportError:
  57. pass
  58. _email_regex = r"[^@]+@[^@]+\.[^@]+"
  59. class FlaskBBCLIError(click.ClickException):
  60. """An exception that signals a usage error including color support.
  61. This aborts any further handling.
  62. :param styles: The style kwargs which should be forwarded to click.secho.
  63. """
  64. def __init__(self, message, **styles):
  65. click.ClickException.__init__(self, message)
  66. self.styles = styles
  67. def show(self, file=None):
  68. if file is None:
  69. file = click._compat.get_text_stderr()
  70. click.secho("[-] Error: %s" % self.format_message(), file=file,
  71. **self.styles)
  72. class EmailType(click.ParamType):
  73. """The choice type allows a value to be checked against a fixed set of
  74. supported values. All of these values have to be strings.
  75. See :ref:`choice-opts` for an example.
  76. """
  77. name = "email"
  78. def convert(self, value, param, ctx):
  79. # Exact match
  80. if re.match(_email_regex, value):
  81. return value
  82. else:
  83. self.fail(("invalid email: %s" % value), param, ctx)
  84. def __repr__(self):
  85. return "email"
  86. def validate_plugin(plugin):
  87. """Checks if a plugin is installed.
  88. TODO: Figure out how to use this in a callback. Doesn't work because
  89. the appcontext can't be found and using with_appcontext doesn't
  90. help either.
  91. """
  92. if plugin not in plugin_manager.all_plugins.keys():
  93. raise FlaskBBCLIError("Plugin {} not found.".format(plugin), fg="red")
  94. return True
  95. def validate_theme(theme):
  96. """Checks if a theme is installed."""
  97. try:
  98. get_theme(theme)
  99. except KeyError:
  100. raise FlaskBBCLIError("Theme {} not found.".format(theme), fg="red")
  101. def check_cookiecutter(ctx, param, value):
  102. if not cookiecutter_available:
  103. raise FlaskBBCLIError(
  104. "Can't create {} because cookiecutter is not installed. "
  105. "You can install it with 'pip install cookiecutter'.".
  106. format(value), fg="red"
  107. )
  108. return value
  109. def get_version(ctx, param, value):
  110. if not value or ctx.resilient_parsing:
  111. return
  112. message = ("FlaskBB %(version)s using Flask %(flask_version)s on "
  113. "Python %(python_version)s")
  114. click.echo(message % {
  115. 'version': __version__,
  116. 'flask_version': flask_version,
  117. 'python_version': sys.version.split("\n")[0]
  118. }, color=ctx.color)
  119. ctx.exit()
  120. def make_app(script_info):
  121. config_file = getattr(script_info, "config_file")
  122. if config_file is not None:
  123. # check if config file exists
  124. if os.path.exists(os.path.abspath(config_file)):
  125. click.secho("[+] Using config from: {}".format(
  126. os.path.abspath(config_file)), fg="cyan")
  127. # config file doesn't exist, maybe it's a module
  128. else:
  129. try:
  130. import_string(config_file)
  131. click.secho("[+] Using config from: {}".format(config_file),
  132. fg="cyan")
  133. except ImportStringError:
  134. click.secho("[~] Config '{}' doesn't exist. "
  135. "Using default config.".format(config_file),
  136. fg="red")
  137. config_file = None
  138. else:
  139. click.secho("[~] Using default config.", fg="yellow")
  140. return create_app(config_file)
  141. def set_config(ctx, param, value):
  142. """This will pass the config file to the create_app function."""
  143. ctx.ensure_object(ScriptInfo).config_file = value
  144. @click.group(cls=FlaskGroup, create_app=make_app)
  145. @click.option("--version", expose_value=False, callback=get_version,
  146. is_flag=True, is_eager=True, help="Show the FlaskBB version.")
  147. @click.option("--config", expose_value=False, callback=set_config,
  148. required=False, is_flag=False, is_eager=True,
  149. help="Specify the config to use in dotted module notation "
  150. "e.g. flaskbb.configs.default.DefaultConfig")
  151. def main():
  152. """This is the commandline interface for flaskbb."""
  153. pass
  154. @main.command()
  155. @click.option("--welcome-forum", default=True, is_flag=True,
  156. help="Creates a welcome forum.")
  157. def install(welcome_forum):
  158. """Installs flaskbb. If no arguments are used, an interactive setup
  159. will be run.
  160. """
  161. click.secho("[+] Installing FlaskBB...", fg="cyan")
  162. if database_exists(db.engine.url):
  163. if click.confirm(click.style(
  164. "Existing database found. Do you want to delete the old one and "
  165. "create a new one?", fg="magenta")
  166. ):
  167. drop_database(db.engine.url)
  168. upgrade_database()
  169. else:
  170. sys.exit(0)
  171. else:
  172. upgrade_database()
  173. click.secho("[+] Creating default settings...", fg="cyan")
  174. create_default_groups()
  175. create_default_settings()
  176. click.secho("[+] Creating admin user...", fg="cyan")
  177. username = click.prompt(
  178. click.style("Username", fg="magenta"), type=str,
  179. default=os.environ.get("USER", "")
  180. )
  181. email = click.prompt(
  182. click.style("Email address", fg="magenta"), type=EmailType()
  183. )
  184. password = click.prompt(
  185. click.style("Password", fg="magenta"), hide_input=True,
  186. confirmation_prompt=True
  187. )
  188. group = click.prompt(
  189. click.style("Group", fg="magenta"),
  190. type=click.Choice(["admin", "super_mod", "mod", "member"]),
  191. default="admin"
  192. )
  193. create_user(username, password, email, group)
  194. if welcome_forum:
  195. click.secho("[+] Creating welcome forum...", fg="cyan")
  196. create_welcome_forum()
  197. click.secho("[+] Compiling translations...", fg="cyan")
  198. compile_translations()
  199. click.secho("[+] FlaskBB has been successfully installed!",
  200. fg="green", bold=True)
  201. @main.command()
  202. @click.option("--test-data", "-t", default=False, is_flag=True,
  203. help="Adds some test data.")
  204. @click.option("--bulk-data", "-b", default=False, is_flag=True,
  205. help="Adds a lot of data.")
  206. @click.option("--posts", default=100,
  207. help="Number of posts to create in each topic (default: 100).")
  208. @click.option("--topics", default=100,
  209. help="Number of topics to create (default: 100).")
  210. @click.option("--force", "-f", is_flag=True,
  211. help="Will delete the database before populating it.")
  212. @click.option("--initdb", "-i", is_flag=True,
  213. help="Initializes the database before populating it.")
  214. def populate(bulk_data, test_data, posts, topics, force, initdb):
  215. """Creates the necessary tables and groups for FlaskBB."""
  216. if force:
  217. click.secho("[+] Recreating database...", fg="cyan")
  218. drop_database(db.engine.url)
  219. # do not initialize the db if -i is passed
  220. if not initdb:
  221. upgrade_database()
  222. if initdb:
  223. click.secho("[+] Initializing database...", fg="cyan")
  224. upgrade_database()
  225. if test_data:
  226. click.secho("[+] Adding some test data...", fg="cyan")
  227. create_test_data()
  228. if bulk_data:
  229. timer = time.time()
  230. topic_count, post_count = insert_bulk_data(int(topics), int(posts))
  231. elapsed = time.time() - timer
  232. click.secho("[+] It took {} seconds to create {} topics and {} posts"
  233. .format(elapsed, topic_count, post_count), fg="cyan")
  234. # this just makes the most sense for the command name; use -i to
  235. # init the db as well
  236. if not test_data:
  237. click.secho("[+] Populating the database with some defaults...",
  238. fg="cyan")
  239. create_default_groups()
  240. create_default_settings()
  241. @main.group()
  242. def translations():
  243. """Translations command sub group."""
  244. pass
  245. @translations.command("new")
  246. @click.option("--plugin", "-p", type=click.STRING,
  247. help="The plugin for which a language should be added.")
  248. @click.argument("lang")
  249. def new_translation(lang, plugin):
  250. """Adds a new language to the translations. "lang" is the language code
  251. of the language, like, "de_AT"."""
  252. if plugin:
  253. validate_plugin(plugin)
  254. click.secho("[+] Adding new language {} for plugin {}..."
  255. .format(lang, plugin), fg="cyan")
  256. add_plugin_translations(plugin, lang)
  257. else:
  258. click.secho("[+] Adding new language {}...".format(lang), fg="cyan")
  259. add_translations(lang)
  260. @translations.command("update")
  261. @click.option("is_all", "--all", "-a", default=True, is_flag=True,
  262. help="Updates the plugin translations as well.")
  263. @click.option("--plugin", "-p", type=click.STRING,
  264. help="The plugin for which the translations should be updated.")
  265. def update_translation(is_all, plugin):
  266. """Updates all translations."""
  267. if plugin is not None:
  268. validate_plugin(plugin)
  269. click.secho("[+] Updating language files for plugin {}..."
  270. .format(plugin), fg="cyan")
  271. update_plugin_translations(plugin)
  272. else:
  273. click.secho("[+] Updating language files...", fg="cyan")
  274. update_translations(include_plugins=is_all)
  275. @translations.command("compile")
  276. @click.option("is_all", "--all", "-a", default=True, is_flag=True,
  277. help="Compiles the plugin translations as well.")
  278. @click.option("--plugin", "-p", type=click.STRING,
  279. help="The plugin for which the translations should be compiled.")
  280. def compile_translation(is_all, plugin):
  281. """Compiles all translations."""
  282. if plugin is not None:
  283. validate_plugin(plugin)
  284. click.secho("[+] Compiling language files for plugin {}..."
  285. .format(plugin), fg="cyan")
  286. compile_plugin_translations(plugin)
  287. else:
  288. click.secho("[+] Compiling language files...", fg="cyan")
  289. compile_translations(include_plugins=is_all)
  290. @main.group()
  291. def plugins():
  292. """Plugins command sub group."""
  293. pass
  294. @plugins.command("new")
  295. @click.argument("plugin_identifier", callback=check_cookiecutter)
  296. @click.option("--template", "-t", type=click.STRING,
  297. default="https://github.com/sh4nks/cookiecutter-flaskbb-plugin",
  298. help="Path to a cookiecutter template or to a valid git repo.")
  299. def new_plugin(plugin_identifier, template):
  300. """Creates a new plugin based on the cookiecutter plugin
  301. template. Defaults to this template:
  302. https://github.com:sh4nks/cookiecutter-flaskbb-plugin.
  303. It will either accept a valid path on the filesystem
  304. or a URL to a Git repository which contains the cookiecutter template.
  305. """
  306. out_dir = os.path.join(current_app.root_path, "plugins", plugin_identifier)
  307. click.secho("[+] Creating new plugin {}".format(plugin_identifier),
  308. fg="cyan")
  309. cookiecutter(template, output_dir=out_dir)
  310. click.secho("[+] Done. Created in {}".format(out_dir),
  311. fg="green", bold=True)
  312. @plugins.command("install")
  313. @click.argument("plugin_identifier")
  314. def install_plugin(plugin_identifier):
  315. """Installs a new plugin."""
  316. validate_plugin(plugin_identifier)
  317. plugin = get_plugin_from_all(plugin_identifier)
  318. click.secho("[+] Installing plugin {}...".format(plugin.name), fg="cyan")
  319. try:
  320. plugin_manager.install_plugins([plugin])
  321. except Exception as e:
  322. click.secho("[-] Couldn't install plugin because of following "
  323. "exception: \n{}".format(e), fg="red")
  324. @plugins.command("uninstall")
  325. @click.argument("plugin_identifier")
  326. def uninstall_plugin(plugin_identifier):
  327. """Uninstalls a plugin from FlaskBB."""
  328. validate_plugin(plugin_identifier)
  329. plugin = get_plugin_from_all(plugin_identifier)
  330. click.secho("[+] Uninstalling plugin {}...".format(plugin.name), fg="cyan")
  331. try:
  332. plugin_manager.uninstall_plugins([plugin])
  333. except AttributeError:
  334. pass
  335. @plugins.command("remove")
  336. @click.argument("plugin_identifier")
  337. @click.option("--force", "-f", default=False, is_flag=True,
  338. help="Removes the plugin without asking for confirmation.")
  339. def remove_plugin(plugin_identifier, force):
  340. """Removes a plugin from the filesystem."""
  341. validate_plugin(plugin_identifier)
  342. if not force and not \
  343. click.confirm(click.style("Are you sure?", fg="magenta")):
  344. sys.exit(0)
  345. plugin = get_plugin_from_all(plugin_identifier)
  346. click.secho("[+] Uninstalling plugin {}...".format(plugin.name), fg="cyan")
  347. try:
  348. plugin_manager.uninstall_plugins([plugin])
  349. except Exception as e:
  350. click.secho("[-] Couldn't uninstall plugin because of following "
  351. "exception: \n{}".format(e), fg="red")
  352. if not click.confirm(click.style(
  353. "Do you want to continue anyway?", fg="magenta")
  354. ):
  355. sys.exit(0)
  356. click.secho("[+] Removing plugin from filesystem...", fg="cyan")
  357. shutil.rmtree(plugin.path, ignore_errors=False, onerror=None)
  358. @plugins.command("list")
  359. def list_plugins():
  360. """Lists all installed plugins."""
  361. click.secho("[+] Listing all installed plugins...", fg="cyan")
  362. # This is subject to change as I am not happy with the current
  363. # plugin system
  364. enabled_plugins = get_enabled_plugins()
  365. disabled_plugins = set(get_all_plugins()) - set(enabled_plugins)
  366. if len(enabled_plugins) > 0:
  367. click.secho("[+] Enabled Plugins:", fg="blue", bold=True)
  368. for plugin in enabled_plugins:
  369. click.secho(" - {} (version {})".format(
  370. plugin.name, plugin.version), bold=True
  371. )
  372. if len(disabled_plugins) > 0:
  373. click.secho("[+] Disabled Plugins:", fg="yellow", bold=True)
  374. for plugin in disabled_plugins:
  375. click.secho(" - {} (version {})".format(
  376. plugin.name, plugin.version), bold=True
  377. )
  378. @main.group()
  379. def themes():
  380. """Themes command sub group."""
  381. pass
  382. @themes.command("list")
  383. def list_themes():
  384. """Lists all installed themes."""
  385. click.secho("[+] Listing all installed themes...", fg="cyan")
  386. active_theme = get_theme(flaskbb_config['DEFAULT_THEME'])
  387. available_themes = set(get_themes_list()) - set([active_theme])
  388. click.secho("[+] Active Theme:", fg="blue", bold=True)
  389. click.secho(" - {} (version {})".format(
  390. active_theme.name, active_theme.version), bold=True
  391. )
  392. click.secho("[+] Available Themes:", fg="yellow", bold=True)
  393. for theme in available_themes:
  394. click.secho(" - {} (version {})".format(
  395. theme.name, theme.version), bold=True
  396. )
  397. @themes.command("new")
  398. @click.argument("theme_identifier", callback=check_cookiecutter)
  399. @click.option("--template", "-t", type=click.STRING,
  400. default="https://github.com/sh4nks/cookiecutter-flaskbb-theme",
  401. help="Path to a cookiecutter template or to a valid git repo.")
  402. def new_theme(theme_identifier, template):
  403. """Creates a new theme based on the cookiecutter theme
  404. template. Defaults to this template:
  405. https://github.com:sh4nks/cookiecutter-flaskbb-theme.
  406. It will either accept a valid path on the filesystem
  407. or a URL to a Git repository which contains the cookiecutter template.
  408. """
  409. out_dir = os.path.join(current_app.root_path, "themes")
  410. click.secho("[+] Creating new theme {}".format(theme_identifier),
  411. fg="cyan")
  412. cookiecutter(template, output_dir=out_dir)
  413. click.secho("[+] Done. Created in {}".format(out_dir),
  414. fg="green", bold=True)
  415. @themes.command("remove")
  416. @click.argument("theme_identifier")
  417. @click.option("--force", "-f", default=False, is_flag=True,
  418. help="Removes the theme without asking for confirmation.")
  419. def remove_theme(theme_identifier, force):
  420. """Removes a theme from the filesystem."""
  421. validate_theme(theme_identifier)
  422. if not force and not \
  423. click.confirm(click.style("Are you sure?", fg="magenta")):
  424. sys.exit(0)
  425. theme = get_theme(theme_identifier)
  426. click.secho("[+] Removing theme from filesystem...", fg="cyan")
  427. shutil.rmtree(theme.path, ignore_errors=False, onerror=None)
  428. @main.group()
  429. def users():
  430. """Create, update or delete users."""
  431. pass
  432. @users.command("new")
  433. @click.option("--username", prompt=True,
  434. default=lambda: os.environ.get("USER", ""),
  435. help="The username of the new user.")
  436. @click.option("--email", prompt=True, type=EmailType(),
  437. help="The email address of the new user.")
  438. @click.option("--password", prompt=True, hide_input=True,
  439. confirmation_prompt=True,
  440. help="The password of the new user.")
  441. @click.option("--group", prompt=True, default="member",
  442. type=click.Choice(["admin", "super_mod", "mod", "member"]))
  443. def new_user(username, email, password, group):
  444. """Creates a new user. Omit any options to use the interactive mode."""
  445. try:
  446. user = create_user(username, password, email, group)
  447. click.secho("[+] User {} with Email {} in Group {} created.".format(
  448. user.username, user.email, user.primary_group.name), fg="cyan"
  449. )
  450. except IntegrityError:
  451. raise FlaskBBCLIError("Couldn't create the user because the "
  452. "username or email address is already taken.",
  453. fg="red")
  454. @users.command("update")
  455. @click.option("--username", prompt=True,
  456. help="The username of the user.")
  457. @click.option("--email", prompt=True, type=EmailType(),
  458. help="The new email address of the user.")
  459. @click.option("--password", prompt=True, hide_input=True,
  460. confirmation_prompt=True,
  461. help="The new password of the user.")
  462. @click.option("--group", prompt=True, default="member",
  463. help="The new primary group of the user",
  464. type=click.Choice(["admin", "super_mod", "mod", "member"]))
  465. def change_user(username, password, email, group):
  466. """Updates an user. Omit any options to use the interactive mode."""
  467. user = update_user(username, password, email, group)
  468. if user is None:
  469. raise FlaskBBCLIError("The user with username {} does not exist."
  470. .format(username), fg="red")
  471. click.secho("[+] User {} updated.".format(user.username), fg="cyan")
  472. @users.command("delete")
  473. @click.option("--username", prompt=True,
  474. help="The username of the user.")
  475. @click.option("--force", "-f", default=False, is_flag=True,
  476. help="Removes the user without asking for confirmation.")
  477. def delete_user(username, force):
  478. """Deletes an user."""
  479. user = User.query.filter_by(username=username).first()
  480. if user is None:
  481. raise FlaskBBCLIError("The user with username {} does not exist."
  482. .format(username), fg="red")
  483. if not force and not \
  484. click.confirm(click.style("Are you sure?", fg="magenta")):
  485. sys.exit(0)
  486. user.delete()
  487. click.secho("[+] User {} deleted.".format(user.username), fg="cyan")
  488. @main.command()
  489. def reindex():
  490. """Reindexes the search index."""
  491. click.secho("[+] Reindexing search index...", fg="cyan")
  492. whooshee.reindex()
  493. @main.command()
  494. @click.option("all_latest", "--all", "-a", default=False, is_flag=True,
  495. help="Upgrades migrations AND fixtures to the latest version.")
  496. @click.option("--fixture/", "-f", default=None,
  497. help="The fixture which should be upgraded or installed.")
  498. @click.option("--force", default=False, is_flag=True,
  499. help="Forcefully upgrades the fixtures.")
  500. def upgrade(all_latest, fixture, force):
  501. """Updates the migrations and fixtures."""
  502. if all_latest:
  503. click.secho("[+] Upgrading migrations to the latest version...",
  504. fg="cyan")
  505. upgrade_database()
  506. if fixture or all_latest:
  507. try:
  508. settings = import_string(
  509. "flaskbb.fixtures.{}".format(fixture)
  510. )
  511. settings = settings.fixture
  512. except ImportError:
  513. raise FlaskBBCLIError("{} fixture is not available"
  514. .format(fixture), fg="red")
  515. click.secho("[+] Updating fixtures...")
  516. count = update_settings_from_fixture(
  517. fixture=settings, overwrite_group=force, overwrite_setting=force
  518. )
  519. click.secho("[+] {} groups and {} settings updated.".format(
  520. len(count.keys()), len(count.values()), fg="green")
  521. )
  522. @main.command("download-emojis")
  523. @with_appcontext
  524. def download_emoji():
  525. """Downloads emojis from emoji-cheat-sheet.com.
  526. This command is probably going to be removed in future version.
  527. """
  528. click.secho("[+] Downloading emojis...", fg="cyan")
  529. HOSTNAME = "https://api.github.com"
  530. REPO = "/repos/arvida/emoji-cheat-sheet.com/contents/public/graphics/emojis"
  531. FULL_URL = "{}{}".format(HOSTNAME, REPO)
  532. DOWNLOAD_PATH = os.path.join(current_app.static_folder, "emoji")
  533. response = requests.get(FULL_URL)
  534. cached_count = 0
  535. count = 0
  536. for image in response.json():
  537. if not os.path.exists(os.path.abspath(DOWNLOAD_PATH)):
  538. raise FlaskBBCLIError(
  539. "{} does not exist.".format(os.path.abspath(DOWNLOAD_PATH)),
  540. fg="red")
  541. full_path = os.path.join(DOWNLOAD_PATH, image["name"])
  542. if not os.path.exists(full_path):
  543. count += 1
  544. f = open(full_path, 'wb')
  545. f.write(requests.get(image["download_url"]).content)
  546. f.close()
  547. if count == cached_count + 50:
  548. cached_count = count
  549. click.secho("[+] {} out of {} Emojis downloaded...".format(
  550. cached_count, len(response.json())), fg="cyan")
  551. click.secho("[+] Finished downloading {} Emojis.".format(count),
  552. fg="green")
  553. @main.command("celery", context_settings=dict(ignore_unknown_options=True,))
  554. @click.argument('celery_args', nargs=-1, type=click.UNPROCESSED)
  555. @click.option("show_help", "--help", "-h", is_flag=True,
  556. help="Shows this message and exits")
  557. @click.option("show_celery_help", "--help-celery", is_flag=True,
  558. help="Shows the celery help message")
  559. @click.pass_context
  560. @with_appcontext
  561. def start_celery(ctx, show_help, show_celery_help, celery_args):
  562. """Preconfigured wrapper around the 'celery' command.
  563. Additional CELERY_ARGS arguments are passed to celery."""
  564. if show_help:
  565. click.echo(ctx.get_help())
  566. sys.exit(0)
  567. if show_celery_help:
  568. click.echo(celery.start(argv=["--help"]))
  569. sys.exit(0)
  570. default_args = ['celery']
  571. default_args = default_args + list(celery_args)
  572. celery.start(argv=default_args)
  573. @main.command()
  574. @click.option("--server", "-s", default="gunicorn",
  575. type=click.Choice(["gunicorn", "gevent"]),
  576. help="The WSGI Server to run FlaskBB on.")
  577. @click.option("--host", "-h", default="127.0.0.1",
  578. help="The interface to bind FlaskBB to.")
  579. @click.option("--port", "-p", default="8000", type=int,
  580. help="The port to bind FlaskBB to.")
  581. @click.option("--workers", "-w", default=4,
  582. help="The number of worker processes for handling requests.")
  583. @click.option("--daemon", "-d", default=False, is_flag=True,
  584. help="Starts gunicorn as daemon.")
  585. @click.option("--config", "-c",
  586. help="The configuration file to use for FlaskBB.")
  587. def start(server, host, port, workers, config, daemon):
  588. """Starts a production ready wsgi server.
  589. TODO: Figure out a way how to forward additional args to gunicorn
  590. without causing any errors.
  591. """
  592. if server == "gunicorn":
  593. try:
  594. from gunicorn.app.base import Application
  595. class FlaskBBApplication(Application):
  596. def __init__(self, app, options=None):
  597. self.options = options or {}
  598. self.application = app
  599. super(FlaskBBApplication, self).__init__()
  600. def load_config(self):
  601. config = dict([
  602. (key, value) for key, value in iteritems(self.options)
  603. if key in self.cfg.settings and value is not None
  604. ])
  605. for key, value in iteritems(config):
  606. self.cfg.set(key.lower(), value)
  607. def load(self):
  608. return self.application
  609. options = {
  610. "bind": "{}:{}".format(host, port),
  611. "workers": workers,
  612. "daemon": daemon,
  613. }
  614. FlaskBBApplication(create_app(config=config), options).run()
  615. except ImportError:
  616. raise FlaskBBCLIError("Cannot import gunicorn. "
  617. "Make sure it is installed.", fg="red")
  618. elif server == "gevent":
  619. try:
  620. from gevent import __version__
  621. from gevent.pywsgi import WSGIServer
  622. click.secho("* Starting gevent {}".format(__version__))
  623. click.secho("* Listening on http://{}:{}/".format(host, port))
  624. http_server = WSGIServer((host, port), create_app(config=config))
  625. http_server.serve_forever()
  626. except ImportError:
  627. raise FlaskBBCLIError("Cannot import gevent. "
  628. "Make sure it is installed.", fg="red")
  629. @main.command("shell", short_help="Runs a shell in the app context.")
  630. @with_appcontext
  631. def shell_command():
  632. """Runs an interactive Python shell in the context of a given
  633. Flask application. The application will populate the default
  634. namespace of this shell according to it"s configuration.
  635. This is useful for executing small snippets of management code
  636. without having to manually configuring the application.
  637. This code snippet is taken from Flask"s cli module and modified to
  638. run IPython and falls back to the normal shell if IPython is not
  639. available.
  640. """
  641. import code
  642. banner = "Python %s on %s\nInstance Path: %s" % (
  643. sys.version,
  644. sys.platform,
  645. current_app.instance_path,
  646. )
  647. ctx = {"db": db}
  648. # Support the regular Python interpreter startup script if someone
  649. # is using it.
  650. startup = os.environ.get("PYTHONSTARTUP")
  651. if startup and os.path.isfile(startup):
  652. with open(startup, "r") as f:
  653. eval(compile(f.read(), startup, "exec"), ctx)
  654. ctx.update(current_app.make_shell_context())
  655. try:
  656. import IPython
  657. IPython.embed(banner1=banner, user_ns=ctx)
  658. except ImportError:
  659. code.interact(banner=banner, local=ctx)
  660. @main.command("urls", short_help="Show routes for the app.")
  661. @click.option("-r", "order_by", flag_value="rule", default=True,
  662. help="Order by route")
  663. @click.option("-e", "order_by", flag_value="endpoint",
  664. help="Order by endpoint")
  665. @click.option("-m", "order_by", flag_value="methods",
  666. help="Order by methods")
  667. @with_appcontext
  668. def list_urls(order_by):
  669. """Lists all available routes."""
  670. from flask import current_app
  671. rules = sorted(
  672. current_app.url_map.iter_rules(),
  673. key=lambda rule: getattr(rule, order_by)
  674. )
  675. max_rule_len = max(len(rule.rule) for rule in rules)
  676. max_rule_len = max(max_rule_len, len("Route"))
  677. max_endpoint_len = max(len(rule.endpoint) for rule in rules)
  678. max_endpoint_len = max(max_endpoint_len, len("Endpoint"))
  679. max_method_len = max(len(", ".join(rule.methods)) for rule in rules)
  680. max_method_len = max(max_method_len, len("Methods"))
  681. column_header_len = max_rule_len + max_endpoint_len + max_method_len + 4
  682. column_template = "{:<%s} {:<%s} {:<%s}" % (
  683. max_rule_len, max_endpoint_len, max_method_len
  684. )
  685. click.secho(column_template.format("Route", "Endpoint", "Methods"),
  686. fg="blue", bold=True)
  687. click.secho("=" * column_header_len, bold=True)
  688. for rule in rules:
  689. methods = ", ".join(rule.methods)
  690. click.echo(column_template.format(rule.rule, rule.endpoint, methods))