From 343330fe6b521f63339ff18d68dd623e9baa0418 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 2 May 2014 12:14:04 +0530 Subject: [PATCH] hooks.txt -> hooks.py --- frappe/__init__.py | 30 +++-- frappe/cli.py | 18 +-- frappe/hooks.py | 73 +++++++++++ frappe/hooks.txt | 55 --------- frappe/model/db_query.py | 5 +- frappe/model/document.py | 5 +- frappe/permissions.py | 2 +- frappe/public/css/desk.css | 6 + frappe/public/js/frappe/dom.js | 6 +- frappe/public/js/frappe/form/grid.js | 6 +- frappe/public/js/frappe/request.js | 2 + frappe/public/js/frappe/views/moduleview.js | 42 +++---- frappe/test_runner.py | 2 +- frappe/tests/test_client_login.py | 13 +- frappe/tests/test_hooks.py | 16 +++ frappe/utils/boilerplate.py | 130 +++++++++++++++----- frappe/utils/scheduler.py | 57 ++++----- frappe/utils/sel.py | 62 ++++++++-- 18 files changed, 351 insertions(+), 179 deletions(-) create mode 100644 frappe/hooks.py delete mode 100644 frappe/hooks.txt create mode 100644 frappe/tests/test_hooks.py diff --git a/frappe/__init__.py b/frappe/__init__.py index 934dcb6f1d..8dc2472fe4 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -391,24 +391,38 @@ def get_installed_apps(): installed = json.loads(db.get_global("installed_apps") or "[]") return installed -def get_hooks(hook=None, app_name=None): +def get_hooks(hook=None, default=None, app_name=None): def load_app_hooks(app_name=None): hooks = {} for app in [app_name] if app_name else get_installed_apps(): - if app=="webnotes": app="frappe" - for item in get_file_items(get_pymodule_path(app, "hooks.txt")): - key, value = item.split("=", 1) - key, value = key.strip(), value.strip() - hooks.setdefault(key, []) - hooks[key].append(value) + app = "frappe" if app=="webnotes" else app + app_hooks = get_module(app + ".hooks") + for key in dir(app_hooks): + if not key.startswith("_"): + append_hook(hooks, key, getattr(app_hooks, key)) return hooks + + def append_hook(target, key, value): + if isinstance(value, dict): + target.setdefault(key, {}) + for inkey in value: + append_hook(target[key], inkey, value[inkey]) + else: + append_to_list(target, key, value) + + def append_to_list(target, key, value): + target.setdefault(key, []) + if not isinstance(value, list): + value = [value] + target[key].extend(value) + if app_name: hooks = _dict(load_app_hooks(app_name)) else: hooks = _dict(cache().get_value("app_hooks", load_app_hooks)) if hook: - return hooks.get(hook) or [] + return hooks.get(hook) or default or [] else: return hooks diff --git a/frappe/cli.py b/frappe/cli.py index d16eac1f49..476e0c3a08 100755 --- a/frappe/cli.py +++ b/frappe/cli.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import os -import time import subprocess import frappe @@ -288,6 +287,9 @@ def install(db_name, root_login="root", root_password=None, source_sql=None, admin_password = admin_password, verbose=verbose, force=force, site_config=site_config, 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) frappe.destroy() @cmd @@ -707,16 +709,8 @@ def run_tests(app=None, module=None, doctype=None, verbose=False, tests=(), with import frappe.test_runner from frappe.utils import sel - frappe.local.localhost = "http://localhost:8888" if not without_serve: - pipe = subprocess.Popen(["frappe", frappe.local.site, "--serve", "--port", "8888"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - while not pipe.stderr.readline(): - time.sleep(0.5) - if verbose: - print "Test server started" - - sel.start(verbose) + sel.start(verbose) ret = 1 try: @@ -726,9 +720,7 @@ def run_tests(app=None, module=None, doctype=None, verbose=False, tests=(), with ret = 0 finally: if not without_serve: - pipe.terminate() - - sel.close() + sel.close() return ret diff --git a/frappe/hooks.py b/frappe/hooks.py new file mode 100644 index 0000000000..5b551d841d --- /dev/null +++ b/frappe/hooks.py @@ -0,0 +1,73 @@ +app_name = "frappe" +app_title = "Frappe Framework" +app_publisher = "Web Notes Technologies Pvt. Ltd. and Contributors" +app_description = "Full Stack Web Application Framwork in Python" +app_icon = "assets/frappe/images/frappe.svg" +app_version = "4.0.0-wip" +app_color = "#3498db" + +before_install = "frappe.utils.install.before_install" +after_install = "frappe.utils.install.after_install" + +# website +app_include_js = "assets/js/frappe.min.js" +app_include_css = [ + "assets/frappe/css/splash.css", + "assets/css/frappe.css" + ] +web_include_js = [ + "assets/js/frappe-web.min.js", + "website_script.js" + ] +web_include_css = [ + "assets/css/frappe-web.css", + "style_settings.css" + ] + +website_clear_cache = "frappe.templates.generators.website_group.clear_cache" + +write_file_keys = ["file_url", "file_name"] + +notification_config = "frappe.core.notifications.get_notification_config" + +before_tests = "frappe.utils.install.before_tests" + +# permissions + +permission_query_conditions = { + "Event": "frappe.core.doctype.event.event.get_permission_query_conditions", + "ToDo": "frappe.core.doctype.todo.todo.get_permission_query_conditions" + } + +has_permission = { + "Event": "frappe.core.doctype.event.event.has_permission", + "ToDo": "frappe.core.doctype.todo.todo.has_permission" + } + +# bean + +doc_events = { + "*": { + "on_update": "frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications", + "on_cancel": "frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications", + "on_trash": "frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications" + }, + "User Vote": { + "after_insert": "frappe.templates.generators.website_group.clear_cache_on_doc_event" + }, + "Website Route Permission": { + "on_update": "frappe.templates.generators.website_group.clear_cache_on_doc_event" + } + } + +scheduler_events = { + "all": ["frappe.utils.email_lib.bulk.flush"], + "daily": [ + "frappe.utils.email_lib.bulk.clear_outbox", + "frappe.core.doctype.notification_count.notification_count.delete_event_notification_count", + "frappe.core.doctype.event.event.send_event_digest", + ], + "hourly": [ + "frappe.templates.generators.website_group.clear_event_cache" + ] +} diff --git a/frappe/hooks.txt b/frappe/hooks.txt deleted file mode 100644 index 404a996818..0000000000 --- a/frappe/hooks.txt +++ /dev/null @@ -1,55 +0,0 @@ -app_name = frappe -app_title = Frappe Framework -app_publisher = Web Notes Technologies Pvt. Ltd. and Contributors -app_description = Full Stack Web Application Framwork in Python -app_icon = assets/frappe/images/frappe.svg -app_version = 4.0.0-wip -app_color = #3498db - -before_install = frappe.utils.install.before_install -after_install = frappe.utils.install.after_install - -# website -app_include_js = assets/js/frappe.min.js -app_include_css = assets/frappe/css/splash.css -app_include_css = assets/css/frappe.css -web_include_js = assets/js/frappe-web.min.js -web_include_css = assets/css/frappe-web.css -web_include_js = website_script.js -web_include_css = style_settings.css - -website_clear_cache = frappe.templates.generators.website_group.clear_cache - -get_desktop_icons = frappe.manage.get_desktop_icons -notification_config = frappe.core.notifications.get_notification_config - -scheduler_event:all = frappe.utils.email_lib.bulk.flush -scheduler_event:daily = frappe.utils.email_lib.bulk.clear_outbox -scheduler_event:daily = frappe.core.doctype.notification_count.notification_count.delete_event_notification_count -scheduler_event:daily = frappe.core.doctype.event.event.send_event_digest -scheduler_event:hourly = frappe.templates.generators.website_group.clear_event_cache - -before_tests = frappe.utils.install.before_tests - -# TODO -# on_session_creation = frappe.auth.notify_administrator_login - -# permissions - -permission_query_conditions:Event = frappe.core.doctype.event.event.get_permission_query_conditions -has_permission:Event = frappe.core.doctype.event.event.has_permission - -permission_query_conditions:ToDo = frappe.core.doctype.todo.todo.get_permission_query_conditions -has_permission:ToDo = frappe.core.doctype.todo.todo.has_permission - -# bean - -doc_event:User Vote:after_insert = frappe.templates.generators.website_group.clear_cache_on_doc_event -doc_event:Website Route Permission:on_update = frappe.templates.generators.website_group.clear_cache_on_doc_event - -doc_event:*:on_update = frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications -doc_event:*:on_cancel = frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications -doc_event:*:on_trash = frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications - -write_file_keys = file_url -write_file_keys = file_name diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 2a85139de1..905d8d53a3 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -21,6 +21,9 @@ class DatabaseQuery(object): def execute(self, query=None, filters=None, fields=None, docstatus=None, group_by=None, order_by=None, limit_start=0, limit_page_length=20, as_list=False, with_childnames=False, debug=False, ignore_permissions=False): + if not frappe.has_permission(self.doctype, "read"): + raise frappe.PermissionError + if fields: self.fields = fields self.filters = filters or [] @@ -254,7 +257,7 @@ class DatabaseQuery(object): return conditions def get_permission_query_conditions(self): - condition_methods = frappe.get_hooks("permission_query_conditions:" + self.doctype) + condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, []) if condition_methods: conditions = [] for method in condition_methods: diff --git a/frappe/model/document.py b/frappe/model/document.py index 966d543aaf..2be654b340 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -423,8 +423,9 @@ class Document(BaseDocument): def composer(self, *args, **kwargs): hooks = [] method = f.__name__ - for handler in frappe.get_hooks("doc_event:" + self.doctype + ":" + method) \ - + frappe.get_hooks("doc_event:*:" + method): + doc_events = frappe.get_hooks("doc_events") + for handler in doc_events.get(self.doctype, {}).get(method, []) \ + + doc_events.get("*", {}).get(method, []): hooks.append(frappe.get_attr(handler)) composed = compose(f, *hooks) diff --git a/frappe/permissions.py b/frappe/permissions.py index 4dfa1533f0..7b5ee026fd 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -116,7 +116,7 @@ def has_controller_permissions(doc): else: doc = frappe.get_doc(doc.doctype, doc.name) - for method in frappe.get_hooks("has_permission:" + doc.doctype): + for method in frappe.get_hooks("has_permission").get(doc.doctype, []): if not frappe.call(frappe.get_attr(method), doc=doc): return False diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index ac26ca2065..0ed2b89c01 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -246,6 +246,12 @@ ul.linked-with-list li { /* form grid */ +.form-grid { + border: 2px solid #c7c7c7; + margin-bottom: 15px; + border-radius: 3px; +} + .grid-heading-row { padding: 8px; border-bottom: 1px solid #dddddd; diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index b8c2939d2c..180fabf93e 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -127,15 +127,15 @@ var pending_req = 0 frappe.set_loading = function() { pending_req++; //$('#spinner').css('visibility', 'visible'); - $('body').css('cursor', 'progress'); + $('body').css('cursor', 'progress').attr("data-ajax-state", "running"); NProgress.start(); + $("body"); } frappe.done_loading = function() { pending_req--; if(!pending_req){ - $('body').css('cursor', 'default'); - //$('#spinner').css('visibility', 'hidden'); + $('body').css('cursor', 'default').attr("data-ajax-state", "complete"); NProgress.done(); } else { NProgress.inc(); diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index e445951a28..580db0ca24 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -11,7 +11,7 @@ frappe.ui.form.Grid = Class.extend({ var me = this; this.wrapper = $('
\ -
\ +
\
\
\
\ @@ -22,7 +22,9 @@ frappe.ui.form.Grid = Class.extend({
\
\
\ -
').appendTo(this.parent); + ') + .appendTo(this.parent) + .attr("data-fieldname", this.df.fieldname); $(this.wrapper).find(".grid-add-row").click(function() { me.add_new_row(null, null, true); diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index cdb792da90..3197f319ec 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -136,9 +136,11 @@ frappe.request.prepare = function(opts) { console.log(opts) throw "Incomplete Request"; } + } frappe.request.cleanup = function(opts, r) { + // stop button indicator if(opts.btn) $(opts.btn).done_working(); diff --git a/frappe/public/js/frappe/views/moduleview.js b/frappe/public/js/frappe/views/moduleview.js index e4f78a0b9a..21891ce916 100644 --- a/frappe/public/js/frappe/views/moduleview.js +++ b/frappe/public/js/frappe/views/moduleview.js @@ -194,7 +194,7 @@ frappe.views.moduleview.ModuleView = Class.extend({ $list_item = $($r('
  • \ \
    %(description)s
    \ \
  • ', item)).appendTo($list); @@ -205,32 +205,32 @@ frappe.views.moduleview.ModuleView = Class.extend({ $list_item.find(".list-item-name").removeClass("col-sm-6").addClass("col-sm-12"); } - $list_item.find("a") - .on("click", function() { - if(item.route) { - frappe.set_route(item.route); - } else if(item.onclick) { + if(item.onclick) { + $list_item.find("a") + .on("click", function() { var fn = eval(item.onclick); if(typeof(fn)==="function") { fn(); } - } else if(item.type==="doctype") { - frappe.set_route("List", item.name) - } - else if(item.type==="page") { - frappe.set_route(item.route || item.link || item.name); - } - else if(item.type==="report") { - if(item.is_query_report) { - frappe.set_route("query-report", item.name); - } else { - frappe.set_route("Report", item.doctype, item.name); - } + }); + } else { + var route = item.route; + if(item.type==="doctype") { + route = "List/" + encodeURIComponent(item.name); + } else if(item.type==="page") { + route = item.route || item.link || item.name; + } else if(item.type==="report") { + if(item.is_query_report) { + route = "query-report/" + encodeURIComponent(item.name); } else { - return; + route = "Report/" + encodeURIComponent(item.doctype) + "/" + encodeURIComponent(item.name); } - return false; - }); + } + + $list_item.find("a") + .attr("href", "#" + route) + } + var show_count = (item.type==="doctype" || (item.type==="page" && item.doctype)) && !item.hide_count if(show_count) { diff --git a/frappe/test_runner.py b/frappe/test_runner.py index f54e0777cd..128961600a 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -25,7 +25,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=()): if verbose: print 'Running "before_tests" hooks' - for fn in frappe.get_hooks("before_tests", app): + for fn in frappe.get_hooks("before_tests", app_name=app): frappe.get_attr(fn)() if doctype: diff --git a/frappe/tests/test_client_login.py b/frappe/tests/test_client_login.py index a432888283..b26359e25e 100644 --- a/frappe/tests/test_client_login.py +++ b/frappe/tests/test_client_login.py @@ -15,6 +15,15 @@ class TestLogin(unittest.TestCase): sel.module("ToDo") sel.find('.appframe-iconbar .icon-plus')[0].click() sel.wait_for_page("Form/ToDo") - sel.set_field_input("description", "test description") + sel.set_field("description", "test description") sel.primary_action() - sel.wait_for_state("clean") + self.assertTrue(sel.wait_for_state("clean")) + + # def test_material_request(self): + # sel.new_doc("Stock", "Material Request") + # sel.add_child("indent_details") + # sel.set_field("item_code", "_Test Item") + # sel.set_field("schedule_date", "10-10-2014") + # sel.primary_action() + # sel.wait_for_state("clean") + diff --git a/frappe/tests/test_hooks.py b/frappe/tests/test_hooks.py new file mode 100644 index 0000000000..554af211a9 --- /dev/null +++ b/frappe/tests/test_hooks.py @@ -0,0 +1,16 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import unittest +import frappe + +class TestHooks(unittest.TestCase): + def test_hooks(self): + hooks = frappe.get_hooks() + self.assertTrue(isinstance(hooks.get("app_name"), list)) + self.assertTrue(isinstance(hooks.get("doc_events"), dict)) + self.assertTrue(isinstance(hooks.get("doc_events").get("*"), dict)) + self.assertTrue(isinstance(hooks.get("doc_events").get("*"), dict)) + self.assertTrue("frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications" in + hooks.get("doc_events").get("*").get("on_update")) diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index f58a0f2e69..6cde4ce69c 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -1,5 +1,5 @@ # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt +# MIT License. See license.txt from __future__ import unicode_literals @@ -10,9 +10,9 @@ def make_boilerplate(dest): if not os.path.exists(dest): print "Destination directory does not exist" return - + hooks = frappe._dict() - for key in ("App Name", "App Title", "App Description", "App Publisher", + for key in ("App Name", "App Title", "App Description", "App Publisher", "App Icon", "App Color", "App Email", "App URL", "App License"): hook_key = key.lower().replace(" ", "_") hook_val = None @@ -21,29 +21,29 @@ def make_boilerplate(dest): if hook_key=="app_name" and hook_val.lower().replace(" ", "_") != hook_val: print "App Name must be all lowercase and without spaces" hook_val = "" - + hooks[hook_key] = hook_val - + frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, hooks.app_name)) frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates")) - frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", + frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", "statics")) - frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", + frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", "pages")) - frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", + frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", "generators")) frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "config")) - + # init files touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "__init__.py")) touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, hooks.app_name, "__init__.py")) touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", "__init__.py")) - touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", + touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", "pages", "__init__.py")) - touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", + touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", "generators", "__init__.py")) touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "__init__.py")) - + with open(os.path.join(dest, hooks.app_name, "MANIFEST.in"), "w") as f: f.write(manifest_template.format(**hooks)) @@ -64,17 +64,17 @@ def make_boilerplate(dest): with open(os.path.join(dest, hooks.app_name, hooks.app_name, "modules.txt"), "w") as f: f.write(hooks.app_name) - with open(os.path.join(dest, hooks.app_name, hooks.app_name, "hooks.txt"), "w") as f: + with open(os.path.join(dest, hooks.app_name, hooks.app_name, "hooks.py"), "w") as f: f.write(hooks_template.format(**hooks)) touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "patches.txt")) with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "desktop.py"), "w") as f: f.write(desktop_template.format(**hooks)) - - - + + + manifest_template = """include MANIFEST.in include requirements.txt include *.json @@ -93,24 +93,98 @@ recursive-include {app_name} *.py recursive-include {app_name} *.svg recursive-include {app_name} *.txt recursive-exclude {app_name} *.pyc""" - -hooks_template = """app_name = {app_name} -app_title = {app_title} -app_publisher = {app_publisher} -app_description = {app_description} -app_icon = {app_icon} -app_color = {app_color} -app_email = {app_email} -app_url = {app_url} -app_version = 0.0.1 + +hooks_template = """app_name = "{app_name}" +app_title = "{app_title}" +app_publisher = "{app_publisher}" +app_description = "{app_description}" +app_icon = "{app_icon}" +app_color = "{app_color}" +app_email = "{app_email}" +app_url = "{app_url}" +app_version = "0.0.1" + +# Includes in +# ------------------ + +# include js, css files in header of desk.html +# app_include_css = "/assets/{app_name}/{app_name}.css" +# app_include_js = "/assets/{app_name}/{app_name}.js" + +# include js, css files in header of web template +# web_include_css = "/assets/{app_name}/{app_name}.css" +# web_include_js = "/assets/{app_name}/{app_name}.js" + +# Installation +# ------------ + +# before_install = "{app_name}.install.before_install" +# after_install = "{app_name}.install.after_install" + +# Desk Notifications +# ------------------ +# See frappe.core.notifications.get_notification_config + +# notification_config = "{app_name}.notifications.get_notification_config" + +# Permissions +# ----------- +# Permissions evaluated in scripted ways + +# permission_query_conditions = { +# "Event": "frappe.core.doctype.event.event.get_permission_query_conditions", +# } +# +# has_permission = { +# "Event": "frappe.core.doctype.event.event.has_permission", +# } + +# Document Events +# --------------- +# Hook on document methods and events + +# doc_events = { +# "*": { +# "on_update": "method", +# "on_cancel": "method", +# "on_trash": "method" +# } +# } + +# Scheduled Tasks +# --------------- + +# scheduler_events = { +# "all": [ +# "{app_name}.tasks.all" +# ], +# "daily": [ +# "{app_name}.tasks.daily" +# ], +# "hourly": [ +# "{app_name}.tasks.hourly" +# ], +# "weekly": [ +# "{app_name}.tasks.weekly" +# ] +# "monthly": [ +# "{app_name}.tasks.monthly" +# ] +# } + +# Testing +# ------- + +# before_tests = "{app_name}.install.before_tests" + """ desktop_template = """from frappe import _ data = {{ "{app_title}": {{ - "color": "{app_color}", - "icon": "{app_icon}", + "color": "{app_color}", + "icon": "{app_icon}", "label": _("{app_title}") }} }} diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index c0153956e8..d1fdca751b 100644 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -1,5 +1,5 @@ # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt +# MIT License. See license.txt """ Events: always @@ -20,7 +20,7 @@ DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' def enqueue_events(site): if is_scheduler_disabled(): return - + # lock before queuing begins try: lock = create_lock('scheduler') @@ -28,31 +28,31 @@ def enqueue_events(site): return except LockTimeoutError: return - + nowtime = frappe.utils.now_datetime() last = frappe.db.get_global('scheduler_last_event') - + # set scheduler last event frappe.db.begin() frappe.db.set_global('scheduler_last_event', nowtime.strftime(DATETIME_FORMAT)) frappe.db.commit() - + out = [] if last: last = datetime.strptime(last, DATETIME_FORMAT) out = enqueue_applicable_events(site, nowtime, last) - + delete_lock('scheduler') - + return '\n'.join(out) - + def enqueue_applicable_events(site, nowtime, last): nowtime_str = nowtime.strftime(DATETIME_FORMAT) out = [] - + def _log(event): out.append("{time} - {event} - queued".format(time=nowtime_str, event=event)) - + if nowtime.day != last.day: # if first task of the day execute daily tasks trigger(site, "daily") and _log("daily") @@ -61,23 +61,23 @@ def enqueue_applicable_events(site, nowtime, last): if nowtime.month != last.month: trigger(site, "monthly") and _log("monthly") trigger(site, "monthly_long") and _log("monthly_long") - + if nowtime.weekday()==0: trigger(site, "weekly") and _log("weekly") trigger(site, "weekly_long") and _log("weekly_long") - + if nowtime.hour != last.hour: trigger(site, "hourly") and _log("hourly") trigger(site, "all") and _log("all") - + return out - + def trigger(site, event, now=False): """trigger method in startup.schedule_handler""" from frappe.tasks import scheduler_task - - for handler in frappe.get_hooks("scheduler_event:{}".format(event)): + + for handler in frappe.get_hooks("scheduler_events").get(event, []): if not check_lock(handler): if not now: scheduler_task.delay(site=site, event=event, handler=handler) @@ -85,36 +85,36 @@ def trigger(site, event, now=False): else: create_lock(handler) scheduler_task(site=site, event=event, handler=handler, now=True) - + def log(method, message=None): """log error in patch_log""" message = frappe.utils.cstr(message) + "\n" if message else "" message += frappe.get_traceback() - + if not (frappe.db and frappe.db._conn): frappe.connect() frappe.db.rollback() frappe.db.begin() - + d = frappe.get_doc("Scheduler Log") d.method = method d.error = message d.save() frappe.db.commit() - + return message - + def is_scheduler_disabled(): return frappe.utils.cint(frappe.db.get_global("disable_scheduler")) def enable_scheduler(): frappe.db.set_global("disable_scheduler", 0) - + def disable_scheduler(): frappe.db.set_global("disable_scheduler", 1) - + def get_errors(from_date, to_date, limit): errors = frappe.db.sql("""select modified, method, error from `tabScheduler Log` where date(modified) between %s and %s @@ -122,23 +122,20 @@ def get_errors(from_date, to_date, limit): order by modified limit %s""", (from_date, to_date, limit), as_dict=True) return ["""

    Time: {modified}

    Method: {method}\n{error}
    """.format(**e) for e in errors] - + def get_error_report(from_date=None, to_date=None, limit=10): from frappe.utils import get_url, now_datetime, add_days - + if not from_date: from_date = add_days(now_datetime().date(), -1) if not to_date: to_date = add_days(now_datetime().date(), -1) - + errors = get_errors(from_date, to_date, limit) - + if errors: return 1, """

    Scheduler Failed Events (max {limit}):

    URL: {url}


    {errors}""".format( limit=limit, url=get_url(), errors="
    ".join(errors)) else: return 0, "

    Scheduler didn't encounter any problems.

    " - -if __name__=='__main__': - execute() diff --git a/frappe/utils/sel.py b/frappe/utils/sel.py index 50f212016b..d11dcbea3f 100644 --- a/frappe/utils/sel.py +++ b/frappe/utils/sel.py @@ -9,7 +9,11 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException +from urllib import unquote +import time, frappe, subprocess +host = "http://localhost:8888" +pipe = None driver = None verbose = None host = None @@ -18,26 +22,35 @@ cur_route = False def start(_verbose=None): global driver, verbose - driver = webdriver.PhantomJS() verbose = _verbose + start_test_server(verbose) + driver = webdriver.PhantomJS() + +def start_test_server(verbose): + global pipe + pipe = subprocess.Popen(["frappe", "--serve", "--port", "8888"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + while not pipe.stderr.readline(): + time.sleep(0.5) + if verbose: + print "Test server started" + def get(url): driver.get(url) -def login(_host): - global host, logged_in +def login(wait_for_id="#page-desktop"): + global logged_in if logged_in: return - host = _host get(host + "/login") wait("#login_email") set_input("#login_email", "Administrator") set_input("#login_password", "admin" + Keys.RETURN) - wait("#page-desktop") + wait(wait_for_id) logged_in = True -def module(module_name): +def go_to_module(module_name, item=None): global cur_route # desktop @@ -56,26 +69,49 @@ def module(module_name): m[0].click() wait_for_page(page) + if item: + elem = find('[data-label="{0}"]'.format(item))[0] + elem.click() + page = unquote(elem.get_attribute("href").split("#", 1)[1]) + wait_for_page(page) + +def new_doc(module, doctype): + go_to_module(module, doctype) + find('.appframe-iconbar .icon-plus')[0].click() + wait_for_page("Form/" + doctype) + +def add_child(fieldname): + find('[data-fieldname="{0}"] .grid-add-row'.format(fieldname))[0].click() + wait('[data-fieldname="{0}"] .form-grid'.format(fieldname)) + def find(selector, everywhere=False): if cur_route and not everywhere: selector = cur_route + " " + selector return driver.find_elements_by_css_selector(selector) -def set_field_input(fieldname, value): - set_input('[data-fieldname="{0}"]'.format(fieldname), value) +def set_field(fieldname, value): + set_input('input[data-fieldname="{0}"]'.format(fieldname), value + Keys.TAB) + wait_for_ajax() + time.sleep(0.5) def primary_action(): find(".appframe-titlebar .btn-primary")[0].click() + wait_for_ajax() + +def wait_for_ajax(): + wait('body[data-ajax-state="complete"]', True) def wait_for_page(name): global cur_route cur_route = None route = '[data-page-route="{0}"]'.format(name) - wait(route) + elem = wait(route) + wait_for_ajax() cur_route = route + return elem def wait_for_state(state): - wait(cur_route + '[data-state="{0}"]'.format(state), True) + return wait(cur_route + '[data-state="{0}"]'.format(state), True) def wait(selector, everywhere=False): if cur_route and not everywhere: @@ -92,9 +128,11 @@ def wait(selector, everywhere=False): def set_input(selector, text): elem = find(selector)[0] + elem.clear() elem.send_keys(text) def close(): - global driver + global driver, pipe driver.quit() - driver = None + pipe.kill() + driver = pipe = None