@@ -391,24 +391,38 @@ def get_installed_apps(): | |||||
installed = json.loads(db.get_global("installed_apps") or "[]") | installed = json.loads(db.get_global("installed_apps") or "[]") | ||||
return installed | 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): | def load_app_hooks(app_name=None): | ||||
hooks = {} | hooks = {} | ||||
for app in [app_name] if app_name else get_installed_apps(): | 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 | 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: | if app_name: | ||||
hooks = _dict(load_app_hooks(app_name)) | hooks = _dict(load_app_hooks(app_name)) | ||||
else: | else: | ||||
hooks = _dict(cache().get_value("app_hooks", load_app_hooks)) | hooks = _dict(cache().get_value("app_hooks", load_app_hooks)) | ||||
if hook: | if hook: | ||||
return hooks.get(hook) or [] | |||||
return hooks.get(hook) or default or [] | |||||
else: | else: | ||||
return hooks | return hooks | ||||
@@ -5,7 +5,6 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import os | import os | ||||
import time | |||||
import subprocess | import subprocess | ||||
import frappe | 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) | admin_password = admin_password, verbose=verbose, force=force, site_config=site_config, reinstall=reinstall) | ||||
make_site_dirs() | make_site_dirs() | ||||
install_app("frappe", verbose=verbose, set_as_patched=not source_sql) | 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() | frappe.destroy() | ||||
@cmd | @cmd | ||||
@@ -707,16 +709,8 @@ def run_tests(app=None, module=None, doctype=None, verbose=False, tests=(), with | |||||
import frappe.test_runner | import frappe.test_runner | ||||
from frappe.utils import sel | from frappe.utils import sel | ||||
frappe.local.localhost = "http://localhost:8888" | |||||
if not without_serve: | 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 | ret = 1 | ||||
try: | try: | ||||
@@ -726,9 +720,7 @@ def run_tests(app=None, module=None, doctype=None, verbose=False, tests=(), with | |||||
ret = 0 | ret = 0 | ||||
finally: | finally: | ||||
if not without_serve: | if not without_serve: | ||||
pipe.terminate() | |||||
sel.close() | |||||
sel.close() | |||||
return ret | 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, | def execute(self, query=None, filters=None, fields=None, docstatus=None, | ||||
group_by=None, order_by=None, limit_start=0, limit_page_length=20, | group_by=None, order_by=None, limit_start=0, limit_page_length=20, | ||||
as_list=False, with_childnames=False, debug=False, ignore_permissions=False): | as_list=False, with_childnames=False, debug=False, ignore_permissions=False): | ||||
if not frappe.has_permission(self.doctype, "read"): | |||||
raise frappe.PermissionError | |||||
if fields: | if fields: | ||||
self.fields = fields | self.fields = fields | ||||
self.filters = filters or [] | self.filters = filters or [] | ||||
@@ -254,7 +257,7 @@ class DatabaseQuery(object): | |||||
return conditions | return conditions | ||||
def get_permission_query_conditions(self): | 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: | if condition_methods: | ||||
conditions = [] | conditions = [] | ||||
for method in condition_methods: | for method in condition_methods: | ||||
@@ -423,8 +423,9 @@ class Document(BaseDocument): | |||||
def composer(self, *args, **kwargs): | def composer(self, *args, **kwargs): | ||||
hooks = [] | hooks = [] | ||||
method = f.__name__ | 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)) | hooks.append(frappe.get_attr(handler)) | ||||
composed = compose(f, *hooks) | composed = compose(f, *hooks) | ||||
@@ -116,7 +116,7 @@ def has_controller_permissions(doc): | |||||
else: | else: | ||||
doc = frappe.get_doc(doc.doctype, doc.name) | 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): | if not frappe.call(frappe.get_attr(method), doc=doc): | ||||
return False | return False | ||||
@@ -246,6 +246,12 @@ ul.linked-with-list li { | |||||
/* form grid */ | /* form grid */ | ||||
.form-grid { | |||||
border: 2px solid #c7c7c7; | |||||
margin-bottom: 15px; | |||||
border-radius: 3px; | |||||
} | |||||
.grid-heading-row { | .grid-heading-row { | ||||
padding: 8px; | padding: 8px; | ||||
border-bottom: 1px solid #dddddd; | border-bottom: 1px solid #dddddd; | ||||
@@ -127,15 +127,15 @@ var pending_req = 0 | |||||
frappe.set_loading = function() { | frappe.set_loading = function() { | ||||
pending_req++; | pending_req++; | ||||
//$('#spinner').css('visibility', 'visible'); | //$('#spinner').css('visibility', 'visible'); | ||||
$('body').css('cursor', 'progress'); | |||||
$('body').css('cursor', 'progress').attr("data-ajax-state", "running"); | |||||
NProgress.start(); | NProgress.start(); | ||||
$("body"); | |||||
} | } | ||||
frappe.done_loading = function() { | frappe.done_loading = function() { | ||||
pending_req--; | pending_req--; | ||||
if(!pending_req){ | if(!pending_req){ | ||||
$('body').css('cursor', 'default'); | |||||
//$('#spinner').css('visibility', 'hidden'); | |||||
$('body').css('cursor', 'default').attr("data-ajax-state", "complete"); | |||||
NProgress.done(); | NProgress.done(); | ||||
} else { | } else { | ||||
NProgress.inc(); | NProgress.inc(); | ||||
@@ -11,7 +11,7 @@ frappe.ui.form.Grid = Class.extend({ | |||||
var me = this; | var me = this; | ||||
this.wrapper = $('<div>\ | 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="grid-heading-row" style="font-size: 15px; background-color: #f9f9f9;"></div>\ | ||||
<div class="panel-body">\ | <div class="panel-body">\ | ||||
<div class="rows"></div>\ | <div class="rows"></div>\ | ||||
@@ -22,7 +22,9 @@ frappe.ui.form.Grid = Class.extend({ | |||||
</div>\ | </div>\ | ||||
</div>\ | </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() { | $(this.wrapper).find(".grid-add-row").click(function() { | ||||
me.add_new_row(null, null, true); | me.add_new_row(null, null, true); | ||||
@@ -136,9 +136,11 @@ frappe.request.prepare = function(opts) { | |||||
console.log(opts) | console.log(opts) | ||||
throw "Incomplete Request"; | throw "Incomplete Request"; | ||||
} | } | ||||
} | } | ||||
frappe.request.cleanup = function(opts, r) { | frappe.request.cleanup = function(opts, r) { | ||||
// stop button indicator | // stop button indicator | ||||
if(opts.btn) $(opts.btn).done_working(); | 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">\ | $list_item = $($r('<li class="list-group-item">\ | ||||
<div class="row">\ | <div class="row">\ | ||||
<div class="col-sm-6 list-item-name">\ | <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 class="col-sm-6 text-muted list-item-description">%(description)s</div>\ | ||||
</div>\ | </div>\ | ||||
</li>', item)).appendTo($list); | </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(".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); | var fn = eval(item.onclick); | ||||
if(typeof(fn)==="function") { | if(typeof(fn)==="function") { | ||||
fn(); | 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 { | } 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 | var show_count = (item.type==="doctype" || (item.type==="page" && item.doctype)) && !item.hide_count | ||||
if(show_count) { | if(show_count) { | ||||
@@ -25,7 +25,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=()): | |||||
if verbose: | if verbose: | ||||
print 'Running "before_tests" hooks' | 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)() | frappe.get_attr(fn)() | ||||
if doctype: | if doctype: | ||||
@@ -15,6 +15,15 @@ class TestLogin(unittest.TestCase): | |||||
sel.module("ToDo") | sel.module("ToDo") | ||||
sel.find('.appframe-iconbar .icon-plus')[0].click() | sel.find('.appframe-iconbar .icon-plus')[0].click() | ||||
sel.wait_for_page("Form/ToDo") | sel.wait_for_page("Form/ToDo") | ||||
sel.set_field_input("description", "test description") | |||||
sel.set_field("description", "test description") | |||||
sel.primary_action() | 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 | # 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 | from __future__ import unicode_literals | ||||
@@ -10,9 +10,9 @@ def make_boilerplate(dest): | |||||
if not os.path.exists(dest): | if not os.path.exists(dest): | ||||
print "Destination directory does not exist" | print "Destination directory does not exist" | ||||
return | return | ||||
hooks = frappe._dict() | 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"): | "App Icon", "App Color", "App Email", "App URL", "App License"): | ||||
hook_key = key.lower().replace(" ", "_") | hook_key = key.lower().replace(" ", "_") | ||||
hook_val = None | hook_val = None | ||||
@@ -21,29 +21,29 @@ def make_boilerplate(dest): | |||||
if hook_key=="app_name" and hook_val.lower().replace(" ", "_") != hook_val: | if hook_key=="app_name" and hook_val.lower().replace(" ", "_") != hook_val: | ||||
print "App Name must be all lowercase and without spaces" | print "App Name must be all lowercase and without spaces" | ||||
hook_val = "" | hook_val = "" | ||||
hooks[hook_key] = 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, 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", | |||||
frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "templates", | |||||
"statics")) | "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")) | "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")) | "generators")) | ||||
frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "config")) | frappe.create_folder(os.path.join(dest, hooks.app_name, hooks.app_name, "config")) | ||||
# init files | # 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, "__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, 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", "__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")) | "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")) | "generators", "__init__.py")) | ||||
touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "__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: | with open(os.path.join(dest, hooks.app_name, "MANIFEST.in"), "w") as f: | ||||
f.write(manifest_template.format(**hooks)) | 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: | with open(os.path.join(dest, hooks.app_name, hooks.app_name, "modules.txt"), "w") as f: | ||||
f.write(hooks.app_name) | 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)) | f.write(hooks_template.format(**hooks)) | ||||
touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "patches.txt")) | 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: | with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "desktop.py"), "w") as f: | ||||
f.write(desktop_template.format(**hooks)) | f.write(desktop_template.format(**hooks)) | ||||
manifest_template = """include MANIFEST.in | manifest_template = """include MANIFEST.in | ||||
include requirements.txt | include requirements.txt | ||||
include *.json | include *.json | ||||
@@ -93,24 +93,98 @@ recursive-include {app_name} *.py | |||||
recursive-include {app_name} *.svg | recursive-include {app_name} *.svg | ||||
recursive-include {app_name} *.txt | recursive-include {app_name} *.txt | ||||
recursive-exclude {app_name} *.pyc""" | 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 _ | desktop_template = """from frappe import _ | ||||
data = {{ | data = {{ | ||||
"{app_title}": {{ | "{app_title}": {{ | ||||
"color": "{app_color}", | |||||
"icon": "{app_icon}", | |||||
"color": "{app_color}", | |||||
"icon": "{app_icon}", | |||||
"label": _("{app_title}") | "label": _("{app_title}") | ||||
}} | }} | ||||
}} | }} | ||||
@@ -1,5 +1,5 @@ | |||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors | ||||
# MIT License. See license.txt | |||||
# MIT License. See license.txt | |||||
""" | """ | ||||
Events: | Events: | ||||
always | always | ||||
@@ -20,7 +20,7 @@ DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' | |||||
def enqueue_events(site): | def enqueue_events(site): | ||||
if is_scheduler_disabled(): | if is_scheduler_disabled(): | ||||
return | return | ||||
# lock before queuing begins | # lock before queuing begins | ||||
try: | try: | ||||
lock = create_lock('scheduler') | lock = create_lock('scheduler') | ||||
@@ -28,31 +28,31 @@ def enqueue_events(site): | |||||
return | return | ||||
except LockTimeoutError: | except LockTimeoutError: | ||||
return | return | ||||
nowtime = frappe.utils.now_datetime() | nowtime = frappe.utils.now_datetime() | ||||
last = frappe.db.get_global('scheduler_last_event') | last = frappe.db.get_global('scheduler_last_event') | ||||
# set scheduler last event | # set scheduler last event | ||||
frappe.db.begin() | frappe.db.begin() | ||||
frappe.db.set_global('scheduler_last_event', nowtime.strftime(DATETIME_FORMAT)) | frappe.db.set_global('scheduler_last_event', nowtime.strftime(DATETIME_FORMAT)) | ||||
frappe.db.commit() | frappe.db.commit() | ||||
out = [] | out = [] | ||||
if last: | if last: | ||||
last = datetime.strptime(last, DATETIME_FORMAT) | last = datetime.strptime(last, DATETIME_FORMAT) | ||||
out = enqueue_applicable_events(site, nowtime, last) | out = enqueue_applicable_events(site, nowtime, last) | ||||
delete_lock('scheduler') | delete_lock('scheduler') | ||||
return '\n'.join(out) | return '\n'.join(out) | ||||
def enqueue_applicable_events(site, nowtime, last): | def enqueue_applicable_events(site, nowtime, last): | ||||
nowtime_str = nowtime.strftime(DATETIME_FORMAT) | nowtime_str = nowtime.strftime(DATETIME_FORMAT) | ||||
out = [] | out = [] | ||||
def _log(event): | def _log(event): | ||||
out.append("{time} - {event} - queued".format(time=nowtime_str, event=event)) | out.append("{time} - {event} - queued".format(time=nowtime_str, event=event)) | ||||
if nowtime.day != last.day: | if nowtime.day != last.day: | ||||
# if first task of the day execute daily tasks | # if first task of the day execute daily tasks | ||||
trigger(site, "daily") and _log("daily") | trigger(site, "daily") and _log("daily") | ||||
@@ -61,23 +61,23 @@ def enqueue_applicable_events(site, nowtime, last): | |||||
if nowtime.month != last.month: | if nowtime.month != last.month: | ||||
trigger(site, "monthly") and _log("monthly") | trigger(site, "monthly") and _log("monthly") | ||||
trigger(site, "monthly_long") and _log("monthly_long") | trigger(site, "monthly_long") and _log("monthly_long") | ||||
if nowtime.weekday()==0: | if nowtime.weekday()==0: | ||||
trigger(site, "weekly") and _log("weekly") | trigger(site, "weekly") and _log("weekly") | ||||
trigger(site, "weekly_long") and _log("weekly_long") | trigger(site, "weekly_long") and _log("weekly_long") | ||||
if nowtime.hour != last.hour: | if nowtime.hour != last.hour: | ||||
trigger(site, "hourly") and _log("hourly") | trigger(site, "hourly") and _log("hourly") | ||||
trigger(site, "all") and _log("all") | trigger(site, "all") and _log("all") | ||||
return out | return out | ||||
def trigger(site, event, now=False): | def trigger(site, event, now=False): | ||||
"""trigger method in startup.schedule_handler""" | """trigger method in startup.schedule_handler""" | ||||
from frappe.tasks import scheduler_task | 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 check_lock(handler): | ||||
if not now: | if not now: | ||||
scheduler_task.delay(site=site, event=event, handler=handler) | scheduler_task.delay(site=site, event=event, handler=handler) | ||||
@@ -85,36 +85,36 @@ def trigger(site, event, now=False): | |||||
else: | else: | ||||
create_lock(handler) | create_lock(handler) | ||||
scheduler_task(site=site, event=event, handler=handler, now=True) | scheduler_task(site=site, event=event, handler=handler, now=True) | ||||
def log(method, message=None): | def log(method, message=None): | ||||
"""log error in patch_log""" | """log error in patch_log""" | ||||
message = frappe.utils.cstr(message) + "\n" if message else "" | message = frappe.utils.cstr(message) + "\n" if message else "" | ||||
message += frappe.get_traceback() | message += frappe.get_traceback() | ||||
if not (frappe.db and frappe.db._conn): | if not (frappe.db and frappe.db._conn): | ||||
frappe.connect() | frappe.connect() | ||||
frappe.db.rollback() | frappe.db.rollback() | ||||
frappe.db.begin() | frappe.db.begin() | ||||
d = frappe.get_doc("Scheduler Log") | d = frappe.get_doc("Scheduler Log") | ||||
d.method = method | d.method = method | ||||
d.error = message | d.error = message | ||||
d.save() | d.save() | ||||
frappe.db.commit() | frappe.db.commit() | ||||
return message | return message | ||||
def is_scheduler_disabled(): | def is_scheduler_disabled(): | ||||
return frappe.utils.cint(frappe.db.get_global("disable_scheduler")) | return frappe.utils.cint(frappe.db.get_global("disable_scheduler")) | ||||
def enable_scheduler(): | def enable_scheduler(): | ||||
frappe.db.set_global("disable_scheduler", 0) | frappe.db.set_global("disable_scheduler", 0) | ||||
def disable_scheduler(): | def disable_scheduler(): | ||||
frappe.db.set_global("disable_scheduler", 1) | frappe.db.set_global("disable_scheduler", 1) | ||||
def get_errors(from_date, to_date, limit): | def get_errors(from_date, to_date, limit): | ||||
errors = frappe.db.sql("""select modified, method, error from `tabScheduler Log` | errors = frappe.db.sql("""select modified, method, error from `tabScheduler Log` | ||||
where date(modified) between %s and %s | 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) | 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) | return ["""<p>Time: {modified}</p><pre><code>Method: {method}\n{error}</code></pre>""".format(**e) | ||||
for e in errors] | for e in errors] | ||||
def get_error_report(from_date=None, to_date=None, limit=10): | def get_error_report(from_date=None, to_date=None, limit=10): | ||||
from frappe.utils import get_url, now_datetime, add_days | from frappe.utils import get_url, now_datetime, add_days | ||||
if not from_date: | if not from_date: | ||||
from_date = add_days(now_datetime().date(), -1) | from_date = add_days(now_datetime().date(), -1) | ||||
if not to_date: | if not to_date: | ||||
to_date = add_days(now_datetime().date(), -1) | to_date = add_days(now_datetime().date(), -1) | ||||
errors = get_errors(from_date, to_date, limit) | errors = get_errors(from_date, to_date, limit) | ||||
if errors: | if errors: | ||||
return 1, """<h4>Scheduler Failed Events (max {limit}):</h4> | return 1, """<h4>Scheduler Failed Events (max {limit}):</h4> | ||||
<p>URL: <a href="{url}" target="_blank">{url}</a></p><hr>{errors}""".format( | <p>URL: <a href="{url}" target="_blank">{url}</a></p><hr>{errors}""".format( | ||||
limit=limit, url=get_url(), errors="<hr>".join(errors)) | limit=limit, url=get_url(), errors="<hr>".join(errors)) | ||||
else: | else: | ||||
return 0, "<p>Scheduler didn't encounter any problems.</p>" | 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.ui import WebDriverWait | ||||
from selenium.webdriver.support import expected_conditions as EC | from selenium.webdriver.support import expected_conditions as EC | ||||
from selenium.common.exceptions import TimeoutException | from selenium.common.exceptions import TimeoutException | ||||
from urllib import unquote | |||||
import time, frappe, subprocess | |||||
host = "http://localhost:8888" | |||||
pipe = None | |||||
driver = None | driver = None | ||||
verbose = None | verbose = None | ||||
host = None | host = None | ||||
@@ -18,26 +22,35 @@ cur_route = False | |||||
def start(_verbose=None): | def start(_verbose=None): | ||||
global driver, verbose | global driver, verbose | ||||
driver = webdriver.PhantomJS() | |||||
verbose = _verbose | 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): | def get(url): | ||||
driver.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: | if logged_in: | ||||
return | return | ||||
host = _host | |||||
get(host + "/login") | get(host + "/login") | ||||
wait("#login_email") | wait("#login_email") | ||||
set_input("#login_email", "Administrator") | set_input("#login_email", "Administrator") | ||||
set_input("#login_password", "admin" + Keys.RETURN) | set_input("#login_password", "admin" + Keys.RETURN) | ||||
wait("#page-desktop") | |||||
wait(wait_for_id) | |||||
logged_in = True | logged_in = True | ||||
def module(module_name): | |||||
def go_to_module(module_name, item=None): | |||||
global cur_route | global cur_route | ||||
# desktop | # desktop | ||||
@@ -56,26 +69,49 @@ def module(module_name): | |||||
m[0].click() | m[0].click() | ||||
wait_for_page(page) | 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): | def find(selector, everywhere=False): | ||||
if cur_route and not everywhere: | if cur_route and not everywhere: | ||||
selector = cur_route + " " + selector | selector = cur_route + " " + selector | ||||
return driver.find_elements_by_css_selector(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(): | def primary_action(): | ||||
find(".appframe-titlebar .btn-primary")[0].click() | 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): | def wait_for_page(name): | ||||
global cur_route | global cur_route | ||||
cur_route = None | cur_route = None | ||||
route = '[data-page-route="{0}"]'.format(name) | route = '[data-page-route="{0}"]'.format(name) | ||||
wait(route) | |||||
elem = wait(route) | |||||
wait_for_ajax() | |||||
cur_route = route | cur_route = route | ||||
return elem | |||||
def wait_for_state(state): | 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): | def wait(selector, everywhere=False): | ||||
if cur_route and not everywhere: | if cur_route and not everywhere: | ||||
@@ -92,9 +128,11 @@ def wait(selector, everywhere=False): | |||||
def set_input(selector, text): | def set_input(selector, text): | ||||
elem = find(selector)[0] | elem = find(selector)[0] | ||||
elem.clear() | |||||
elem.send_keys(text) | elem.send_keys(text) | ||||
def close(): | def close(): | ||||
global driver | |||||
global driver, pipe | |||||
driver.quit() | driver.quit() | ||||
driver = None | |||||
pipe.kill() | |||||
driver = pipe = None |