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 = $('
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, """URL: {url}
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