@@ -8,14 +8,33 @@ import frappe.model | |||||
import frappe.utils | import frappe.utils | ||||
import json, os | import json, os | ||||
''' | |||||
Handle RESTful requests that are mapped to the `/api/resource` route. | |||||
Requests via FrappeClient are also handled here. | |||||
''' | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_list(doctype, fields=None, filters=None, order_by=None, | def get_list(doctype, fields=None, filters=None, order_by=None, | ||||
limit_start=None, limit_page_length=20): | limit_start=None, limit_page_length=20): | ||||
'''Returns a list of records by filters, fields, ordering and limit | |||||
:param doctype: DocType of the data to be queried | |||||
:param fields: fields to be returned. Default is `name` | |||||
:param filters: filter list by this dict | |||||
:param order_by: Order by this fieldname | |||||
:param limit_start: Start at this index | |||||
:param limit_page_length: Number of records to be returned (default 20)''' | |||||
return frappe.get_list(doctype, fields=fields, filters=filters, order_by=order_by, | return frappe.get_list(doctype, fields=fields, filters=filters, order_by=order_by, | ||||
limit_start=limit_start, limit_page_length=limit_page_length, ignore_permissions=False) | limit_start=limit_start, limit_page_length=limit_page_length, ignore_permissions=False) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get(doctype, name=None, filters=None): | def get(doctype, name=None, filters=None): | ||||
'''Returns a document by name or filters | |||||
:param doctype: DocType of the document to be returned | |||||
:param name: return document of this `name` | |||||
:param filters: If name is not set, filter by these values and return the first match''' | |||||
if filters and not name: | if filters and not name: | ||||
name = frappe.db.get_value(doctype, json.loads(filters)) | name = frappe.db.get_value(doctype, json.loads(filters)) | ||||
if not name: | if not name: | ||||
@@ -29,6 +48,12 @@ def get(doctype, name=None, filters=None): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False): | def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False): | ||||
'''Returns a value form a document | |||||
:param doctype: DocType to be queried | |||||
:param fieldname: Field to be returned (default `name`) | |||||
:param filters: dict or string for identifying the record''' | |||||
if not frappe.has_permission(doctype): | if not frappe.has_permission(doctype): | ||||
frappe.throw(_("Not permitted"), frappe.PermissionError) | frappe.throw(_("Not permitted"), frappe.PermissionError) | ||||
@@ -80,6 +105,9 @@ def set_value(doctype, name, fieldname, value=None): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def insert(doc=None): | def insert(doc=None): | ||||
'''Insert a document | |||||
:param doc: JSON or dict object to be inserted''' | |||||
if isinstance(doc, basestring): | if isinstance(doc, basestring): | ||||
doc = json.loads(doc) | doc = json.loads(doc) | ||||
@@ -95,6 +123,9 @@ def insert(doc=None): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def insert_many(docs=None): | def insert_many(docs=None): | ||||
'''Insert multiple documents | |||||
:param docs: JSON or list of dict objects to be inserted in one request''' | |||||
if isinstance(docs, basestring): | if isinstance(docs, basestring): | ||||
docs = json.loads(docs) | docs = json.loads(docs) | ||||
@@ -118,6 +149,9 @@ def insert_many(docs=None): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def save(doc): | def save(doc): | ||||
'''Update (save) an existing document | |||||
:param doc: JSON or dict object with the properties of the document to be updated''' | |||||
if isinstance(doc, basestring): | if isinstance(doc, basestring): | ||||
doc = json.loads(doc) | doc = json.loads(doc) | ||||
@@ -126,11 +160,19 @@ def save(doc): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def rename_doc(doctype, old_name, new_name, merge=False): | def rename_doc(doctype, old_name, new_name, merge=False): | ||||
'''Rename document | |||||
:param doctype: DocType of the document to be renamed | |||||
:param old_name: Current `name` of the document to be renamed | |||||
:param new_name: New `name` to be set''' | |||||
new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge) | new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge) | ||||
return new_name | return new_name | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def submit(doc): | def submit(doc): | ||||
'''Submit a document | |||||
:param doc: JSON or dict object to be submitted remotely''' | |||||
if isinstance(doc, basestring): | if isinstance(doc, basestring): | ||||
doc = json.loads(doc) | doc = json.loads(doc) | ||||
@@ -141,6 +183,10 @@ def submit(doc): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def cancel(doctype, name): | def cancel(doctype, name): | ||||
'''Cancel a document | |||||
:param doctype: DocType of the document to be cancelled | |||||
:param name: name of the document to be cancelled''' | |||||
wrapper = frappe.get_doc(doctype, name) | wrapper = frappe.get_doc(doctype, name) | ||||
wrapper.cancel() | wrapper.cancel() | ||||
@@ -148,6 +194,10 @@ def cancel(doctype, name): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def delete(doctype, name): | def delete(doctype, name): | ||||
'''Delete a remote document | |||||
:param doctype: DocType of the document to be deleted | |||||
:param name: name of the document to be deleted''' | |||||
frappe.delete_doc(doctype, name) | frappe.delete_doc(doctype, name) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@@ -158,6 +208,9 @@ def set_default(key, value, parent=None): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def make_width_property_setter(doc): | def make_width_property_setter(doc): | ||||
'''Set width Property Setter | |||||
:param doc: Property Setter document with `width` property''' | |||||
if isinstance(doc, basestring): | if isinstance(doc, basestring): | ||||
doc = json.loads(doc) | doc = json.loads(doc) | ||||
if doc["doctype"]=="Property Setter" and doc["property"]=="width": | if doc["doctype"]=="Property Setter" and doc["property"]=="width": | ||||
@@ -165,6 +218,9 @@ def make_width_property_setter(doc): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def bulk_update(docs): | def bulk_update(docs): | ||||
'''Bulk update documents | |||||
:param docs: JSON list of documents to be updated remotely. Each document must have `docname` property''' | |||||
docs = json.loads(docs) | docs = json.loads(docs) | ||||
failed_docs = [] | failed_docs = [] | ||||
for doc in docs: | for doc in docs: | ||||
@@ -184,17 +240,32 @@ def bulk_update(docs): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def has_permission(doctype, docname, perm_type="read"): | def has_permission(doctype, docname, perm_type="read"): | ||||
'''Returns a JSON with data whether the document has the requested permission | |||||
:param doctype: DocType of the document to be checked | |||||
:param docname: `name` of the document to be checked | |||||
:param perm_type: one of `read`, `write`, `create`, `submit`, `cancel`, `report`. Default is `read`''' | |||||
# perm_type can be one of read, write, create, submit, cancel, report | # perm_type can be one of read, write, create, submit, cancel, report | ||||
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)} | return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)} | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_password(doctype, name, fieldname): | def get_password(doctype, name, fieldname): | ||||
'''Return a password type property. Only applicable for System Managers | |||||
:param doctype: DocType of the document that holds the password | |||||
:param name: `name` of the document that holds the password | |||||
:param fieldname: `fieldname` of the password property | |||||
''' | |||||
frappe.only_for("System Manager") | frappe.only_for("System Manager") | ||||
return frappe.get_doc(doctype, name).get_password(fieldname) | return frappe.get_doc(doctype, name).get_password(fieldname) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_js(items): | def get_js(items): | ||||
'''Load JS code files. Will also append translations | |||||
and extend `frappe._messages` | |||||
:param items: JSON list of paths of the js files to be loaded.''' | |||||
items = json.loads(items) | items = json.loads(items) | ||||
out = [] | out = [] | ||||
for src in items: | for src in items: | ||||
@@ -2,6 +2,13 @@ import requests | |||||
import json | import json | ||||
import frappe | import frappe | ||||
''' | |||||
FrappeClient is a library that helps you connect with other frappe systems | |||||
''' | |||||
class AuthError(Exception): | class AuthError(Exception): | ||||
pass | pass | ||||
@@ -13,7 +20,7 @@ class FrappeClient(object): | |||||
self.verify = verify | self.verify = verify | ||||
self.session = requests.session() | self.session = requests.session() | ||||
self.url = url | self.url = url | ||||
self.login(username, password) | |||||
self._login(username, password) | |||||
def __enter__(self): | def __enter__(self): | ||||
return self | return self | ||||
@@ -21,7 +28,8 @@ class FrappeClient(object): | |||||
def __exit__(self, *args, **kwargs): | def __exit__(self, *args, **kwargs): | ||||
self.logout() | self.logout() | ||||
def login(self, username, password): | |||||
def _login(self, username, password): | |||||
'''Login/start a sesion. Called internally on init''' | |||||
r = self.session.post(self.url, data={ | r = self.session.post(self.url, data={ | ||||
'cmd': 'login', | 'cmd': 'login', | ||||
'usr': username, | 'usr': username, | ||||
@@ -34,6 +42,7 @@ class FrappeClient(object): | |||||
raise AuthError | raise AuthError | ||||
def logout(self): | def logout(self): | ||||
'''Logout session''' | |||||
self.session.get(self.url, params={ | self.session.get(self.url, params={ | ||||
'cmd': 'logout', | 'cmd': 'logout', | ||||
}, verify=self.verify) | }, verify=self.verify) | ||||
@@ -54,41 +63,65 @@ class FrappeClient(object): | |||||
return self.post_process(res) | return self.post_process(res) | ||||
def insert(self, doc): | def insert(self, doc): | ||||
'''Insert a document to the remote server | |||||
:param doc: A dict or Document object to be inserted remotely''' | |||||
res = self.session.post(self.url + "/api/resource/" + doc.get("doctype"), | res = self.session.post(self.url + "/api/resource/" + doc.get("doctype"), | ||||
data={"data":frappe.as_json(doc)}, verify=self.verify) | data={"data":frappe.as_json(doc)}, verify=self.verify) | ||||
return self.post_process(res) | return self.post_process(res) | ||||
def insert_many(self, docs): | def insert_many(self, docs): | ||||
'''Insert multiple documents to the remote server | |||||
:param docs: List of dict or Document objects to be inserted in one request''' | |||||
return self.post_request({ | return self.post_request({ | ||||
"cmd": "frappe.client.insert_many", | "cmd": "frappe.client.insert_many", | ||||
"docs": frappe.as_json(docs) | "docs": frappe.as_json(docs) | ||||
}) | }) | ||||
def update(self, doc): | def update(self, doc): | ||||
'''Update a remote document | |||||
:param doc: dict or Document object to be updated remotely. `name` is mandatory for this''' | |||||
url = self.url + "/api/resource/" + doc.get("doctype") + "/" + doc.get("name") | url = self.url + "/api/resource/" + doc.get("doctype") + "/" + doc.get("name") | ||||
res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify) | res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify) | ||||
return self.post_process(res) | return self.post_process(res) | ||||
def bulk_update(self, docs): | def bulk_update(self, docs): | ||||
'''Bulk update documents remotely | |||||
:param docs: List of dict or Document objects to be updated remotely (by `name`)''' | |||||
return self.post_request({ | return self.post_request({ | ||||
"cmd": "frappe.client.bulk_update", | "cmd": "frappe.client.bulk_update", | ||||
"docs": frappe.as_json(docs) | "docs": frappe.as_json(docs) | ||||
}) | }) | ||||
def delete(self, doctype, name): | def delete(self, doctype, name): | ||||
'''Delete remote document by name | |||||
:param doctype: `doctype` to be deleted | |||||
:param name: `name` of document to be deleted''' | |||||
return self.post_request({ | return self.post_request({ | ||||
"cmd": "frappe.model.delete_doc", | "cmd": "frappe.model.delete_doc", | ||||
"doctype": doctype, | "doctype": doctype, | ||||
"name": name | "name": name | ||||
}) | }) | ||||
def submit(self, doclist): | |||||
def submit(self, doc): | |||||
'''Submit remote document | |||||
:param doc: dict or Document object to be submitted remotely''' | |||||
return self.post_request({ | return self.post_request({ | ||||
"cmd": "frappe.client.submit", | "cmd": "frappe.client.submit", | ||||
"doclist": frappe.as_json(doclist) | |||||
"doc": frappe.as_json(doc) | |||||
}) | }) | ||||
def get_value(self, doctype, fieldname=None, filters=None): | def get_value(self, doctype, fieldname=None, filters=None): | ||||
'''Returns a value form a document | |||||
:param doctype: DocType to be queried | |||||
:param fieldname: Field to be returned (default `name`) | |||||
:param filters: dict or string for identifying the record''' | |||||
return self.get_request({ | return self.get_request({ | ||||
"cmd": "frappe.client.get_value", | "cmd": "frappe.client.get_value", | ||||
"doctype": doctype, | "doctype": doctype, | ||||
@@ -97,6 +130,12 @@ class FrappeClient(object): | |||||
}) | }) | ||||
def set_value(self, doctype, docname, fieldname, value): | def set_value(self, doctype, docname, fieldname, value): | ||||
'''Set a value in a remote document | |||||
:param doctype: DocType of the document to be updated | |||||
:param docname: name of the document to be updated | |||||
:param fieldname: fieldname of the document to be updated | |||||
:param value: value to be updated''' | |||||
return self.post_request({ | return self.post_request({ | ||||
"cmd": "frappe.client.set_value", | "cmd": "frappe.client.set_value", | ||||
"doctype": doctype, | "doctype": doctype, | ||||
@@ -106,6 +145,10 @@ class FrappeClient(object): | |||||
}) | }) | ||||
def cancel(self, doctype, name): | def cancel(self, doctype, name): | ||||
'''Cancel a remote document | |||||
:param doctype: DocType of the document to be cancelled | |||||
:param name: name of the document to be cancelled''' | |||||
return self.post_request({ | return self.post_request({ | ||||
"cmd": "frappe.client.cancel", | "cmd": "frappe.client.cancel", | ||||
"doctype": doctype, | "doctype": doctype, | ||||
@@ -113,6 +156,12 @@ class FrappeClient(object): | |||||
}) | }) | ||||
def get_doc(self, doctype, name="", filters=None, fields=None): | def get_doc(self, doctype, name="", filters=None, fields=None): | ||||
'''Returns a single remote document | |||||
:param doctype: DocType of the document to be returned | |||||
:param name: (optional) `name` of the document to be returned | |||||
:param filters: (optional) Filter by this dict if name is not set | |||||
:param fields: (optional) Fields to be returned, will return everythign if not set''' | |||||
params = {} | params = {} | ||||
if filters: | if filters: | ||||
params["filters"] = json.dumps(filters) | params["filters"] = json.dumps(filters) | ||||
@@ -125,6 +174,11 @@ class FrappeClient(object): | |||||
return self.post_process(res) | return self.post_process(res) | ||||
def rename_doc(self, doctype, old_name, new_name): | def rename_doc(self, doctype, old_name, new_name): | ||||
'''Rename remote document | |||||
:param doctype: DocType of the document to be renamed | |||||
:param old_name: Current `name` of the document to be renamed | |||||
:param new_name: New `name` to be set''' | |||||
params = { | params = { | ||||
"cmd": "frappe.client.rename_doc", | "cmd": "frappe.client.rename_doc", | ||||
"doctype": doctype, | "doctype": doctype, | ||||
@@ -137,10 +137,21 @@ def parse(docs): | |||||
def strip_leading_tabs(docs): | def strip_leading_tabs(docs): | ||||
"""Strip leading tabs from __doc__ text.""" | """Strip leading tabs from __doc__ text.""" | ||||
lines = docs.splitlines() | lines = docs.splitlines() | ||||
# remove empty lines in the front | |||||
start = 0 | |||||
for line in lines: | |||||
if line != '': break | |||||
start += 1 | |||||
if start: | |||||
lines = lines[start:] | |||||
# remove default indentation | |||||
if len(lines) > 1: | if len(lines) > 1: | ||||
start_line = 1 | start_line = 1 | ||||
ref_line = lines[start_line] | ref_line = lines[start_line] | ||||
while not ref_line: | while not ref_line: | ||||
# find reference line for indentations (the first line that is nonempty (false)) | |||||
start_line += 1 | start_line += 1 | ||||
if start_line > len(lines): break | if start_line > len(lines): break | ||||
ref_line = lines[start_line] | ref_line = lines[start_line] | ||||