25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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