From 7a1a4c872a7cb377a5e2ecb7561685abe75c3809 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 5 Feb 2014 17:05:52 +0530 Subject: [PATCH] added /api --- webnotes/__init__.py | 14 +- webnotes/api.py | 97 ++++++++++++ webnotes/app.py | 5 +- webnotes/auth.py | 38 ++--- .../notification_count/notification_count.py | 4 +- webnotes/handler.py | 143 ++---------------- webnotes/model/bean.py | 30 ++-- webnotes/sessions.py | 3 +- webnotes/utils/response.py | 102 +++++++++++++ webnotes/widgets/reportview.py | 17 ++- 10 files changed, 283 insertions(+), 170 deletions(-) create mode 100644 webnotes/api.py create mode 100644 webnotes/utils/response.py diff --git a/webnotes/__init__.py b/webnotes/__init__.py index 11dd1555bb..cc49b656f6 100644 --- a/webnotes/__init__.py +++ b/webnotes/__init__.py @@ -11,7 +11,7 @@ from werkzeug.local import Local, release_local from werkzeug.exceptions import NotFound from MySQLdb import ProgrammingError as SQLError -import os, sys, importlib +import os, sys, importlib, inspect import json import semantic_version @@ -459,6 +459,18 @@ def get_attr(method_string): modulename = '.'.join(method_string.split('.')[:-1]) methodname = method_string.split('.')[-1] return getattr(get_module(modulename), methodname) + +def call(fn, *args, **kwargs): + if hasattr(fn, 'fnargs'): + fnargs = fn.fnargs + else: + fnargs, varargs, varkw, defaults = inspect.getargspec(fn) + + newargs = {} + for a in fnargs: + if a in kwargs: + newargs[a] = kwargs.get(a) + return fn(*args, **newargs) def make_property_setter(args): args = _dict(args) diff --git a/webnotes/api.py b/webnotes/api.py new file mode 100644 index 0000000000..7305bf6618 --- /dev/null +++ b/webnotes/api.py @@ -0,0 +1,97 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import webnotes +import webnotes.handler +import webnotes.client +import webnotes.widgets.reportview +from webnotes.utils.response import build_response, report_error + +def handle(): + """ + /api/method/{methodname} will call a whitelisted method + /api/resource/{doctype} will query a table + examples: + ?fields=["name", "owner"] + ?filters=[["Task", "name", "like", "%005"]] + ?limit_start=0 + ?limit_page_length=20 + /api/resource/{doctype}/{name} will point to a resource + GET will return doclist + POST will insert + PUT will update + DELETE will delete + /api/resource/{doctype}/{name}?run_method={method} will run a whitelisted controller method + """ + parts = webnotes.request.path[1:].split("/") + call = doctype = name = None + + if len(parts) > 1: + call = parts[1] + + if len(parts) > 2: + doctype = parts[2] + + if len(parts) > 3: + name = parts[3] + + try: + if call=="method": + webnotes.local.form_dict.cmd = doctype + webnotes.handler.handle() + return + + elif call=="resource": + if "run_method" in webnotes.local.form_dict: + bean = webnotes.bean(doctype, name) + + if webnotes.local.request.method=="GET": + if not bean.has_permission("read"): + webnotes.throw("No Permission", webnotes.PermissionError) + webnotes.local.response.update({"data": bean.run_method(webnotes.local.form_dict.run_method, + **webnotes.local.form_dict)}) + + if webnotes.local.request.method=="POST": + if not bean.has_permission("write"): + webnotes.throw("No Permission", webnotes.PermissionError) + webnotes.local.response.update({"data":bean.run_method(webnotes.local.form_dict.run_method, + **webnotes.local.form_dict)}) + webnotes.conn.commit() + + else: + if name: + if webnotes.local.request.method=="GET": + webnotes.local.response.update({ + "doclist": webnotes.client.get(doctype, + name)}) + + if webnotes.local.request.method=="POST": + webnotes.local.response.update({ + "doclist": webnotes.client.insert(webnotes.local.form_dict.doclist)}) + webnotes.conn.commit() + + if webnotes.local.request.method=="PUT": + webnotes.local.response.update({ + "doclist":webnotes.client.save(webnotes.local.form_dict.doclist)}) + webnotes.conn.commit() + + if webnotes.local.request.method=="DELETE": + webnotes.client.delete(doctype, name) + webnotes.local.response.message = "ok" + + elif doctype: + if webnotes.local.request.method=="GET": + webnotes.local.response.update({ + "data": webnotes.call(webnotes.widgets.reportview.execute, + doctype, **webnotes.local.form_dict)}) + + else: + raise Exception("Bad API") + + else: + raise Exception("Bad API") + + except Exception, e: + report_error(500) + + build_response() diff --git a/webnotes/app.py b/webnotes/app.py index e2b819337c..92040aebf4 100644 --- a/webnotes/app.py +++ b/webnotes/app.py @@ -18,6 +18,7 @@ import mimetypes import webnotes import webnotes.handler import webnotes.auth +import webnotes.api import webnotes.webutils from webnotes.utils import get_site_name @@ -56,8 +57,10 @@ def application(request): webnotes.local._response = Response() webnotes.http_request = webnotes.auth.HTTPRequest() - if webnotes.form_dict.cmd: + if webnotes.local.form_dict.cmd: webnotes.handler.handle() + elif webnotes.request.path.startswith("/api/"): + webnotes.api.handle() elif webnotes.local.request.method in ('GET', 'HEAD'): webnotes.webutils.render(webnotes.request.path[1:]) else: diff --git a/webnotes/auth.py b/webnotes/auth.py index e0bc737c6c..cffed6f9ea 100644 --- a/webnotes/auth.py +++ b/webnotes/auth.py @@ -87,11 +87,11 @@ class HTTPRequest: class LoginManager: def __init__(self): self.user = None - if webnotes.form_dict.get('cmd')=='login': + if webnotes.local.form_dict.get('cmd')=='login' or webnotes.local.request.path=="/api/method/login": self.login() else: self.make_session(resume=True) - + def login(self): # clear cache webnotes.clear_cache(user = webnotes.form_dict.get('usr')) @@ -109,16 +109,16 @@ class LoginManager: info = webnotes.conn.get_value("Profile", self.user, ["user_type", "first_name", "last_name"], as_dict=1) if info.user_type=="Website User": - webnotes._response.set_cookie("system_user", "no") - webnotes.response["message"] = "No App" + webnotes.local._response.set_cookie("system_user", "no") + webnotes.local.response["message"] = "No App" else: - webnotes._response.set_cookie("system_user", "yes") - webnotes.response['message'] = 'Logged In' + webnotes.local._response.set_cookie("system_user", "yes") + webnotes.local.response['message'] = 'Logged In' full_name = " ".join(filter(None, [info.first_name, info.last_name])) - webnotes.response["full_name"] = full_name - webnotes._response.set_cookie("full_name", full_name) - webnotes._response.set_cookie("user_id", self.user) + webnotes.local.response["full_name"] = full_name + webnotes.local._response.set_cookie("full_name", full_name) + webnotes.local._response.set_cookie("user_id", self.user) def make_session(self, resume=False): # start session @@ -154,7 +154,7 @@ class LoginManager: return user[0][0] # in correct case def fail(self, message): - webnotes.response['message'] = message + webnotes.local.response['message'] = message raise webnotes.AuthenticationError @@ -213,12 +213,12 @@ class LoginManager: if user == webnotes.session.user: webnotes.session.sid = "" - webnotes._response.delete_cookie("full_name") - webnotes._response.delete_cookie("user_id") - webnotes._response.delete_cookie("sid") - webnotes._response.set_cookie("full_name", "") - webnotes._response.set_cookie("user_id", "") - webnotes._response.set_cookie("sid", "") + webnotes.local._response.delete_cookie("full_name") + webnotes.local._response.delete_cookie("user_id") + webnotes.local._response.delete_cookie("sid") + webnotes.local._response.set_cookie("full_name", "") + webnotes.local._response.set_cookie("user_id", "") + webnotes.local._response.set_cookie("sid", "") class CookieManager: def __init__(self): @@ -231,9 +231,9 @@ class CookieManager: # sid expires in 3 days expires = datetime.datetime.now() + datetime.timedelta(days=3) if webnotes.session.sid: - webnotes._response.set_cookie("sid", webnotes.session.sid, expires = expires) + webnotes.local._response.set_cookie("sid", webnotes.session.sid, expires = expires) if webnotes.session.session_country: - webnotes._response.set_cookie('country', webnotes.session.get("session_country")) + webnotes.local._response.set_cookie('country', webnotes.session.get("session_country")) def set_remember_me(self): from webnotes.utils import cint @@ -247,7 +247,7 @@ class CookieManager: expires = datetime.datetime.now() + \ datetime.timedelta(days=remember_days) - webnotes._response.set_cookie["remember_me"] = 1 + webnotes.local._response.set_cookie["remember_me"] = 1 def _update_password(user, password): diff --git a/webnotes/core/doctype/notification_count/notification_count.py b/webnotes/core/doctype/notification_count/notification_count.py index 2512099f60..1c686a6137 100644 --- a/webnotes/core/doctype/notification_count/notification_count.py +++ b/webnotes/core/doctype/notification_count/notification_count.py @@ -56,12 +56,12 @@ def delete_notification_count_for(doctype): def delete_event_notification_count(): delete_notification_count_for("Event") -def clear_doctype_notifications(controller, method=None): +def clear_doctype_notifications(bean, method=None): if webnotes.flags.in_import: return config = get_notification_config() - doctype = controller.doc.doctype + doctype = bean.doc.doctype if doctype in config.for_doctype: delete_notification_count_for(doctype) diff --git a/webnotes/handler.py b/webnotes/handler.py index 95d28e6631..e3d24479d4 100755 --- a/webnotes/handler.py +++ b/webnotes/handler.py @@ -2,23 +2,20 @@ # MIT License. See license.txt from __future__ import unicode_literals -import sys, os +import json import webnotes import webnotes.utils import webnotes.sessions +import webnotes.utils.file_manager +import webnotes.widgets.form.run_method +from webnotes.utils.response import build_response, report_error @webnotes.whitelist(allow_guest=True) def startup(): webnotes.response.update(webnotes.sessions.get()) -def cleanup_docs(): - import webnotes.model.utils - if webnotes.response.get('docs') and type(webnotes.response['docs'])!=dict: - webnotes.response['docs'] = webnotes.model.utils.compress(webnotes.response['docs']) - @webnotes.whitelist() def runserverobj(arg=None): - import webnotes.widgets.form.run_method webnotes.widgets.form.run_method.runserverobj() @webnotes.whitelist(allow_guest=True) @@ -38,16 +35,12 @@ def run_custom_method(doctype, name, custom_method): bean = webnotes.bean(doctype, name) controller = bean.get_controller() if getattr(controller, custom_method, webnotes._dict()).is_whitelisted: - call(getattr(controller, custom_method), webnotes.local.form_dict) + webnotes.call(getattr(controller, custom_method), **webnotes.local.form_dict) else: webnotes.throw("Not Allowed") @webnotes.whitelist() def uploadfile(): - import webnotes.utils - import webnotes.utils.file_manager - import json - try: if webnotes.form_dict.get('from_form'): try: @@ -67,19 +60,9 @@ def uploadfile(): def handle(): """handle request""" - cmd = webnotes.form_dict['cmd'] + cmd = webnotes.local.form_dict.cmd - def _error(status_code): - webnotes.errprint(webnotes.utils.get_traceback()) - webnotes._response.status_code = status_code - if webnotes.request_method == "POST": - webnotes.conn.rollback() - - if cmd!='login': - # login executed in webnotes.auth - if webnotes.request_method == "POST": - webnotes.conn.begin() - + if cmd!='login': status_codes = { webnotes.PermissionError: 403, webnotes.AuthenticationError: 401, @@ -91,12 +74,12 @@ def handle(): try: execute_cmd(cmd) except Exception, e: - _error(status_codes.get(e.__class__, 500)) + report_error(status_codes.get(e.__class__, 500)) else: - if webnotes.request_method == "POST" and webnotes.conn: + if webnotes.local.request.method in ("POST", "PUT") and webnotes.conn: webnotes.conn.commit() - print_response() + build_response() if webnotes.conn: webnotes.conn.close() @@ -117,7 +100,7 @@ def execute_cmd(cmd): webnotes.msgprint('Not Allowed, %s' % str(method)) raise webnotes.PermissionError('Not Allowed, %s' % str(method)) - ret = call(method, webnotes.form_dict) + ret = webnotes.call(method, **webnotes.form_dict) # returns with a message if ret: @@ -128,20 +111,6 @@ def execute_cmd(cmd): webnotes.local.session_obj.update() -def call(fn, args): - import inspect - - if hasattr(fn, 'fnargs'): - fnargs = fn.fnargs - else: - fnargs, varargs, varkw, defaults = inspect.getargspec(fn) - - newargs = {} - for a in fnargs: - if a in args: - newargs[a] = args.get(a) - return fn(**newargs) - def get_attr(cmd): """get method object from cmd""" if '.' in cmd: @@ -151,93 +120,3 @@ def get_attr(cmd): webnotes.log("method:" + cmd) return method -def print_response(): - print_map = { - 'csv': print_csv, - 'download': print_raw, - 'json': print_json, - 'page': print_page - } - - print_map.get(webnotes.response.get('type'), print_json)() - -def print_page(): - """print web page""" - - from webnotes.webutils import render - render(webnotes.response['page_name']) - -def print_json(): - make_logs() - cleanup_docs() - - webnotes._response.headers["Content-Type"] = "text/json; charset: utf-8" - - import json - - print_zip(json.dumps(webnotes.local.response, default=json_handler, separators=(',',':'))) - -def print_csv(): - webnotes._response.headers["Content-Type"] = \ - "text/csv; charset: utf-8" - webnotes._response.headers["Content-Disposition"] = \ - "attachment; filename=%s.csv" % webnotes.response['doctype'].replace(' ', '_') - webnotes._response.data = webnotes.response['result'] - -def print_raw(): - webnotes._response.headers["Content-Type"] = \ - mimetypes.guess_type(webnotes.response['filename'])[0] or "application/unknown" - webnotes._response.headers["Content-Disposition"] = \ - "filename=%s" % webnotes.response['filename'].replace(' ', '_') - webnotes._response.data = webnotes.response['filecontent'] - -def make_logs(): - """make strings for msgprint and errprint""" - import json - from webnotes import conf - from webnotes.utils import cstr - if webnotes.error_log: - # webnotes.response['exc'] = json.dumps("\n".join([cstr(d) for d in webnotes.error_log])) - webnotes.response['exc'] = json.dumps([cstr(d) for d in webnotes.local.error_log]) - - if webnotes.local.message_log: - webnotes.response['_server_messages'] = json.dumps([cstr(d) for d in webnotes.local.message_log]) - - if webnotes.debug_log and conf.get("logging") or False: - webnotes.response['_debug_messages'] = json.dumps(webnotes.local.debug_log) - -def print_zip(response): - response = response.encode('utf-8') - orig_len = len(response) - if accept_gzip() and orig_len>512: - response = compressBuf(response) - webnotes._response.headers["Content-Encoding"] = "gzip" - - webnotes._response.headers["Content-Length"] = str(len(response)) - webnotes._response.data = response - -def json_handler(obj): - """serialize non-serializable data for json""" - import datetime - from werkzeug.local import LocalProxy - - # serialize date - if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)): - return unicode(obj) - elif isinstance(obj, LocalProxy): - return unicode(obj) - else: - raise TypeError, """Object of type %s with value of %s is not JSON serializable""" % \ - (type(obj), repr(obj)) - -def accept_gzip(): - if "gzip" in webnotes.get_request_header("HTTP_ACCEPT_ENCODING", ""): - return True - -def compressBuf(buf): - import gzip, cStringIO - zbuf = cStringIO.StringIO() - zfile = gzip.GzipFile(mode = 'wb', fileobj = zbuf, compresslevel = 5) - zfile.write(buf) - zfile.close() - return zbuf.getvalue() diff --git a/webnotes/model/bean.py b/webnotes/model/bean.py index 637e3935d3..2d96e9de23 100644 --- a/webnotes/model/bean.py +++ b/webnotes/model/bean.py @@ -220,17 +220,28 @@ class Bean: idx_map[d.parentfield] = d.idx def run_method(self, method, *args, **kwargs): + if not args: + args = [] self.make_controller() + def add_to_response(out, new_response): + if isinstance(new_response, dict): + out.update(new_response) + if hasattr(self.controller, method): - getattr(self.controller, method)(*args, **kwargs) + add_to_response(webnotes.local.response, webnotes.call(getattr(self.controller, method), *args, **kwargs)) if hasattr(self.controller, 'custom_' + method): - getattr(self.controller, 'custom_' + method)(*args, **kwargs) + add_to_response(webnotes.local.response, webnotes.call(getattr(self.controller, 'custom_' + method), *args, **kwargs)) + + args = [self, method] + args + for handler in webnotes.get_hooks("bean_event:" + self.doc.doctype + ":" + method) \ + + webnotes.get_hooks("bean_event:*:" + method): + add_to_response(webnotes.local.response, webnotes.call(webnotes.get_attr(handler), *args, **kwargs)) - notify(self, method, *args, **kwargs) - self.set_doclist(self.controller.doclist) + return webnotes.local.response + def get_attr(self, method): self.make_controller() return getattr(self.controller, method, None) @@ -273,7 +284,10 @@ class Bean: self.set_doclist(new_doclist) def has_read_perm(self): - return webnotes.has_permission(self.doc.doctype, "read", self.doc) + return self.has_permission("read") + + def has_permission(self, permtype): + return webnotes.has_permission(self.doc.doctype, permtype, self.doc) def save(self, check_links=1, ignore_permissions=None): if ignore_permissions: @@ -494,12 +508,6 @@ def clone(source_wrapper): return new_wrapper -def notify(bean, caller, *args, **kwargs): - for hook in webnotes.get_hooks().bean_event or []: - doctype, trigger, handler = hook.split(":") - if ((doctype=="*") or (doctype==bean.doc.doctype)) and caller==trigger: - webnotes.get_attr(handler)(bean, trigger, *args, **kwargs) - # for bc def getlist(doclist, parentfield): import webnotes.model.utils diff --git a/webnotes/sessions.py b/webnotes/sessions.py index 671a829765..bcb9a56e5f 100644 --- a/webnotes/sessions.py +++ b/webnotes/sessions.py @@ -148,8 +148,7 @@ class Session: data = self.get_session_record() if data: # set language - self.data = webnotes._dict({'data': data, - 'user':data.user, 'sid': self.sid}) + self.data = webnotes._dict({'data': data, 'user':data.user, 'sid': self.sid}) else: self.start_as_guest() diff --git a/webnotes/utils/response.py b/webnotes/utils/response.py new file mode 100644 index 0000000000..4e80d38e3a --- /dev/null +++ b/webnotes/utils/response.py @@ -0,0 +1,102 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import json, inspect +import datetime +import gzip, cStringIO +import webnotes +import webnotes.utils +import webnotes.sessions +import webnotes.model.utils +from werkzeug.local import LocalProxy + +def report_error(status_code): + webnotes.errprint(webnotes.utils.get_traceback()) + webnotes._response.status_code = status_code + if webnotes.request_method == "POST": + webnotes.conn.rollback() + +def build_response(): + print_map = { + 'csv': print_csv, + 'download': print_raw, + 'json': print_json, + 'page': print_page + } + + print_map.get(webnotes.response.get('type'), print_json)() + +def print_page(): + """print web page""" + from webnotes.webutils import render + render(webnotes.response['page_name']) + +def print_json(): + make_logs() + cleanup_docs() + webnotes._response.headers["Content-Type"] = "text/json; charset: utf-8" + print_zip(json.dumps(webnotes.local.response, default=json_handler, separators=(',',':'))) + +def cleanup_docs(): + if webnotes.response.get('docs') and type(webnotes.response['docs'])!=dict: + webnotes.response['docs'] = webnotes.model.utils.compress(webnotes.response['docs']) + +def print_csv(): + webnotes._response.headers["Content-Type"] = \ + "text/csv; charset: utf-8" + webnotes._response.headers["Content-Disposition"] = \ + "attachment; filename=%s.csv" % webnotes.response['doctype'].replace(' ', '_') + webnotes._response.data = webnotes.response['result'] + +def print_raw(): + webnotes._response.headers["Content-Type"] = \ + mimetypes.guess_type(webnotes.response['filename'])[0] or "application/unknown" + webnotes._response.headers["Content-Disposition"] = \ + "filename=%s" % webnotes.response['filename'].replace(' ', '_') + webnotes._response.data = webnotes.response['filecontent'] + +def make_logs(): + """make strings for msgprint and errprint""" + if webnotes.error_log: + # webnotes.response['exc'] = json.dumps("\n".join([cstr(d) for d in webnotes.error_log])) + webnotes.response['exc'] = json.dumps([webnotes.utils.cstr(d) for d in webnotes.local.error_log]) + + if webnotes.local.message_log: + webnotes.response['_server_messages'] = json.dumps([webnotes.utils.cstr(d) for d in webnotes.local.message_log]) + + if webnotes.debug_log and webnotes.conf.get("logging") or False: + webnotes.response['_debug_messages'] = json.dumps(webnotes.local.debug_log) + +def print_zip(response): + response = response.encode('utf-8') + orig_len = len(response) + if accept_gzip() and orig_len>512: + response = compressBuf(response) + webnotes._response.headers["Content-Encoding"] = "gzip" + + webnotes._response.headers["Content-Length"] = str(len(response)) + webnotes._response.data = response + +def json_handler(obj): + """serialize non-serializable data for json""" + + # serialize date + if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)): + return unicode(obj) + elif isinstance(obj, LocalProxy): + return unicode(obj) + else: + raise TypeError, """Object of type %s with value of %s is not JSON serializable""" % \ + (type(obj), repr(obj)) + +def accept_gzip(): + if "gzip" in webnotes.get_request_header("HTTP_ACCEPT_ENCODING", ""): + return True + +def compressBuf(buf): + zbuf = cStringIO.StringIO() + zfile = gzip.GzipFile(mode = 'wb', fileobj = zbuf, compresslevel = 5) + zfile.write(buf) + zfile.close() + return zbuf.getvalue() diff --git a/webnotes/widgets/reportview.py b/webnotes/widgets/reportview.py index 608038b878..fec1641bea 100644 --- a/webnotes/widgets/reportview.py +++ b/webnotes/widgets/reportview.py @@ -29,12 +29,25 @@ def get_form_params(): def execute(doctype, query=None, filters=None, fields=None, docstatus=None, group_by=None, order_by=None, limit_start=0, limit_page_length=None, as_list=False, with_childnames=False, debug=False): + + """ + fields as list ["name", "owner"] or ["tabTask.name", "tabTask.owner"] + filters as list of list [["Task", "name", "=", "TASK00001"]] + """ if query: return run_custom_query(query) - if not filters: filters = [] - if not docstatus: docstatus = [] + if not filters: + filters = [] + if isinstance(filters, basestring): + filters = json.loads(filters) + if not docstatus: + docstatus = [] + if not fields: + fields = ["name"] + if isinstance(fields, basestring): + filters = json.loads(fields) args = prepare_args(doctype, filters, fields, docstatus, group_by, order_by, with_childnames) args.limit = add_limit(limit_start, limit_page_length)