From 49de395407a3afaff291c84be4bae83807c925d0 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Wed, 18 May 2016 17:14:06 +0530 Subject: [PATCH] [fix] split commands into multiple files --- frappe/commands.py | 1184 ---------------------------------- frappe/commands/__init__.py | 59 ++ frappe/commands/docs.py | 108 ++++ frappe/commands/scheduler.py | 202 ++++++ frappe/commands/site.py | 348 ++++++++++ frappe/commands/translate.py | 91 +++ frappe/commands/utils.py | 417 ++++++++++++ 7 files changed, 1225 insertions(+), 1184 deletions(-) delete mode 100755 frappe/commands.py create mode 100644 frappe/commands/__init__.py create mode 100644 frappe/commands/docs.py create mode 100644 frappe/commands/scheduler.py create mode 100644 frappe/commands/site.py create mode 100644 frappe/commands/translate.py create mode 100644 frappe/commands/utils.py diff --git a/frappe/commands.py b/frappe/commands.py deleted file mode 100755 index 2ac72bfb4c..0000000000 --- a/frappe/commands.py +++ /dev/null @@ -1,1184 +0,0 @@ -# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals, absolute_import -import sys -import os -import json -import click -import hashlib -import cProfile -import StringIO -import pstats -import frappe -import frappe.utils -from frappe.utils import cint -from distutils.spawn import find_executable -from functools import wraps - -click.disable_unicode_literals_warning = True - -def pass_context(f): - @wraps(f) - def _func(ctx, *args, **kwargs): - profile = ctx.obj['profile'] - if profile: - pr = cProfile.Profile() - pr.enable() - - ret = f(frappe._dict(ctx.obj), *args, **kwargs) - - if profile: - pr.disable() - s = StringIO.StringIO() - ps = pstats.Stats(pr, stream=s)\ - .sort_stats('cumtime', 'tottime', 'ncalls') - ps.print_stats() - print s.getvalue() - - return ret - - return click.pass_context(_func) - -def get_single_site(context): - if not context.sites or not len(context.sites) == 1: - print 'please select a site' - sys.exit(1) - site = context.sites[0] - return site - -def call_command(cmd, context): - return click.Context(cmd, obj=context).forward(cmd) - -@click.command('new-site') -@click.argument('site') -@click.option('--db-name', help='Database name') -@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB') -@click.option('--mariadb-root-password', help='Root password for MariaDB') -@click.option('--admin-password', help='Administrator password for new site', default=None) -@click.option('--verbose', is_flag=True, default=False, help='Verbose') -@click.option('--force', help='Force restore if site/database already exists', is_flag=True, default=False) -@click.option('--source_sql', help='Initiate database with a SQL file') -@click.option('--install-app', multiple=True, help='Install app after installation') -def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None, verbose=False, install_apps=None, source_sql=None, force=None, install_app=None, db_name=None): - "Create a new site" - if not db_name: - db_name = hashlib.sha1(site).hexdigest()[:10] - - frappe.init(site=site, new_site=True) - _new_site(db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force) - if len(frappe.utils.get_sites()) == 1: - use(site) - -def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None, verbose=False, install_apps=None, source_sql=None,force=False, reinstall=False): - "Install a new Frappe site" - from frappe.installer import install_db, make_site_dirs - from frappe.installer import install_app as _install_app - import frappe.utils.scheduler - - frappe.init(site=site) - - try: - # enable scheduler post install? - enable_scheduler = _is_scheduler_enabled() - except: - enable_scheduler = False - - install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name, admin_password=admin_password, verbose=verbose, source_sql=source_sql,force=force, reinstall=reinstall) - make_site_dirs() - _install_app("frappe", verbose=verbose, set_as_patched=not source_sql) - - if frappe.conf.get("install_apps"): - for app in frappe.conf.install_apps: - _install_app(app, verbose=verbose, set_as_patched=not source_sql) - - if install_apps: - for app in install_apps: - _install_app(app, verbose=verbose, set_as_patched=not source_sql) - - frappe.utils.scheduler.toggle_scheduler(enable_scheduler) - frappe.db.commit() - - scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled" - print "*** Scheduler is", scheduler_status, "***" - frappe.destroy() - -def _is_scheduler_enabled(): - enable_scheduler = False - try: - frappe.connect() - enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False - except: - pass - finally: - frappe.db.close() - - return enable_scheduler - -@click.command('restore') -@click.argument('sql-file-path') -@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB') -@click.option('--mariadb-root-password', help='Root password for MariaDB') -@click.option('--db-name', help='Database name for site in case it is a new one') -@click.option('--admin-password', help='Administrator password for new site') -@click.option('--install-app', multiple=True, help='Install app after installation') -@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file') -@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file') -@pass_context -def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): - "Restore site database from an sql file" - from frappe.installer import extract_sql_gzip, extract_tar_files - # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file - if sql_file_path.endswith('sql.gz'): - sql_file_path = extract_sql_gzip(os.path.abspath(sql_file_path)) - - site = get_single_site(context) - frappe.init(site=site) - db_name = db_name or frappe.conf.db_name or hashlib.sha1(site).hexdigest()[:10] - _new_site(db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=context.verbose, install_apps=install_app, source_sql=sql_file_path, force=context.force) - - # Extract public and/or private files to the restored site, if user has given the path - if with_public_files: - extract_tar_files(site, with_public_files, 'public') - - if with_private_files: - extract_tar_files(site, with_private_files, 'private') - -@click.command('reinstall') -@pass_context -def reinstall(context): - "Reinstall site ie. wipe all data and start over" - site = get_single_site(context) - try: - frappe.init(site=site) - frappe.connect() - frappe.clear_cache() - installed = frappe.get_installed_apps() - frappe.clear_cache() - except Exception: - installed = [] - finally: - if frappe.db: - frappe.db.close() - frappe.destroy() - - frappe.init(site=site) - _new_site(frappe.conf.db_name, site, verbose=context.verbose, force=True, reinstall=True, install_apps=installed) - -@click.command('install-app') -@click.argument('app') -@pass_context -def install_app(context, app): - "Install a new app to site" - from frappe.installer import install_app as _install_app - for site in context.sites: - frappe.init(site=site) - frappe.connect() - try: - _install_app(app, verbose=context.verbose) - finally: - frappe.destroy() - -@click.command('list-apps') -@pass_context -def list_apps(context): - "List apps in site" - site = get_single_site(context) - frappe.init(site=site) - frappe.connect() - print "\n".join(frappe.get_installed_apps()) - frappe.destroy() - -@click.command('add-system-manager') -@click.argument('email') -@click.option('--first-name') -@click.option('--last-name') -@pass_context -def add_system_manager(context, email, first_name, last_name): - "Add a new system manager to a site" - import frappe.utils.user - for site in context.sites: - frappe.connect(site=site) - try: - frappe.utils.user.add_system_manager(email, first_name, last_name) - frappe.db.commit() - finally: - frappe.destroy() - -@click.command('migrate') -@click.option('--rebuild-website', help="Rebuild webpages after migration") -@pass_context -def migrate(context, rebuild_website=False): - "Run patches, sync schema and rebuild files/translations" - from frappe.migrate import migrate - - for site in context.sites: - print 'Migrating', site - frappe.init(site=site) - frappe.connect() - try: - migrate(context.verbose, rebuild_website=rebuild_website) - finally: - frappe.destroy() - -@click.command('run-patch') -@click.argument('module') -@pass_context -def run_patch(context, module): - "Run a particular patch" - import frappe.modules.patch_handler - for site in context.sites: - frappe.init(site=site) - try: - frappe.connect() - frappe.modules.patch_handler.run_single(module, force=context.force) - finally: - frappe.destroy() - -@click.command('reload-doc') -@click.argument('module') -@click.argument('doctype') -@click.argument('docname') -@pass_context -def reload_doc(context, module, doctype, docname): - "Reload schema for a DocType" - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - frappe.reload_doc(module, doctype, docname, force=context.force) - frappe.db.commit() - finally: - frappe.destroy() - -@click.command('build') -@click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking') -@click.option('--verbose', is_flag=True, default=False, help='Verbose') -def build(make_copy=False, verbose=False): - "Minify + concatenate JS and CSS files, build translations" - import frappe.build - import frappe - frappe.init('') - frappe.build.bundle(False, make_copy=make_copy, verbose=verbose) - -@click.command('watch') -def watch(): - "Watch and concatenate JS and CSS files as and when they change" - import frappe.build - frappe.init('') - frappe.build.watch(True) - -@click.command('clear-cache') -@pass_context -def clear_cache(context): - "Clear cache, doctype cache and defaults" - import frappe.sessions - import frappe.website.render - from frappe.desk.notifications import clear_notifications - for site in context.sites: - try: - frappe.connect(site) - frappe.clear_cache() - clear_notifications() - frappe.website.render.clear_cache() - finally: - frappe.destroy() - -@click.command('clear-website-cache') -@pass_context -def clear_website_cache(context): - "Clear website cache" - import frappe.website.render - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - frappe.website.render.clear_cache() - finally: - frappe.destroy() - -@click.command('destroy-all-sessions') -@pass_context -def destroy_all_sessions(context): - "Clear sessions of all users (logs them out)" - import frappe.sessions - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - frappe.sessions.clear_all_sessions() - frappe.db.commit() - finally: - frappe.destroy() - -@click.command('sync-www') -@click.option('--force', help='Rebuild all pages', is_flag=True, default=False) -@pass_context -def sync_www(context, force=False): - "Sync files from static pages from www directory to Web Pages" - from frappe.website import statics - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - statics.sync_statics(rebuild=force) - frappe.db.commit() - finally: - frappe.destroy() - -@click.command('build-website') -@pass_context -def build_website(context): - "Sync statics and clear cache" - from frappe.website import render, statics - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - render.clear_cache() - statics.sync(verbose=context.verbose).start(rebuild=True) - frappe.db.commit() - finally: - frappe.destroy() - -@click.command('make-docs') -@pass_context -@click.argument('app') -@click.argument('docs_version') -def make_docs(context, app, docs_version): - "Setup docs in target folder of target app" - from frappe.utils.setup_docs import setup_docs - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - make = setup_docs(app) - make.build(docs_version) - finally: - frappe.destroy() - -@click.command('sync-docs') -@pass_context -@click.argument('app') -def sync_docs(context, app): - "Sync docs from /docs folder into the database (Web Page)" - from frappe.utils.setup_docs import setup_docs - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - make = setup_docs(app) - make.sync_docs() - finally: - frappe.destroy() - - -@click.command('write-docs') -@pass_context -@click.argument('app') -@click.argument('target') -@click.option('--local', default=False, is_flag=True, help='Run app locally') -def write_docs(context, app, target, local=False): - "Setup docs in target folder of target app" - from frappe.utils.setup_docs import setup_docs - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - make = setup_docs(app) - make.make_docs(target, local) - finally: - frappe.destroy() - -@click.command('build-docs') -@pass_context -@click.argument('app') -@click.option('--docs-version', default='current') -@click.option('--target', default=None) -@click.option('--local', default=False, is_flag=True, help='Run app locally') -@click.option('--watch', default=False, is_flag=True, help='Watch for changes and rewrite') -def build_docs(context, app, docs_version="current", target=None, local=False, watch=False): - "Setup docs in target folder of target app" - from frappe.utils import watch as start_watch - if not target: - target = os.path.abspath(os.path.join("..", "docs", app)) - - for site in context.sites: - _build_docs_once(site, app, docs_version, target, local) - - if watch: - def trigger_make(source_path, event_type): - if "/templates/autodoc/" in source_path: - _build_docs_once(site, app, docs_version, target, local) - - elif ("/docs.css" in source_path - or "/docs/" in source_path - or "docs.py" in source_path): - _build_docs_once(site, app, docs_version, target, local, only_content_updated=True) - - apps_path = frappe.get_app_path("frappe", "..", "..") - start_watch(apps_path, handler=trigger_make) - -def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False): - from frappe.utils.setup_docs import setup_docs - - try: - - frappe.init(site=site) - frappe.connect() - make = setup_docs(app) - - if not only_content_updated: - make.build(docs_version) - make.sync_docs() - - make.make_docs(target, local) - - finally: - frappe.destroy() - - -@click.command('reset-perms') -@pass_context -def reset_perms(context): - "Reset permissions for all doctypes" - from frappe.permissions import reset_perms - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - for d in frappe.db.sql_list("""select name from `tabDocType` - where istable=0 and custom=0"""): - frappe.clear_cache(doctype=d) - reset_perms(d) - finally: - frappe.destroy() - -@click.command('execute') -@click.argument('method') -@click.option('--args') -@click.option('--kwargs') -@pass_context -def execute(context, method, args=None, kwargs=None): - "Execute a function" - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - - if args: - try: - args = eval(args) - except NameError: - args = [args] - else: - args = () - - if kwargs: - kwargs = eval(args) - else: - kwargs = {} - - ret = frappe.get_attr(method)(*args, **kwargs) - - if frappe.db: - frappe.db.commit() - finally: - frappe.destroy() - if ret: - print json.dumps(ret) - - -@click.command('trigger-scheduler-event') -@click.argument('event') -@pass_context -def trigger_scheduler_event(context, event): - "Trigger a scheduler event" - import frappe.utils.scheduler - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - frappe.utils.scheduler.trigger(site, event, now=context.force) - finally: - frappe.destroy() - -@click.command('enable-scheduler') -@pass_context -def enable_scheduler(context): - "Enable scheduler" - import frappe.utils.scheduler - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - frappe.utils.scheduler.enable_scheduler() - frappe.db.commit() - print "Enabled for", site - finally: - frappe.destroy() - -@click.command('disable-scheduler') -@pass_context -def disable_scheduler(context): - "Disable scheduler" - import frappe.utils.scheduler - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - frappe.utils.scheduler.disable_scheduler() - frappe.db.commit() - print "Disabled for", site - finally: - frappe.destroy() - - - -@click.command('scheduler') -@click.option('--site', help='site name') -@click.argument('state') -@pass_context -def scheduler(context, state, site=None): - from frappe.installer import update_site_config - if not site: - try: - site = context.sites[0] - except (IndexError, TypeError): - print 'Please specify --site sitename' - return sys.exit(1) - try: - frappe.init(site=site) - state = state.lower() - if state == 'pause': - update_site_config('pause_scheduler', 1) - elif state == 'resume': - update_site_config('pause_scheduler', 0) - elif state == 'disable': - update_site_config('disable_scheduler', 1) - elif state == 'enable': - update_site_config('disable_scheduler', 0) - else: - print "Invalid option\nValid options : pause/resume/enable/disable" - finally: - frappe.destroy() - - -@click.command('maintenance-mode') -@click.option('--site', help='site name') -@click.argument('state') -@pass_context -def maintenance_mode(context, state, site=None): - from frappe.installer import update_site_config - if not site: - try: - site = context.sites[0] - except (IndexError, TypeError): - print 'Please specify --site sitename' - return sys.exit(1) - try: - frappe.init(site=site) - state = state.lower() - if state == 'on': - update_site_config('maintenance_mode', 1) - elif state == 'off': - update_site_config('maintenance_mode', 0) - else: - print "Invalid option\nValid options : on/off" - finally: - frappe.destroy() - - - -@click.command('ready-for-migration') -@click.option('--site', help='site name') -@pass_context -def ready_for_migration(context, site=None): - from frappe.utils.doctor import get_pending_jobs - "Check if site is ready for migration" - if not site: - try: - site = context.sites[0] - except (IndexError, TypeError): - print 'Please specify --site sitename' - return sys.exit(1) - try: - frappe.init(site=site) - if get_pending_jobs(site=site): - print 'Pending jobs for site, not ready for migration' - return sys.exit(1) - else: - print 'No pending jobs, ready for migration' - return 0 - finally: - frappe.destroy() - - -@click.command('export-doc') -@click.argument('doctype') -@click.argument('docname') -@pass_context -def export_doc(context, doctype, docname): - "Export a single document to csv" - import frappe.modules - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - frappe.modules.export_doc(doctype, docname) - finally: - frappe.destroy() - -@click.command('export-json') -@click.argument('doctype') -@click.argument('name') -@click.argument('path') -@pass_context -def export_json(context, doctype, name, path): - "Export doclist as json to the given path, use '-' as name for Singles." - from frappe.core.page.data_import_tool import data_import_tool - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - data_import_tool.export_json(doctype, path, name=name) - finally: - frappe.destroy() - -@click.command('export-csv') -@click.argument('doctype') -@click.argument('path') -@pass_context -def export_csv(context, doctype, path): - "Export data import template for DocType" - from frappe.core.page.data_import_tool import data_import_tool - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - data_import_tool.export_csv(doctype, path) - finally: - frappe.destroy() - -@click.command('export-fixtures') -@pass_context -def export_fixtures(context): - "Export fixtures" - from frappe.utils.fixtures import export_fixtures - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - export_fixtures() - finally: - frappe.destroy() - -@click.command('import-doc') -@click.argument('path') -@pass_context -def import_doc(context, path, force=False): - "Import (insert/update) doclist. If the argument is a directory, all files ending with .json are imported" - from frappe.core.page.data_import_tool import data_import_tool - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - data_import_tool.import_doc(path, overwrite=context.force) - finally: - frappe.destroy() - -@click.command('import-csv') -@click.argument('path') -@click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records') -@click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it') -@click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode') -@pass_context -def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False): - "Import CSV using data import tool" - from frappe.core.page.data_import_tool import importer - from frappe.utils.csvutils import read_csv_content - site = get_single_site(context) - - with open(path, 'r') as csvfile: - content = read_csv_content(csvfile.read()) - - frappe.init(site=site) - frappe.connect() - - try: - importer.upload(content, submit_after_import=submit_after_import, - ignore_encoding_errors=ignore_encoding_errors, overwrite=not only_insert, - via_console=True) - frappe.db.commit() - except Exception: - print frappe.get_traceback() - - frappe.destroy() - -@click.command('bulk-rename') -@click.argument('doctype') -@click.argument('path') -@pass_context -def _bulk_rename(context, doctype, path): - "Rename multiple records via CSV file" - from frappe.model.rename_doc import bulk_rename - from frappe.utils.csvutils import read_csv_content - - site = get_single_site(context) - - with open(path, 'r') as csvfile: - rows = read_csv_content(csvfile.read()) - - frappe.init(site=site) - frappe.connect() - - bulk_rename(doctype, rows, via_console = True) - - frappe.destroy() - -# translation -@click.command('build-message-files') -@pass_context -def build_message_files(context): - "Build message files for translation" - import frappe.translate - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - frappe.translate.rebuild_all_translation_files() - finally: - frappe.destroy() - -@click.command('new-language') #, help="Create lang-code.csv for given app") -@pass_context -@click.argument('lang_code') #, help="Language code eg. en") -@click.argument('app') #, help="App name eg. frappe") -def new_language(context, lang_code, app): - """Create lang-code.csv for given app""" - import frappe.translate - - if not context['sites']: - raise Exception('--site is required') - - # init site - frappe.connect(site=context['sites'][0]) - frappe.translate.write_translations_file(app, lang_code) - - print "File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(app=app, lang_code=lang_code) - print "You will need to add the language in frappe/data/languages.txt, if you haven't done it already." - -@click.command('get-untranslated') -@click.argument('lang') -@click.argument('untranslated_file') -@click.option('--all', default=False, is_flag=True, help='Get all message strings') -@pass_context -def get_untranslated(context, lang, untranslated_file, all=None): - "Get untranslated strings for language" - import frappe.translate - site = get_single_site(context) - try: - frappe.init(site=site) - frappe.connect() - frappe.translate.get_untranslated(lang, untranslated_file, get_all=all) - finally: - frappe.destroy() - -@click.command('update-translations') -@click.argument('lang') -@click.argument('untranslated_file') -@click.argument('translated-file') -@pass_context -def update_translations(context, lang, untranslated_file, translated_file): - "Update translated strings" - import frappe.translate - site = get_single_site(context) - try: - frappe.init(site=site) - frappe.connect() - frappe.translate.update_translations(lang, untranslated_file, translated_file) - finally: - frappe.destroy() - -@click.command('import-translations') -@click.argument('lang') -@click.argument('path') -@pass_context -def import_translations(context, lang, path): - "Update translated strings" - import frappe.translate - site = get_single_site(context) - try: - frappe.init(site=site) - frappe.connect() - frappe.translate.import_translations(lang, path) - finally: - frappe.destroy() - - -@click.command('set-admin-password') -@click.argument('admin-password') -@pass_context -def set_admin_password(context, admin_password): - "Set Administrator password for a site" - import getpass - - for site in context.sites: - try: - frappe.init(site=site) - - while not admin_password: - admin_password = getpass.getpass("Administrator's password for {0}: ".format(site)) - - frappe.connect() - frappe.db.sql("""update __Auth set `password`=password(%s) - where user='Administrator'""", (admin_password,)) - frappe.db.commit() - admin_password = None - finally: - frappe.destroy() - -@click.command('mysql') -@pass_context -def mysql(context): - "Start Mariadb console for a site" - site = get_single_site(context) - frappe.init(site=site) - msq = find_executable('mysql') - os.execv(msq, [msq, '-u', frappe.conf.db_name, '-p'+frappe.conf.db_password, frappe.conf.db_name, '-h', frappe.conf.db_host or "localhost", "-A"]) - -@click.command('console') -@pass_context -def console(context): - "Start ipython console for a site" - site = get_single_site(context) - frappe.init(site=site) - frappe.connect() - frappe.local.lang = frappe.db.get_default("lang") - import IPython - IPython.embed() - -@click.command('run-tests') -@click.option('--app', help="For App") -@click.option('--doctype', help="For DocType") -@click.option('--test', multiple=True, help="Specific test") -@click.option('--driver', help="For Travis") -@click.option('--module', help="Run tests in a module") -@click.option('--profile', is_flag=True, default=False) -@pass_context -def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None, profile=False): - "Run tests" - import frappe.test_runner - from frappe.utils import sel - tests = test - - site = get_single_site(context) - frappe.init(site=site) - - if frappe.conf.run_selenium_tests and False: - sel.start(context.verbose, driver) - - try: - ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, - force=context.force, profile=profile) - if len(ret.failures) == 0 and len(ret.errors) == 0: - ret = 0 - finally: - pass - if frappe.conf.run_selenium_tests: - sel.close() - - sys.exit(ret) - -@click.command('serve') -@click.option('--port', default=8000) -@click.option('--profile', is_flag=True, default=False) -@pass_context -def serve(context, port=None, profile=False, sites_path='.', site=None): - "Start development web server" - if not context.sites: - site = None - else: - site = context.sites[0] - import frappe.app - frappe.app.serve(port=port, profile=profile, site=site, sites_path='.') - -@click.command('request') -@click.argument('args') -@pass_context -def request(context, args): - "Run a request as an admin" - import frappe.handler - import frappe.api - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - if "?" in args: - frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")]) - else: - frappe.local.form_dict = frappe._dict() - - if args.startswith("/api/method"): - frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1] - - frappe.handler.execute_cmd(frappe.form_dict.cmd) - - print frappe.response - finally: - frappe.destroy() - -@click.command('doctor') #Passing context always gets a site and if there is no use site it breaks -@click.option('--site', help='site name') -def doctor(site=None): - "Get diagnostic info about background workers" - from frappe.utils.doctor import doctor as _doctor - return _doctor(site=site) - -@click.command('show-pending-jobs') -@click.option('--site', help='site name') -@pass_context -def show_pending_jobs(context, site=None): - "Get diagnostic info about background jobs" - if not site: - try: - site = context.sites[0] - except (IndexError, TypeError): - print 'Please specify --site sitename' - return 1 - - from frappe.utils.doctor import pending_jobs as _pending_jobs - return _pending_jobs(site=site) - -@click.command('purge-jobs') -@click.option('--site', help='site name') -@click.option('--queue', default=None, help='one of "low", "default", "high') -@click.option('--event', default=None, help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"') -def purge_jobs(site=None, queue=None, event=None): - "Purge any pending periodic tasks, if event option is not given, it will purge everything for the site" - from frappe.utils.doctor import purge_pending_jobs - frappe.init(site or '') - count = purge_pending_jobs(event=event, site=site, queue=queue) - print "Purged {} jobs".format(count) - -@click.command('dump-queue-status') -def dump_queue_status(): - "Dump detailed diagnostic infomation for task queues in JSON format" - frappe.init('') - from frappe.utils.doctor import dump_queue_status as _dump_queue_status, inspect_queue - print json.dumps(_dump_queue_status(), indent=1) - inspect_queue() - -@click.command('make-app') -@click.argument('destination') -@click.argument('app_name') -def make_app(destination, app_name): - "Creates a boilerplate app" - from frappe.utils.boilerplate import make_boilerplate - make_boilerplate(destination, app_name) - -@click.command('use') -@click.argument('site') -def _use(site, sites_path='.'): - "Set a default site" - use(site, sites_path=sites_path) - -def use(site, sites_path='.'): - with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile: - sitefile.write(site) - -@click.command('backup') -@click.option('--with-files', default=False, is_flag=True, help="Take backup with files") -@pass_context -def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, - backup_path_private_files=None, quiet=False): - "Backup" - from frappe.utils.backups import scheduled_backup - verbose = context.verbose - for site in context.sites: - frappe.init(site=site) - frappe.connect() - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) - if verbose: - from frappe.utils import now - print "database backup taken -", odb.backup_path_db, "- on", now() - if with_files: - print "files backup taken -", odb.backup_path_files, "- on", now() - print "private files backup taken -", odb.backup_path_private_files, "- on", now() - - frappe.destroy() - -@click.command('remove-from-installed-apps') -@click.argument('app') -@pass_context -def remove_from_installed_apps(context, app): - "Remove app from site's installed-apps list" - from frappe.installer import remove_from_installed_apps - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - remove_from_installed_apps(app) - finally: - frappe.destroy() - -@click.command('uninstall-app') -@click.argument('app') -@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False) -@pass_context -def uninstall(context, app, dry_run=False): - "Remove app and linked modules from site" - from frappe.installer import remove_app - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - remove_app(app, dry_run) - finally: - frappe.destroy() - -def move(dest_dir, site): - import os - if not os.path.isdir(dest_dir): - raise Exception, "destination is not a directory or does not exist" - - frappe.init(site) - old_path = frappe.utils.get_site_path() - new_path = os.path.join(dest_dir, site) - - # check if site dump of same name already exists - site_dump_exists = True - count = 0 - while site_dump_exists: - final_new_path = new_path + (count and str(count) or "") - site_dump_exists = os.path.exists(final_new_path) - count = int(count or 0) + 1 - - os.rename(old_path, final_new_path) - frappe.destroy() - return final_new_path - - -@click.command('set-config') -@click.argument('key') -@click.argument('value') -@pass_context -def set_config(context, key, value): - "Insert/Update a value in site_config.json" - from frappe.installer import update_site_config - for site in context.sites: - frappe.init(site=site) - update_site_config(key, value) - frappe.destroy() - -@click.command('drop-site') -@click.argument('site') -@click.option('--root-login', default='root') -@click.option('--root-password') -@click.option('--archived-sites-path') -def drop_site(site, root_login='root', root_password=None, archived_sites_path=None): - "Remove site from database and filesystem" - from frappe.installer import get_current_host, make_connection - from frappe.model.db_schema import DbManager - from frappe.utils.backups import scheduled_backup - - frappe.init(site=site) - frappe.connect() - scheduled_backup(ignore_files=False, force=True) - - db_name = frappe.local.conf.db_name - frappe.local.db = make_connection(root_login, root_password) - dbman = DbManager(frappe.local.db) - dbman.delete_user(db_name, get_current_host()) - dbman.drop_database(db_name) - - if not archived_sites_path: - archived_sites_path = os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived_sites') - - if not os.path.exists(archived_sites_path): - os.mkdir(archived_sites_path) - - move(archived_sites_path, site) - -@click.command('version') -def get_version(): - "Show the versions of all the installed apps" - frappe.init('') - for m in sorted(frappe.get_all_apps()): - module = frappe.get_module(m) - if hasattr(module, "__version__"): - print "{0} {1}".format(m, module.__version__) - -@click.command('schedule') -def start_scheduler(): - from frappe.utils.scheduler import start_scheduler - start_scheduler() - -@click.command('worker') -@click.option('--queue', type=str) -def start_worker(queue): - from frappe.utils.background_jobs import start_worker - start_worker(queue) - -commands = [ - new_site, - restore, - reinstall, - install_app, - list_apps, - add_system_manager, - migrate, - run_patch, - reload_doc, - build, - watch, - clear_cache, - clear_website_cache, - destroy_all_sessions, - sync_www, - build_website, - make_docs, - sync_docs, - write_docs, - build_docs, - reset_perms, - execute, - trigger_scheduler_event, - enable_scheduler, - disable_scheduler, - scheduler, - maintenance_mode, - ready_for_migration, - export_doc, - export_json, - export_csv, - export_fixtures, - import_doc, - import_csv, - _bulk_rename, - build_message_files, - get_untranslated, - update_translations, - import_translations, - set_admin_password, - mysql, - run_tests, - serve, - request, - doctor, - show_pending_jobs, - purge_jobs, - dump_queue_status, - console, - make_app, - _use, - backup, - remove_from_installed_apps, - uninstall, - drop_site, - set_config, - get_version, - new_language, - start_worker, - start_scheduler, -] diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py new file mode 100644 index 0000000000..d0c4bdb3a3 --- /dev/null +++ b/frappe/commands/__init__.py @@ -0,0 +1,59 @@ +# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals, absolute_import +import sys +import click +import cProfile +import StringIO +import pstats +import frappe +import frappe.utils +from functools import wraps + +click.disable_unicode_literals_warning = True + +def pass_context(f): + @wraps(f) + def _func(ctx, *args, **kwargs): + profile = ctx.obj['profile'] + if profile: + pr = cProfile.Profile() + pr.enable() + + ret = f(frappe._dict(ctx.obj), *args, **kwargs) + + if profile: + pr.disable() + s = StringIO.StringIO() + ps = pstats.Stats(pr, stream=s)\ + .sort_stats('cumtime', 'tottime', 'ncalls') + ps.print_stats() + print s.getvalue() + + return ret + + return click.pass_context(_func) + +def get_site(context): + try: + site = context.sites[0] + return site + except (IndexError, TypeError): + print 'Please specify --site sitename' + sys.exit(1) + +def call_command(cmd, context): + return click.Context(cmd, obj=context).forward(cmd) + +def get_commands(): + # prevent circular imports + from .docs import commands as doc_commands + from .scheduler import commands as scheduler_commands + from .site import commands as site_commands + from .translate import commands as translate_commands + from .utils import commands as utils_commands + + return list(set(doc_commands + scheduler_commands + site_commands + translate_commands + utils_commands)) + +commands = get_commands() diff --git a/frappe/commands/docs.py b/frappe/commands/docs.py new file mode 100644 index 0000000000..16242f6c1d --- /dev/null +++ b/frappe/commands/docs.py @@ -0,0 +1,108 @@ +from __future__ import unicode_literals, absolute_import +import click +import os +import frappe +from frappe.commands import pass_context + +@click.command('make-docs') +@pass_context +@click.argument('app') +@click.argument('docs_version') +def make_docs(context, app, docs_version): + "Setup docs in target folder of target app" + from frappe.utils.setup_docs import setup_docs + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + make = setup_docs(app) + make.build(docs_version) + finally: + frappe.destroy() + +@click.command('sync-docs') +@pass_context +@click.argument('app') +def sync_docs(context, app): + "Sync docs from /docs folder into the database (Web Page)" + from frappe.utils.setup_docs import setup_docs + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + make = setup_docs(app) + make.sync_docs() + finally: + frappe.destroy() + + +@click.command('write-docs') +@pass_context +@click.argument('app') +@click.argument('target') +@click.option('--local', default=False, is_flag=True, help='Run app locally') +def write_docs(context, app, target, local=False): + "Setup docs in target folder of target app" + from frappe.utils.setup_docs import setup_docs + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + make = setup_docs(app) + make.make_docs(target, local) + finally: + frappe.destroy() + +@click.command('build-docs') +@pass_context +@click.argument('app') +@click.option('--docs-version', default='current') +@click.option('--target', default=None) +@click.option('--local', default=False, is_flag=True, help='Run app locally') +@click.option('--watch', default=False, is_flag=True, help='Watch for changes and rewrite') +def build_docs(context, app, docs_version="current", target=None, local=False, watch=False): + "Setup docs in target folder of target app" + from frappe.utils import watch as start_watch + if not target: + target = os.path.abspath(os.path.join("..", "docs", app)) + + for site in context.sites: + _build_docs_once(site, app, docs_version, target, local) + + if watch: + def trigger_make(source_path, event_type): + if "/templates/autodoc/" in source_path: + _build_docs_once(site, app, docs_version, target, local) + + elif ("/docs.css" in source_path + or "/docs/" in source_path + or "docs.py" in source_path): + _build_docs_once(site, app, docs_version, target, local, only_content_updated=True) + + apps_path = frappe.get_app_path("frappe", "..", "..") + start_watch(apps_path, handler=trigger_make) + +def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False): + from frappe.utils.setup_docs import setup_docs + + try: + + frappe.init(site=site) + frappe.connect() + make = setup_docs(app) + + if not only_content_updated: + make.build(docs_version) + make.sync_docs() + + make.make_docs(target, local) + + finally: + frappe.destroy() + +commands = [ + build_docs, + make_docs, + sync_docs, + write_docs, +] diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py new file mode 100644 index 0000000000..c6d90ff40f --- /dev/null +++ b/frappe/commands/scheduler.py @@ -0,0 +1,202 @@ +from __future__ import unicode_literals, absolute_import +import click +import json, sys +import frappe +from frappe.utils import cint +from frappe.commands import pass_context, get_site + +def _is_scheduler_enabled(): + enable_scheduler = False + try: + frappe.connect() + enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False + except: + pass + finally: + frappe.db.close() + + return enable_scheduler + +@click.command('trigger-scheduler-event') +@click.argument('event') +@pass_context +def trigger_scheduler_event(context, event): + "Trigger a scheduler event" + import frappe.utils.scheduler + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + frappe.utils.scheduler.trigger(site, event, now=context.force) + finally: + frappe.destroy() + +@click.command('enable-scheduler') +@pass_context +def enable_scheduler(context): + "Enable scheduler" + import frappe.utils.scheduler + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + frappe.utils.scheduler.enable_scheduler() + frappe.db.commit() + print "Enabled for", site + finally: + frappe.destroy() + +@click.command('disable-scheduler') +@pass_context +def disable_scheduler(context): + "Disable scheduler" + import frappe.utils.scheduler + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + frappe.utils.scheduler.disable_scheduler() + frappe.db.commit() + print "Disabled for", site + finally: + frappe.destroy() + + + +@click.command('scheduler') +@click.option('--site', help='site name') +@click.argument('state', type=click.Choice(['pause', 'resume', 'disable', 'enable'])) +@pass_context +def scheduler(context, state, site=None): + from frappe.installer import update_site_config + import frappe.utils.scheduler + + if not site: + site = get_site(context) + + try: + frappe.init(site=site) + + if state == 'pause': + update_site_config('pause_scheduler', 1) + elif state == 'resume': + update_site_config('pause_scheduler', 0) + elif state == 'disable': + frappe.connect() + frappe.utils.scheduler.disable_scheduler() + frappe.db.commit() + elif state == 'enable': + frappe.connect() + frappe.utils.scheduler.enable_scheduler() + frappe.db.commit() + + print 'Scheduler {0}d for site {1}'.format(state, site) + + finally: + frappe.destroy() + + +@click.command('set-maintenance-mode') +@click.option('--site', help='site name') +@click.argument('state', type=click.Choice(['on', 'off'])) +@pass_context +def set_maintenance_mode(context, state, site=None): + from frappe.installer import update_site_config + if not site: + site = get_site(context) + + try: + frappe.init(site=site) + update_site_config('maintenance_mode', 1 if (state == 'on') else 0) + + finally: + frappe.destroy() + + +@click.command('doctor') #Passing context always gets a site and if there is no use site it breaks +@click.option('--site', help='site name') +def doctor(site=None): + "Get diagnostic info about background workers" + from frappe.utils.doctor import doctor as _doctor + return _doctor(site=site) + +@click.command('show-pending-jobs') +@click.option('--site', help='site name') +@pass_context +def show_pending_jobs(context, site=None): + "Get diagnostic info about background jobs" + if not site: + site = get_site(context) + + from frappe.utils.doctor import pending_jobs as _pending_jobs + return _pending_jobs(site=site) + +@click.command('purge-jobs') +@click.option('--site', help='site name') +@click.option('--queue', default=None, help='one of "low", "default", "high') +@click.option('--event', default=None, help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"') +def purge_jobs(site=None, queue=None, event=None): + "Purge any pending periodic tasks, if event option is not given, it will purge everything for the site" + from frappe.utils.doctor import purge_pending_jobs + frappe.init(site or '') + count = purge_pending_jobs(event=event, site=site, queue=queue) + print "Purged {} jobs".format(count) + +@click.command('dump-queue-status') +def dump_queue_status(): + "Dump detailed diagnostic infomation for task queues in JSON format" + frappe.init('') + from frappe.utils.doctor import dump_queue_status as _dump_queue_status, inspect_queue + print json.dumps(_dump_queue_status(), indent=1) + inspect_queue() + + +@click.command('schedule') +def start_scheduler(): + from frappe.utils.scheduler import start_scheduler + start_scheduler() + +@click.command('worker') +@click.option('--queue', type=str) +def start_worker(queue): + from frappe.utils.background_jobs import start_worker + start_worker(queue) + +@click.command('ready-for-migration') +@click.option('--site', help='site name') +@pass_context +def ready_for_migration(context, site=None): + from frappe.utils.doctor import get_pending_jobs + + if not site: + site = get_site(context) + + try: + frappe.init(site=site) + pending_jobs = get_pending_jobs(site=site) + + if pending_jobs: + print 'NOT READY for migration: site {0} has pending background jobs'.format(site) + sys.exit(1) + + else: + print 'READY for migration: site {0} does not have any background jobs'.format(site) + return 0 + + finally: + frappe.destroy() + +commands = [ + disable_scheduler, + doctor, + dump_queue_status, + enable_scheduler, + purge_jobs, + ready_for_migration, + scheduler, + set_maintenance_mode, + show_pending_jobs, + start_scheduler, + start_worker, + trigger_scheduler_event, +] diff --git a/frappe/commands/site.py b/frappe/commands/site.py new file mode 100644 index 0000000000..b53a6299b3 --- /dev/null +++ b/frappe/commands/site.py @@ -0,0 +1,348 @@ +from __future__ import unicode_literals, absolute_import +import click +import hashlib, os +import frappe +from frappe.commands import pass_context, get_site +from frappe.commands.scheduler import _is_scheduler_enabled + +@click.command('new-site') +@click.argument('site') +@click.option('--db-name', help='Database name') +@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB') +@click.option('--mariadb-root-password', help='Root password for MariaDB') +@click.option('--admin-password', help='Administrator password for new site', default=None) +@click.option('--verbose', is_flag=True, default=False, help='Verbose') +@click.option('--force', help='Force restore if site/database already exists', is_flag=True, default=False) +@click.option('--source_sql', help='Initiate database with a SQL file') +@click.option('--install-app', multiple=True, help='Install app after installation') +def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None, verbose=False, install_apps=None, source_sql=None, force=None, install_app=None, db_name=None): + "Create a new site" + if not db_name: + db_name = hashlib.sha1(site).hexdigest()[:10] + + frappe.init(site=site, new_site=True) + _new_site(db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force) + if len(frappe.utils.get_sites()) == 1: + use(site) + +def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None, verbose=False, install_apps=None, source_sql=None,force=False, reinstall=False): + "Install a new Frappe site" + from frappe.installer import install_db, make_site_dirs + from frappe.installer import install_app as _install_app + import frappe.utils.scheduler + + frappe.init(site=site) + + try: + # enable scheduler post install? + enable_scheduler = _is_scheduler_enabled() + except: + enable_scheduler = False + + install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name, admin_password=admin_password, verbose=verbose, source_sql=source_sql,force=force, reinstall=reinstall) + make_site_dirs() + _install_app("frappe", verbose=verbose, set_as_patched=not source_sql) + + if frappe.conf.get("install_apps"): + for app in frappe.conf.install_apps: + _install_app(app, verbose=verbose, set_as_patched=not source_sql) + + if install_apps: + for app in install_apps: + _install_app(app, verbose=verbose, set_as_patched=not source_sql) + + frappe.utils.scheduler.toggle_scheduler(enable_scheduler) + frappe.db.commit() + + scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled" + print "*** Scheduler is", scheduler_status, "***" + frappe.destroy() + +@click.command('restore') +@click.argument('sql-file-path') +@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB') +@click.option('--mariadb-root-password', help='Root password for MariaDB') +@click.option('--db-name', help='Database name for site in case it is a new one') +@click.option('--admin-password', help='Administrator password for new site') +@click.option('--install-app', multiple=True, help='Install app after installation') +@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file') +@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file') +@pass_context +def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): + "Restore site database from an sql file" + from frappe.installer import extract_sql_gzip, extract_tar_files + # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file + if sql_file_path.endswith('sql.gz'): + sql_file_path = extract_sql_gzip(os.path.abspath(sql_file_path)) + + site = get_site(context) + frappe.init(site=site) + db_name = db_name or frappe.conf.db_name or hashlib.sha1(site).hexdigest()[:10] + _new_site(db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=context.verbose, install_apps=install_app, source_sql=sql_file_path, force=context.force) + + # Extract public and/or private files to the restored site, if user has given the path + if with_public_files: + extract_tar_files(site, with_public_files, 'public') + + if with_private_files: + extract_tar_files(site, with_private_files, 'private') + +@click.command('reinstall') +@pass_context +def reinstall(context): + "Reinstall site ie. wipe all data and start over" + site = get_site(context) + try: + frappe.init(site=site) + frappe.connect() + frappe.clear_cache() + installed = frappe.get_installed_apps() + frappe.clear_cache() + except Exception: + installed = [] + finally: + if frappe.db: + frappe.db.close() + frappe.destroy() + + frappe.init(site=site) + _new_site(frappe.conf.db_name, site, verbose=context.verbose, force=True, reinstall=True, install_apps=installed) + +@click.command('install-app') +@click.argument('app') +@pass_context +def install_app(context, app): + "Install a new app to site" + from frappe.installer import install_app as _install_app + for site in context.sites: + frappe.init(site=site) + frappe.connect() + try: + _install_app(app, verbose=context.verbose) + finally: + frappe.destroy() + +@click.command('list-apps') +@pass_context +def list_apps(context): + "List apps in site" + site = get_site(context) + frappe.init(site=site) + frappe.connect() + print "\n".join(frappe.get_installed_apps()) + frappe.destroy() + +@click.command('add-system-manager') +@click.argument('email') +@click.option('--first-name') +@click.option('--last-name') +@pass_context +def add_system_manager(context, email, first_name, last_name): + "Add a new system manager to a site" + import frappe.utils.user + for site in context.sites: + frappe.connect(site=site) + try: + frappe.utils.user.add_system_manager(email, first_name, last_name) + frappe.db.commit() + finally: + frappe.destroy() + +@click.command('migrate') +@click.option('--rebuild-website', help="Rebuild webpages after migration") +@pass_context +def migrate(context, rebuild_website=False): + "Run patches, sync schema and rebuild files/translations" + from frappe.migrate import migrate + + for site in context.sites: + print 'Migrating', site + frappe.init(site=site) + frappe.connect() + try: + migrate(context.verbose, rebuild_website=rebuild_website) + finally: + frappe.destroy() + +@click.command('run-patch') +@click.argument('module') +@pass_context +def run_patch(context, module): + "Run a particular patch" + import frappe.modules.patch_handler + for site in context.sites: + frappe.init(site=site) + try: + frappe.connect() + frappe.modules.patch_handler.run_single(module, force=context.force) + finally: + frappe.destroy() + +@click.command('reload-doc') +@click.argument('module') +@click.argument('doctype') +@click.argument('docname') +@pass_context +def reload_doc(context, module, doctype, docname): + "Reload schema for a DocType" + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + frappe.reload_doc(module, doctype, docname, force=context.force) + frappe.db.commit() + finally: + frappe.destroy() + + +@click.command('use') +@click.argument('site') +def _use(site, sites_path='.'): + "Set a default site" + use(site, sites_path=sites_path) + +def use(site, sites_path='.'): + with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile: + sitefile.write(site) + +@click.command('backup') +@click.option('--with-files', default=False, is_flag=True, help="Take backup with files") +@pass_context +def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, + backup_path_private_files=None, quiet=False): + "Backup" + from frappe.utils.backups import scheduled_backup + verbose = context.verbose + for site in context.sites: + frappe.init(site=site) + frappe.connect() + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) + if verbose: + from frappe.utils import now + print "database backup taken -", odb.backup_path_db, "- on", now() + if with_files: + print "files backup taken -", odb.backup_path_files, "- on", now() + print "private files backup taken -", odb.backup_path_private_files, "- on", now() + + frappe.destroy() + +@click.command('remove-from-installed-apps') +@click.argument('app') +@pass_context +def remove_from_installed_apps(context, app): + "Remove app from site's installed-apps list" + from frappe.installer import remove_from_installed_apps + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + remove_from_installed_apps(app) + finally: + frappe.destroy() + +@click.command('uninstall-app') +@click.argument('app') +@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False) +@pass_context +def uninstall(context, app, dry_run=False): + "Remove app and linked modules from site" + from frappe.installer import remove_app + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + remove_app(app, dry_run) + finally: + frappe.destroy() + + +@click.command('drop-site') +@click.argument('site') +@click.option('--root-login', default='root') +@click.option('--root-password') +@click.option('--archived-sites-path') +def drop_site(site, root_login='root', root_password=None, archived_sites_path=None): + "Remove site from database and filesystem" + from frappe.installer import get_current_host, make_connection + from frappe.model.db_schema import DbManager + from frappe.utils.backups import scheduled_backup + + frappe.init(site=site) + frappe.connect() + scheduled_backup(ignore_files=False, force=True) + + db_name = frappe.local.conf.db_name + frappe.local.db = make_connection(root_login, root_password) + dbman = DbManager(frappe.local.db) + dbman.delete_user(db_name, get_current_host()) + dbman.drop_database(db_name) + + if not archived_sites_path: + archived_sites_path = os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived_sites') + + if not os.path.exists(archived_sites_path): + os.mkdir(archived_sites_path) + + move(archived_sites_path, site) + +def move(dest_dir, site): + import os + if not os.path.isdir(dest_dir): + raise Exception, "destination is not a directory or does not exist" + + frappe.init(site) + old_path = frappe.utils.get_site_path() + new_path = os.path.join(dest_dir, site) + + # check if site dump of same name already exists + site_dump_exists = True + count = 0 + while site_dump_exists: + final_new_path = new_path + (count and str(count) or "") + site_dump_exists = os.path.exists(final_new_path) + count = int(count or 0) + 1 + + os.rename(old_path, final_new_path) + frappe.destroy() + return final_new_path + + +@click.command('set-admin-password') +@click.argument('admin-password') +@pass_context +def set_admin_password(context, admin_password): + "Set Administrator password for a site" + import getpass + + for site in context.sites: + try: + frappe.init(site=site) + + while not admin_password: + admin_password = getpass.getpass("Administrator's password for {0}: ".format(site)) + + frappe.connect() + frappe.db.sql("""update __Auth set `password`=password(%s) + where user='Administrator'""", (admin_password,)) + frappe.db.commit() + admin_password = None + finally: + frappe.destroy() + +commands = [ + add_system_manager, + backup, + drop_site, + install_app, + list_apps, + migrate, + new_site, + reinstall, + reload_doc, + remove_from_installed_apps, + restore, + run_patch, + set_admin_password, + uninstall, + _use, +] diff --git a/frappe/commands/translate.py b/frappe/commands/translate.py new file mode 100644 index 0000000000..0dd2546aed --- /dev/null +++ b/frappe/commands/translate.py @@ -0,0 +1,91 @@ +from __future__ import unicode_literals, absolute_import +import click +import frappe +from frappe.commands import pass_context, get_site + +# translation +@click.command('build-message-files') +@pass_context +def build_message_files(context): + "Build message files for translation" + import frappe.translate + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + frappe.translate.rebuild_all_translation_files() + finally: + frappe.destroy() + +@click.command('new-language') #, help="Create lang-code.csv for given app") +@pass_context +@click.argument('lang_code') #, help="Language code eg. en") +@click.argument('app') #, help="App name eg. frappe") +def new_language(context, lang_code, app): + """Create lang-code.csv for given app""" + import frappe.translate + + if not context['sites']: + raise Exception('--site is required') + + # init site + frappe.connect(site=context['sites'][0]) + frappe.translate.write_translations_file(app, lang_code) + + print "File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(app=app, lang_code=lang_code) + print "You will need to add the language in frappe/data/languages.txt, if you haven't done it already." + +@click.command('get-untranslated') +@click.argument('lang') +@click.argument('untranslated_file') +@click.option('--all', default=False, is_flag=True, help='Get all message strings') +@pass_context +def get_untranslated(context, lang, untranslated_file, all=None): + "Get untranslated strings for language" + import frappe.translate + site = get_site(context) + try: + frappe.init(site=site) + frappe.connect() + frappe.translate.get_untranslated(lang, untranslated_file, get_all=all) + finally: + frappe.destroy() + +@click.command('update-translations') +@click.argument('lang') +@click.argument('untranslated_file') +@click.argument('translated-file') +@pass_context +def update_translations(context, lang, untranslated_file, translated_file): + "Update translated strings" + import frappe.translate + site = get_site(context) + try: + frappe.init(site=site) + frappe.connect() + frappe.translate.update_translations(lang, untranslated_file, translated_file) + finally: + frappe.destroy() + +@click.command('import-translations') +@click.argument('lang') +@click.argument('path') +@pass_context +def import_translations(context, lang, path): + "Update translated strings" + import frappe.translate + site = get_site(context) + try: + frappe.init(site=site) + frappe.connect() + frappe.translate.import_translations(lang, path) + finally: + frappe.destroy() + +commands = [ + build_message_files, + get_untranslated, + import_translations, + new_language, + update_translations, +] diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py new file mode 100644 index 0000000000..894f51a550 --- /dev/null +++ b/frappe/commands/utils.py @@ -0,0 +1,417 @@ +from __future__ import unicode_literals, absolute_import +import click +import json, os, sys +from distutils.spawn import find_executable +import frappe +from frappe.commands import pass_context, get_site + +@click.command('build') +@click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking') +@click.option('--verbose', is_flag=True, default=False, help='Verbose') +def build(make_copy=False, verbose=False): + "Minify + concatenate JS and CSS files, build translations" + import frappe.build + import frappe + frappe.init('') + frappe.build.bundle(False, make_copy=make_copy, verbose=verbose) + +@click.command('watch') +def watch(): + "Watch and concatenate JS and CSS files as and when they change" + import frappe.build + frappe.init('') + frappe.build.watch(True) + +@click.command('clear-cache') +@pass_context +def clear_cache(context): + "Clear cache, doctype cache and defaults" + import frappe.sessions + import frappe.website.render + from frappe.desk.notifications import clear_notifications + for site in context.sites: + try: + frappe.connect(site) + frappe.clear_cache() + clear_notifications() + frappe.website.render.clear_cache() + finally: + frappe.destroy() + +@click.command('clear-website-cache') +@pass_context +def clear_website_cache(context): + "Clear website cache" + import frappe.website.render + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + frappe.website.render.clear_cache() + finally: + frappe.destroy() + +@click.command('destroy-all-sessions') +@pass_context +def destroy_all_sessions(context): + "Clear sessions of all users (logs them out)" + import frappe.sessions + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + frappe.sessions.clear_all_sessions() + frappe.db.commit() + finally: + frappe.destroy() + +@click.command('sync-www') +@click.option('--force', help='Rebuild all pages', is_flag=True, default=False) +@pass_context +def sync_www(context, force=False): + "Sync files from static pages from www directory to Web Pages" + from frappe.website import statics + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + statics.sync_statics(rebuild=force) + frappe.db.commit() + finally: + frappe.destroy() + +@click.command('build-website') +@pass_context +def build_website(context): + "Sync statics and clear cache" + from frappe.website import render, statics + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + render.clear_cache() + statics.sync(verbose=context.verbose).start(rebuild=True) + frappe.db.commit() + finally: + frappe.destroy() + +@click.command('reset-perms') +@pass_context +def reset_perms(context): + "Reset permissions for all doctypes" + from frappe.permissions import reset_perms + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + for d in frappe.db.sql_list("""select name from `tabDocType` + where istable=0 and custom=0"""): + frappe.clear_cache(doctype=d) + reset_perms(d) + finally: + frappe.destroy() + +@click.command('execute') +@click.argument('method') +@click.option('--args') +@click.option('--kwargs') +@pass_context +def execute(context, method, args=None, kwargs=None): + "Execute a function" + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + + if args: + try: + args = eval(args) + except NameError: + args = [args] + else: + args = () + + if kwargs: + kwargs = eval(args) + else: + kwargs = {} + + ret = frappe.get_attr(method)(*args, **kwargs) + + if frappe.db: + frappe.db.commit() + finally: + frappe.destroy() + if ret: + print json.dumps(ret) + + +@click.command('export-doc') +@click.argument('doctype') +@click.argument('docname') +@pass_context +def export_doc(context, doctype, docname): + "Export a single document to csv" + import frappe.modules + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + frappe.modules.export_doc(doctype, docname) + finally: + frappe.destroy() + +@click.command('export-json') +@click.argument('doctype') +@click.argument('name') +@click.argument('path') +@pass_context +def export_json(context, doctype, name, path): + "Export doclist as json to the given path, use '-' as name for Singles." + from frappe.core.page.data_import_tool import data_import_tool + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + data_import_tool.export_json(doctype, path, name=name) + finally: + frappe.destroy() + +@click.command('export-csv') +@click.argument('doctype') +@click.argument('path') +@pass_context +def export_csv(context, doctype, path): + "Export data import template for DocType" + from frappe.core.page.data_import_tool import data_import_tool + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + data_import_tool.export_csv(doctype, path) + finally: + frappe.destroy() + +@click.command('export-fixtures') +@pass_context +def export_fixtures(context): + "Export fixtures" + from frappe.utils.fixtures import export_fixtures + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + export_fixtures() + finally: + frappe.destroy() + +@click.command('import-doc') +@click.argument('path') +@pass_context +def import_doc(context, path, force=False): + "Import (insert/update) doclist. If the argument is a directory, all files ending with .json are imported" + from frappe.core.page.data_import_tool import data_import_tool + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + data_import_tool.import_doc(path, overwrite=context.force) + finally: + frappe.destroy() + +@click.command('import-csv') +@click.argument('path') +@click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records') +@click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it') +@click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode') +@pass_context +def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False): + "Import CSV using data import tool" + from frappe.core.page.data_import_tool import importer + from frappe.utils.csvutils import read_csv_content + site = get_site(context) + + with open(path, 'r') as csvfile: + content = read_csv_content(csvfile.read()) + + frappe.init(site=site) + frappe.connect() + + try: + importer.upload(content, submit_after_import=submit_after_import, + ignore_encoding_errors=ignore_encoding_errors, overwrite=not only_insert, + via_console=True) + frappe.db.commit() + except Exception: + print frappe.get_traceback() + + frappe.destroy() + +@click.command('bulk-rename') +@click.argument('doctype') +@click.argument('path') +@pass_context +def _bulk_rename(context, doctype, path): + "Rename multiple records via CSV file" + from frappe.model.rename_doc import bulk_rename + from frappe.utils.csvutils import read_csv_content + + site = get_site(context) + + with open(path, 'r') as csvfile: + rows = read_csv_content(csvfile.read()) + + frappe.init(site=site) + frappe.connect() + + bulk_rename(doctype, rows, via_console = True) + + frappe.destroy() + +@click.command('mysql') +@pass_context +def mysql(context): + "Start Mariadb console for a site" + site = get_site(context) + frappe.init(site=site) + msq = find_executable('mysql') + os.execv(msq, [msq, '-u', frappe.conf.db_name, '-p'+frappe.conf.db_password, frappe.conf.db_name, '-h', frappe.conf.db_host or "localhost", "-A"]) + +@click.command('console') +@pass_context +def console(context): + "Start ipython console for a site" + site = get_site(context) + frappe.init(site=site) + frappe.connect() + frappe.local.lang = frappe.db.get_default("lang") + import IPython + IPython.embed() + +@click.command('run-tests') +@click.option('--app', help="For App") +@click.option('--doctype', help="For DocType") +@click.option('--test', multiple=True, help="Specific test") +@click.option('--driver', help="For Travis") +@click.option('--module', help="Run tests in a module") +@click.option('--profile', is_flag=True, default=False) +@pass_context +def run_tests(context, app=None, module=None, doctype=None, test=(), driver=None, profile=False): + "Run tests" + import frappe.test_runner + from frappe.utils import sel + tests = test + + site = get_site(context) + frappe.init(site=site) + + if frappe.conf.run_selenium_tests and False: + sel.start(context.verbose, driver) + + try: + ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, + force=context.force, profile=profile) + if len(ret.failures) == 0 and len(ret.errors) == 0: + ret = 0 + finally: + pass + if frappe.conf.run_selenium_tests: + sel.close() + + sys.exit(ret) + +@click.command('serve') +@click.option('--port', default=8000) +@click.option('--profile', is_flag=True, default=False) +@pass_context +def serve(context, port=None, profile=False, sites_path='.', site=None): + "Start development web server" + import frappe.app + + if not context.sites: + site = None + else: + site = context.sites[0] + + frappe.app.serve(port=port, profile=profile, site=site, sites_path='.') + +@click.command('request') +@click.argument('args') +@pass_context +def request(context, args): + "Run a request as an admin" + import frappe.handler + import frappe.api + for site in context.sites: + try: + frappe.init(site=site) + frappe.connect() + if "?" in args: + frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")]) + else: + frappe.local.form_dict = frappe._dict() + + if args.startswith("/api/method"): + frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1] + + frappe.handler.execute_cmd(frappe.form_dict.cmd) + + print frappe.response + finally: + frappe.destroy() + +@click.command('make-app') +@click.argument('destination') +@click.argument('app_name') +def make_app(destination, app_name): + "Creates a boilerplate app" + from frappe.utils.boilerplate import make_boilerplate + make_boilerplate(destination, app_name) + +@click.command('set-config') +@click.argument('key') +@click.argument('value') +@pass_context +def set_config(context, key, value): + "Insert/Update a value in site_config.json" + from frappe.installer import update_site_config + for site in context.sites: + frappe.init(site=site) + update_site_config(key, value) + frappe.destroy() + +@click.command('version') +def get_version(): + "Show the versions of all the installed apps" + frappe.init('') + for m in sorted(frappe.get_all_apps()): + module = frappe.get_module(m) + if hasattr(module, "__version__"): + print "{0} {1}".format(m, module.__version__) + +commands = [ + build, + build_website, + clear_cache, + clear_website_cache, + console, + destroy_all_sessions, + execute, + export_csv, + export_doc, + export_fixtures, + export_json, + get_version, + import_csv, + import_doc, + make_app, + mysql, + request, + reset_perms, + run_tests, + serve, + set_config, + sync_www, + watch, + _bulk_rename, +]