選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

845 行
25 KiB

  1. # -*- coding: utf-8 -*-
  2. import json
  3. import os
  4. import subprocess
  5. import sys
  6. from distutils.spawn import find_executable
  7. import click
  8. import frappe
  9. from frappe.commands import get_site, pass_context
  10. from frappe.exceptions import SiteNotSpecifiedError
  11. from frappe.utils import get_bench_path, update_progress_bar, cint
  12. @click.command('build')
  13. @click.option('--app', help='Build assets for app')
  14. @click.option('--apps', help='Build assets for specific apps')
  15. @click.option('--hard-link', is_flag=True, default=False, help='Copy the files instead of symlinking')
  16. @click.option('--make-copy', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking')
  17. @click.option('--restore', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking with force')
  18. @click.option('--production', is_flag=True, default=False, help='Build assets in production mode')
  19. @click.option('--verbose', is_flag=True, default=False, help='Verbose')
  20. @click.option('--force', is_flag=True, default=False, help='Force build assets instead of downloading available')
  21. def build(app=None, apps=None, hard_link=False, make_copy=False, restore=False, production=False, verbose=False, force=False):
  22. "Compile JS and CSS source files"
  23. from frappe.build import bundle, download_frappe_assets
  24. frappe.init('')
  25. if not apps and app:
  26. apps = app
  27. # dont try downloading assets if force used, app specified or running via CI
  28. if not (force or apps or os.environ.get('CI')):
  29. # skip building frappe if assets exist remotely
  30. skip_frappe = download_frappe_assets(verbose=verbose)
  31. else:
  32. skip_frappe = False
  33. # don't minify in developer_mode for faster builds
  34. development = frappe.local.conf.developer_mode or frappe.local.dev_server
  35. mode = "development" if development else "production"
  36. if production:
  37. mode = "production"
  38. if make_copy or restore:
  39. hard_link = make_copy or restore
  40. click.secho(
  41. "bench build: --make-copy and --restore options are deprecated in favour of --hard-link",
  42. fg="yellow",
  43. )
  44. bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_frappe=skip_frappe)
  45. @click.command('watch')
  46. @click.option('--apps', help='Watch assets for specific apps')
  47. def watch(apps=None):
  48. "Watch and compile JS and CSS files as and when they change"
  49. from frappe.build import watch
  50. frappe.init('')
  51. watch(apps)
  52. @click.command('clear-cache')
  53. @pass_context
  54. def clear_cache(context):
  55. "Clear cache, doctype cache and defaults"
  56. import frappe.sessions
  57. import frappe.website.render
  58. from frappe.desk.notifications import clear_notifications
  59. for site in context.sites:
  60. try:
  61. frappe.connect(site)
  62. frappe.clear_cache()
  63. clear_notifications()
  64. frappe.website.render.clear_cache()
  65. finally:
  66. frappe.destroy()
  67. if not context.sites:
  68. raise SiteNotSpecifiedError
  69. @click.command('clear-website-cache')
  70. @pass_context
  71. def clear_website_cache(context):
  72. "Clear website cache"
  73. import frappe.website.render
  74. for site in context.sites:
  75. try:
  76. frappe.init(site=site)
  77. frappe.connect()
  78. frappe.website.render.clear_cache()
  79. finally:
  80. frappe.destroy()
  81. if not context.sites:
  82. raise SiteNotSpecifiedError
  83. @click.command('destroy-all-sessions')
  84. @click.option('--reason')
  85. @pass_context
  86. def destroy_all_sessions(context, reason=None):
  87. "Clear sessions of all users (logs them out)"
  88. import frappe.sessions
  89. for site in context.sites:
  90. try:
  91. frappe.init(site=site)
  92. frappe.connect()
  93. frappe.sessions.clear_all_sessions(reason)
  94. frappe.db.commit()
  95. finally:
  96. frappe.destroy()
  97. if not context.sites:
  98. raise SiteNotSpecifiedError
  99. @click.command('show-config')
  100. @click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
  101. @pass_context
  102. def show_config(context, format):
  103. "Print configuration file to STDOUT in speified format"
  104. if not context.sites:
  105. raise SiteNotSpecifiedError
  106. sites_config = {}
  107. sites_path = os.getcwd()
  108. from frappe.utils.commands import render_table
  109. def transform_config(config, prefix=None):
  110. prefix = f"{prefix}." if prefix else ""
  111. site_config = []
  112. for conf, value in config.items():
  113. if isinstance(value, dict):
  114. site_config += transform_config(value, prefix=f"{prefix}{conf}")
  115. else:
  116. log_value = json.dumps(value) if isinstance(value, list) else value
  117. site_config += [[f"{prefix}{conf}", log_value]]
  118. return site_config
  119. for site in context.sites:
  120. frappe.init(site)
  121. if len(context.sites) != 1 and format == "text":
  122. if context.sites.index(site) != 0:
  123. click.echo()
  124. click.secho(f"Site {site}", fg="yellow")
  125. configuration = frappe.get_site_config(sites_path=sites_path, site_path=site)
  126. if format == "text":
  127. data = transform_config(configuration)
  128. data.insert(0, ['Config','Value'])
  129. render_table(data)
  130. if format == "json":
  131. sites_config[site] = configuration
  132. frappe.destroy()
  133. if format == "json":
  134. click.echo(frappe.as_json(sites_config))
  135. @click.command('reset-perms')
  136. @pass_context
  137. def reset_perms(context):
  138. "Reset permissions for all doctypes"
  139. from frappe.permissions import reset_perms
  140. for site in context.sites:
  141. try:
  142. frappe.init(site=site)
  143. frappe.connect()
  144. for d in frappe.db.sql_list("""select name from `tabDocType`
  145. where istable=0 and custom=0"""):
  146. frappe.clear_cache(doctype=d)
  147. reset_perms(d)
  148. finally:
  149. frappe.destroy()
  150. if not context.sites:
  151. raise SiteNotSpecifiedError
  152. @click.command('execute')
  153. @click.argument('method')
  154. @click.option('--args')
  155. @click.option('--kwargs')
  156. @click.option('--profile', is_flag=True, default=False)
  157. @pass_context
  158. def execute(context, method, args=None, kwargs=None, profile=False):
  159. "Execute a function"
  160. for site in context.sites:
  161. ret = ""
  162. try:
  163. frappe.init(site=site)
  164. frappe.connect()
  165. if args:
  166. try:
  167. args = eval(args)
  168. except NameError:
  169. args = [args]
  170. else:
  171. args = ()
  172. if kwargs:
  173. kwargs = eval(kwargs)
  174. else:
  175. kwargs = {}
  176. if profile:
  177. import cProfile
  178. pr = cProfile.Profile()
  179. pr.enable()
  180. try:
  181. ret = frappe.get_attr(method)(*args, **kwargs)
  182. except Exception:
  183. ret = frappe.safe_eval(method + "(*args, **kwargs)", eval_globals=globals(), eval_locals=locals())
  184. if profile:
  185. import pstats
  186. from six import StringIO
  187. pr.disable()
  188. s = StringIO()
  189. pstats.Stats(pr, stream=s).sort_stats('cumulative').print_stats(.5)
  190. print(s.getvalue())
  191. if frappe.db:
  192. frappe.db.commit()
  193. finally:
  194. frappe.destroy()
  195. if ret:
  196. from frappe.utils.response import json_handler
  197. print(json.dumps(ret, default=json_handler))
  198. if not context.sites:
  199. raise SiteNotSpecifiedError
  200. @click.command('add-to-email-queue')
  201. @click.argument('email-path')
  202. @pass_context
  203. def add_to_email_queue(context, email_path):
  204. "Add an email to the Email Queue"
  205. site = get_site(context)
  206. if os.path.isdir(email_path):
  207. with frappe.init_site(site):
  208. frappe.connect()
  209. for email in os.listdir(email_path):
  210. with open(os.path.join(email_path, email)) as email_data:
  211. kwargs = json.load(email_data)
  212. kwargs['delayed'] = True
  213. frappe.sendmail(**kwargs)
  214. frappe.db.commit()
  215. @click.command('export-doc')
  216. @click.argument('doctype')
  217. @click.argument('docname')
  218. @pass_context
  219. def export_doc(context, doctype, docname):
  220. "Export a single document to csv"
  221. import frappe.modules
  222. for site in context.sites:
  223. try:
  224. frappe.init(site=site)
  225. frappe.connect()
  226. frappe.modules.export_doc(doctype, docname)
  227. finally:
  228. frappe.destroy()
  229. if not context.sites:
  230. raise SiteNotSpecifiedError
  231. @click.command('export-json')
  232. @click.argument('doctype')
  233. @click.argument('path')
  234. @click.option('--name', help='Export only one document')
  235. @pass_context
  236. def export_json(context, doctype, path, name=None):
  237. "Export doclist as json to the given path, use '-' as name for Singles."
  238. from frappe.core.doctype.data_import.data_import import export_json
  239. for site in context.sites:
  240. try:
  241. frappe.init(site=site)
  242. frappe.connect()
  243. export_json(doctype, path, name=name)
  244. finally:
  245. frappe.destroy()
  246. if not context.sites:
  247. raise SiteNotSpecifiedError
  248. @click.command('export-csv')
  249. @click.argument('doctype')
  250. @click.argument('path')
  251. @pass_context
  252. def export_csv(context, doctype, path):
  253. "Export data import template with data for DocType"
  254. from frappe.core.doctype.data_import.data_import import export_csv
  255. for site in context.sites:
  256. try:
  257. frappe.init(site=site)
  258. frappe.connect()
  259. export_csv(doctype, path)
  260. finally:
  261. frappe.destroy()
  262. if not context.sites:
  263. raise SiteNotSpecifiedError
  264. @click.command('export-fixtures')
  265. @click.option('--app', default=None, help='Export fixtures of a specific app')
  266. @pass_context
  267. def export_fixtures(context, app=None):
  268. "Export fixtures"
  269. from frappe.utils.fixtures import export_fixtures
  270. for site in context.sites:
  271. try:
  272. frappe.init(site=site)
  273. frappe.connect()
  274. export_fixtures(app=app)
  275. finally:
  276. frappe.destroy()
  277. if not context.sites:
  278. raise SiteNotSpecifiedError
  279. @click.command('import-doc')
  280. @click.argument('path')
  281. @pass_context
  282. def import_doc(context, path, force=False):
  283. "Import (insert/update) doclist. If the argument is a directory, all files ending with .json are imported"
  284. from frappe.core.doctype.data_import.data_import import import_doc
  285. if not os.path.exists(path):
  286. path = os.path.join('..', path)
  287. if not os.path.exists(path):
  288. print('Invalid path {0}'.format(path))
  289. sys.exit(1)
  290. for site in context.sites:
  291. try:
  292. frappe.init(site=site)
  293. frappe.connect()
  294. import_doc(path)
  295. finally:
  296. frappe.destroy()
  297. if not context.sites:
  298. raise SiteNotSpecifiedError
  299. @click.command('import-csv')
  300. @click.argument('path')
  301. @click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records')
  302. @click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it')
  303. @click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode')
  304. @click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
  305. @pass_context
  306. def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True):
  307. "Import CSV using data import"
  308. from frappe.core.doctype.data_import_legacy import importer
  309. from frappe.utils.csvutils import read_csv_content
  310. site = get_site(context)
  311. if not os.path.exists(path):
  312. path = os.path.join('..', path)
  313. if not os.path.exists(path):
  314. print('Invalid path {0}'.format(path))
  315. sys.exit(1)
  316. with open(path, 'r') as csvfile:
  317. content = read_csv_content(csvfile.read())
  318. frappe.init(site=site)
  319. frappe.connect()
  320. try:
  321. importer.upload(content, submit_after_import=submit_after_import, no_email=no_email,
  322. ignore_encoding_errors=ignore_encoding_errors, overwrite=not only_insert,
  323. via_console=True)
  324. frappe.db.commit()
  325. except Exception:
  326. print(frappe.get_traceback())
  327. frappe.destroy()
  328. @click.command('data-import')
  329. @click.option('--file', 'file_path', type=click.Path(), required=True, help="Path to import file (.csv, .xlsx)")
  330. @click.option('--doctype', type=str, required=True)
  331. @click.option('--type', 'import_type', type=click.Choice(['Insert', 'Update'], case_sensitive=False), default='Insert', help="Insert New Records or Update Existing Records")
  332. @click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it')
  333. @click.option('--mute-emails', default=True, is_flag=True, help='Mute emails during import')
  334. @pass_context
  335. def data_import(context, file_path, doctype, import_type=None, submit_after_import=False, mute_emails=True):
  336. "Import documents in bulk from CSV or XLSX using data import"
  337. from frappe.core.doctype.data_import.data_import import import_file
  338. site = get_site(context)
  339. frappe.init(site=site)
  340. frappe.connect()
  341. import_file(doctype, file_path, import_type, submit_after_import, console=True)
  342. frappe.destroy()
  343. @click.command('bulk-rename')
  344. @click.argument('doctype')
  345. @click.argument('path')
  346. @pass_context
  347. def bulk_rename(context, doctype, path):
  348. "Rename multiple records via CSV file"
  349. from frappe.model.rename_doc import bulk_rename
  350. from frappe.utils.csvutils import read_csv_content
  351. site = get_site(context)
  352. with open(path, 'r') as csvfile:
  353. rows = read_csv_content(csvfile.read())
  354. frappe.init(site=site)
  355. frappe.connect()
  356. bulk_rename(doctype, rows, via_console = True)
  357. frappe.destroy()
  358. @click.command('mariadb')
  359. @pass_context
  360. def mariadb(context):
  361. """
  362. Enter into mariadb console for a given site.
  363. """
  364. import os
  365. site = get_site(context)
  366. if not site:
  367. raise SiteNotSpecifiedError
  368. frappe.init(site=site)
  369. # This is assuming you're within the bench instance.
  370. mysql = find_executable('mysql')
  371. os.execv(mysql, [
  372. mysql,
  373. '-u', frappe.conf.db_name,
  374. '-p'+frappe.conf.db_password,
  375. frappe.conf.db_name,
  376. '-h', frappe.conf.db_host or "localhost",
  377. '--pager=less -SFX',
  378. '--safe-updates',
  379. "-A"])
  380. @click.command('postgres')
  381. @pass_context
  382. def postgres(context):
  383. """
  384. Enter into postgres console for a given site.
  385. """
  386. site = get_site(context)
  387. frappe.init(site=site)
  388. # This is assuming you're within the bench instance.
  389. psql = find_executable('psql')
  390. subprocess.run([ psql, '-d', frappe.conf.db_name])
  391. @click.command('jupyter')
  392. @pass_context
  393. def jupyter(context):
  394. installed_packages = (r.split('==')[0] for r in subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'], encoding='utf8'))
  395. if 'jupyter' not in installed_packages:
  396. subprocess.check_output([sys.executable, '-m', 'pip', 'install', 'jupyter'])
  397. site = get_site(context)
  398. frappe.init(site=site)
  399. jupyter_notebooks_path = os.path.abspath(frappe.get_site_path('jupyter_notebooks'))
  400. sites_path = os.path.abspath(frappe.get_site_path('..'))
  401. try:
  402. os.stat(jupyter_notebooks_path)
  403. except OSError:
  404. print('Creating folder to keep jupyter notebooks at {}'.format(jupyter_notebooks_path))
  405. os.mkdir(jupyter_notebooks_path)
  406. bin_path = os.path.abspath('../env/bin')
  407. print('''
  408. Starting Jupyter notebook
  409. Run the following in your first cell to connect notebook to frappe
  410. ```
  411. import frappe
  412. frappe.init(site='{site}', sites_path='{sites_path}')
  413. frappe.connect()
  414. frappe.local.lang = frappe.db.get_default('lang')
  415. frappe.db.connect()
  416. ```
  417. '''.format(site=site, sites_path=sites_path))
  418. os.execv('{0}/jupyter'.format(bin_path), [
  419. '{0}/jupyter'.format(bin_path),
  420. 'notebook',
  421. jupyter_notebooks_path,
  422. ])
  423. @click.command('console')
  424. @pass_context
  425. def console(context):
  426. "Start ipython console for a site"
  427. site = get_site(context)
  428. frappe.init(site=site)
  429. frappe.connect()
  430. frappe.local.lang = frappe.db.get_default("lang")
  431. import IPython
  432. all_apps = frappe.get_installed_apps()
  433. failed_to_import = []
  434. for app in all_apps:
  435. try:
  436. locals()[app] = __import__(app)
  437. except ModuleNotFoundError:
  438. failed_to_import.append(app)
  439. all_apps.remove(app)
  440. print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
  441. if failed_to_import:
  442. print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
  443. IPython.embed(display_banner="", header="", colors="neutral")
  444. @click.command('run-tests')
  445. @click.option('--app', help="For App")
  446. @click.option('--doctype', help="For DocType")
  447. @click.option('--doctype-list-path', help="Path to .txt file for list of doctypes. Example erpnext/tests/server/agriculture.txt")
  448. @click.option('--test', multiple=True, help="Specific test")
  449. @click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests")
  450. @click.option('--module', help="Run tests in a module")
  451. @click.option('--profile', is_flag=True, default=False)
  452. @click.option('--coverage', is_flag=True, default=False)
  453. @click.option('--skip-test-records', is_flag=True, default=False, help="Don't create test records")
  454. @click.option('--skip-before-tests', is_flag=True, default=False, help="Don't run before tests hook")
  455. @click.option('--junit-xml-output', help="Destination file path for junit xml report")
  456. @click.option('--failfast', is_flag=True, default=False)
  457. @pass_context
  458. def run_tests(context, app=None, module=None, doctype=None, test=(), profile=False,
  459. coverage=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None,
  460. skip_test_records=False, skip_before_tests=False, failfast=False):
  461. "Run tests"
  462. import frappe.test_runner
  463. tests = test
  464. site = get_site(context)
  465. allow_tests = frappe.get_conf(site).allow_tests
  466. if not (allow_tests or os.environ.get('CI')):
  467. click.secho('Testing is disabled for the site!', bold=True)
  468. click.secho('You can enable tests by entering following command:')
  469. click.secho('bench --site {0} set-config allow_tests true'.format(site), fg='green')
  470. return
  471. frappe.init(site=site)
  472. frappe.flags.skip_before_tests = skip_before_tests
  473. frappe.flags.skip_test_records = skip_test_records
  474. if coverage:
  475. from coverage import Coverage
  476. # Generate coverage report only for app that is being tested
  477. source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
  478. omit=[
  479. '*.html',
  480. '*.js',
  481. '*.xml',
  482. '*.css',
  483. '*.less',
  484. '*.scss',
  485. '*.vue',
  486. '*/doctype/*/*_dashboard.py',
  487. '*/patches/*'
  488. ]
  489. if not app or app == 'frappe':
  490. omit.append('*/commands/*')
  491. cov = Coverage(source=[source_path], omit=omit)
  492. cov.start()
  493. ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
  494. force=context.force, profile=profile, junit_xml_output=junit_xml_output,
  495. ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast)
  496. if coverage:
  497. cov.stop()
  498. cov.save()
  499. if len(ret.failures) == 0 and len(ret.errors) == 0:
  500. ret = 0
  501. if os.environ.get('CI'):
  502. sys.exit(ret)
  503. @click.command('run-parallel-tests')
  504. @click.option('--app', help="For App", default='frappe')
  505. @click.option('--build-number', help="Build number", default=1)
  506. @click.option('--total-builds', help="Total number of builds", default=1)
  507. @click.option('--with-coverage', is_flag=True, help="Build coverage file")
  508. @click.option('--use-orchestrator', is_flag=True, help="Use orchestrator to run parallel tests")
  509. @pass_context
  510. def run_parallel_tests(context, app, build_number, total_builds, with_coverage=False, use_orchestrator=False):
  511. site = get_site(context)
  512. if use_orchestrator:
  513. from frappe.parallel_test_runner import ParallelTestWithOrchestrator
  514. ParallelTestWithOrchestrator(app, site=site, with_coverage=with_coverage)
  515. else:
  516. from frappe.parallel_test_runner import ParallelTestRunner
  517. ParallelTestRunner(app, site=site, build_number=build_number, total_builds=total_builds, with_coverage=with_coverage)
  518. @click.command('run-ui-tests')
  519. @click.argument('app')
  520. @click.option('--headless', is_flag=True, help="Run UI Test in headless mode")
  521. @click.option('--parallel', is_flag=True, help="Run UI Test in parallel mode")
  522. @click.option('--ci-build-id')
  523. @pass_context
  524. def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
  525. "Run UI tests"
  526. site = get_site(context)
  527. app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..'))
  528. site_url = frappe.utils.get_site_url(site)
  529. admin_password = frappe.get_conf(site).admin_password
  530. # override baseUrl using env variable
  531. site_env = 'CYPRESS_baseUrl={}'.format(site_url)
  532. password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
  533. os.chdir(app_base_path)
  534. node_bin = subprocess.getoutput("npm bin")
  535. cypress_path = "{0}/cypress".format(node_bin)
  536. plugin_path = "{0}/../cypress-file-upload".format(node_bin)
  537. # check if cypress in path...if not, install it.
  538. if not (
  539. os.path.exists(cypress_path)
  540. and os.path.exists(plugin_path)
  541. and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
  542. ):
  543. # install cypress
  544. click.secho("Installing Cypress...", fg="yellow")
  545. frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
  546. # run for headless mode
  547. run_or_open = 'run --browser firefox --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
  548. command = '{site_env} {password_env} {cypress} {run_or_open}'
  549. formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
  550. if parallel:
  551. formatted_command += ' --parallel'
  552. if ci_build_id:
  553. formatted_command += ' --ci-build-id {}'.format(ci_build_id)
  554. click.secho("Running Cypress...", fg="yellow")
  555. frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
  556. @click.command('serve')
  557. @click.option('--port', default=8000)
  558. @click.option('--profile', is_flag=True, default=False)
  559. @click.option('--noreload', "no_reload", is_flag=True, default=False)
  560. @click.option('--nothreading', "no_threading", is_flag=True, default=False)
  561. @pass_context
  562. def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None):
  563. "Start development web server"
  564. import frappe.app
  565. if not context.sites:
  566. site = None
  567. else:
  568. site = context.sites[0]
  569. frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.')
  570. @click.command('request')
  571. @click.option('--args', help='arguments like `?cmd=test&key=value` or `/api/request/method?..`')
  572. @click.option('--path', help='path to request JSON')
  573. @pass_context
  574. def request(context, args=None, path=None):
  575. "Run a request as an admin"
  576. import frappe.handler
  577. import frappe.api
  578. for site in context.sites:
  579. try:
  580. frappe.init(site=site)
  581. frappe.connect()
  582. if args:
  583. if "?" in args:
  584. frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")])
  585. else:
  586. frappe.local.form_dict = frappe._dict()
  587. if args.startswith("/api/method"):
  588. frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1]
  589. elif path:
  590. with open(os.path.join('..', path), 'r') as f:
  591. args = json.loads(f.read())
  592. frappe.local.form_dict = frappe._dict(args)
  593. frappe.handler.execute_cmd(frappe.form_dict.cmd)
  594. print(frappe.response)
  595. finally:
  596. frappe.destroy()
  597. if not context.sites:
  598. raise SiteNotSpecifiedError
  599. @click.command('make-app')
  600. @click.argument('destination')
  601. @click.argument('app_name')
  602. def make_app(destination, app_name):
  603. "Creates a boilerplate app"
  604. from frappe.utils.boilerplate import make_boilerplate
  605. make_boilerplate(destination, app_name)
  606. @click.command('set-config')
  607. @click.argument('key')
  608. @click.argument('value')
  609. @click.option('-g', '--global', 'global_', is_flag=True, default=False, help='Set value in bench config')
  610. @click.option('-p', '--parse', is_flag=True, default=False, help='Evaluate as Python Object')
  611. @click.option('--as-dict', is_flag=True, default=False, help='Legacy: Evaluate as Python Object')
  612. @pass_context
  613. def set_config(context, key, value, global_=False, parse=False, as_dict=False):
  614. "Insert/Update a value in site_config.json"
  615. from frappe.installer import update_site_config
  616. if as_dict:
  617. from frappe.utils.commands import warn
  618. warn("--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning)
  619. parse = as_dict
  620. if parse:
  621. import ast
  622. value = ast.literal_eval(value)
  623. if global_:
  624. sites_path = os.getcwd()
  625. common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
  626. update_site_config(key, value, validate=False, site_config_path=common_site_config_path)
  627. else:
  628. for site in context.sites:
  629. frappe.init(site=site)
  630. update_site_config(key, value, validate=False)
  631. frappe.destroy()
  632. @click.command('version')
  633. def get_version():
  634. "Show the versions of all the installed apps"
  635. from frappe.utils.change_log import get_app_branch
  636. frappe.init('')
  637. for m in sorted(frappe.get_all_apps()):
  638. branch_name = get_app_branch(m)
  639. module = frappe.get_module(m)
  640. app_hooks = frappe.get_module(m + ".hooks")
  641. if hasattr(app_hooks, '{0}_version'.format(branch_name)):
  642. print("{0} {1}".format(m, getattr(app_hooks, '{0}_version'.format(branch_name))))
  643. elif hasattr(module, "__version__"):
  644. print("{0} {1}".format(m, module.__version__))
  645. @click.command('rebuild-global-search')
  646. @click.option('--static-pages', is_flag=True, default=False, help='Rebuild global search for static pages')
  647. @pass_context
  648. def rebuild_global_search(context, static_pages=False):
  649. '''Setup help table in the current site (called after migrate)'''
  650. from frappe.utils.global_search import (get_doctypes_with_global_search, rebuild_for_doctype,
  651. get_routes_to_index, add_route_to_global_search, sync_global_search)
  652. for site in context.sites:
  653. try:
  654. frappe.init(site)
  655. frappe.connect()
  656. if static_pages:
  657. routes = get_routes_to_index()
  658. for i, route in enumerate(routes):
  659. add_route_to_global_search(route)
  660. frappe.local.request = None
  661. update_progress_bar('Rebuilding Global Search', i, len(routes))
  662. sync_global_search()
  663. else:
  664. doctypes = get_doctypes_with_global_search()
  665. for i, doctype in enumerate(doctypes):
  666. rebuild_for_doctype(doctype)
  667. update_progress_bar('Rebuilding Global Search', i, len(doctypes))
  668. finally:
  669. frappe.destroy()
  670. if not context.sites:
  671. raise SiteNotSpecifiedError
  672. commands = [
  673. build,
  674. clear_cache,
  675. clear_website_cache,
  676. jupyter,
  677. console,
  678. destroy_all_sessions,
  679. execute,
  680. export_csv,
  681. export_doc,
  682. export_fixtures,
  683. export_json,
  684. get_version,
  685. import_csv,
  686. data_import,
  687. import_doc,
  688. make_app,
  689. mariadb,
  690. postgres,
  691. request,
  692. reset_perms,
  693. run_tests,
  694. run_ui_tests,
  695. serve,
  696. set_config,
  697. show_config,
  698. watch,
  699. bulk_rename,
  700. add_to_email_queue,
  701. rebuild_global_search,
  702. run_parallel_tests
  703. ]