@@ -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() |
@@ -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, | |||
] |
@@ -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, | |||
] |
@@ -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, | |||
] |
@@ -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, | |||
] |
@@ -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, | |||
] |