@@ -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 | |||
@@ -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 | |||
@@ -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" | |||
] | |||
} |
@@ -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 |
@@ -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: | |||
@@ -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) | |||
@@ -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 | |||
@@ -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; | |||
@@ -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(); | |||
@@ -11,7 +11,7 @@ frappe.ui.form.Grid = Class.extend({ | |||
var me = this; | |||
this.wrapper = $('<div>\ | |||
<div class="" style="border: 2px solid #c7c7c7; margin-bottom: 15px; border-radius: 3px;">\ | |||
<div class="form-grid">\ | |||
<div class="grid-heading-row" style="font-size: 15px; background-color: #f9f9f9;"></div>\ | |||
<div class="panel-body">\ | |||
<div class="rows"></div>\ | |||
@@ -22,7 +22,9 @@ frappe.ui.form.Grid = Class.extend({ | |||
</div>\ | |||
</div>\ | |||
</div>\ | |||
</div>').appendTo(this.parent); | |||
</div>') | |||
.appendTo(this.parent) | |||
.attr("data-fieldname", this.df.fieldname); | |||
$(this.wrapper).find(".grid-add-row").click(function() { | |||
me.add_new_row(null, null, true); | |||
@@ -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(); | |||
@@ -194,7 +194,7 @@ frappe.views.moduleview.ModuleView = Class.extend({ | |||
$list_item = $($r('<li class="list-group-item">\ | |||
<div class="row">\ | |||
<div class="col-sm-6 list-item-name">\ | |||
<a><i class="%(icon)s icon-fixed-width"></i> %(label)s</a></div>\ | |||
<a data-label="%(label)s"><i class="%(icon)s icon-fixed-width"></i> %(label)s</a></div>\ | |||
<div class="col-sm-6 text-muted list-item-description">%(description)s</div>\ | |||
</div>\ | |||
</li>', 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) { | |||
@@ -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: | |||
@@ -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") | |||
@@ -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")) |
@@ -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 <head> | |||
# ------------------ | |||
# 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}") | |||
}} | |||
}} | |||
@@ -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 ["""<p>Time: {modified}</p><pre><code>Method: {method}\n{error}</code></pre>""".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, """<h4>Scheduler Failed Events (max {limit}):</h4> | |||
<p>URL: <a href="{url}" target="_blank">{url}</a></p><hr>{errors}""".format( | |||
limit=limit, url=get_url(), errors="<hr>".join(errors)) | |||
else: | |||
return 0, "<p>Scheduler didn't encounter any problems.</p>" | |||
if __name__=='__main__': | |||
execute() |
@@ -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 |