PageRenderTime 743ms CodeModel.GetById 23ms RepoModel.GetById 2ms app.codeStats 1ms

/restfulOpenErpProxy.py

https://github.com/dh-benamor/restful-openerp
Python | 833 lines | 752 code | 41 blank | 40 comment | 36 complexity | 789c6e6cb347c8b4b87797758c790837 MD5 | raw file
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. # (C) 2012 Tobias G. Pfeiffer <tgpfeiffer@web.de>
  4. # This program is free software; you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License version 3 as published by
  6. # the Free Software Foundation.
  7. import sys, xmlrpclib, ConfigParser, datetime, dateutil.tz, inspect, re
  8. from xml.sax.saxutils import escape as xmlescape
  9. from lxml import etree
  10. from twisted.web.server import Site, NOT_DONE_YET
  11. from twisted.web.resource import ErrorPage, Resource
  12. from twisted.internet import reactor, task
  13. from twisted.python import log
  14. from twisted.web.xmlrpc import Proxy
  15. import pyatom
  16. def hello():
  17. """If you wanted, you could log some message from here to understand the
  18. call stack a bit better..."""
  19. stack = inspect.stack()
  20. parent = stack[1][3]
  21. #print parent
  22. def localTimeStringToUtcDatetime(s):
  23. # get local and UTC timezone to convert the time stamps
  24. tz=dateutil.tz.tzlocal()
  25. utc=dateutil.tz.tzutc()
  26. t = datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S.%f') # this time is in local tz
  27. t_withtz = t.replace(tzinfo=tz)
  28. return t_withtz.astimezone(utc)
  29. def httpdate(dt):
  30. return dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
  31. class UnauthorizedPage(ErrorPage):
  32. def __init__(self):
  33. ErrorPage.__init__(self, 401, "Unauthorized", "Use HTTP Basic Authentication!")
  34. def render(self, request):
  35. r = ErrorPage.render(self, request)
  36. request.setHeader("WWW-Authenticate", 'Basic realm="OpenERP"')
  37. return r
  38. class OpenErpDispatcher(Resource, object):
  39. def __init__(self, openerpUrl):
  40. Resource.__init__(self)
  41. self.databases = {}
  42. self.openerpUrl = openerpUrl
  43. log.msg("Server starting up with backend: " + self.openerpUrl)
  44. # @override http://twistedmatrix.com/documents/10.0.0/api/twisted.web.resource.Resource.html#getChildWithDefault
  45. def getChildWithDefault(self, pathElement, request):
  46. """Ensure that we have HTTP Basic Auth."""
  47. if not (request.getUser() and request.getPassword()):
  48. return UnauthorizedPage()
  49. else:
  50. return super(OpenErpDispatcher, self).getChildWithDefault(pathElement, request)
  51. # @override http://twistedmatrix.com/documents/10.0.0/api/twisted.web.resource.Resource.html#getChild
  52. def getChild(self, path, request):
  53. """Return a resource for the correct database."""
  54. if self.databases.has_key(path):
  55. return self.databases[path]
  56. else:
  57. log.msg("Creating resource for '%s' database." % path)
  58. self.databases[path] = OpenErpDbResource(self.openerpUrl, path)
  59. return self.databases[path]
  60. class OpenErpDbResource(Resource):
  61. """This is accessed when going to /{database}."""
  62. def __init__(self, openerpUrl, dbname):
  63. Resource.__init__(self)
  64. self.openerpUrl = openerpUrl
  65. self.dbname = dbname
  66. self.models = {}
  67. # @override http://twistedmatrix.com/documents/10.0.0/api/twisted.web.resource.Resource.html#getChild
  68. def getChild(self, path, request):
  69. if self.models.has_key(path):
  70. return self.models[path]
  71. else:
  72. log.msg("Creating resource for '%s' model." % path)
  73. self.models[path] = OpenErpModelResource(self.openerpUrl, self.dbname, path)
  74. return self.models[path]
  75. class OpenErpModelResource(Resource):
  76. isLeaf = True
  77. """This is accessed when going to /{database}/{model}."""
  78. def __init__(self, openerpUrl, dbname, model):
  79. Resource.__init__(self)
  80. self.openerpUrl = openerpUrl
  81. self.dbname = dbname
  82. self.model = model
  83. self.desc = {}
  84. self.workflowDesc = []
  85. self.defaults = {}
  86. # clear self.desc and self.default every two hours
  87. self.cleanUpTask = task.LoopingCall(self.clearCachedValues)
  88. self.cleanUpTask.start(60 * 60 * 2)
  89. def clearCachedValues(self):
  90. log.msg("clearing schema/default cache for "+self.model)
  91. self.desc = {}
  92. self.defaults = {}
  93. ### list items of a collection
  94. def __getCollection(self, uid, request, pwd):
  95. """This is called after successful login to list the items
  96. of a certain collection, e.g. all res.partners."""
  97. hello()
  98. params = []
  99. for key, vals in request.args.iteritems():
  100. if not self.desc.has_key(key) and key != "id":
  101. raise InvalidParameter("field '%s' not present in model '%s'" % (key, self.model))
  102. else:
  103. params.append((key, '=', vals[0]))
  104. proxy = Proxy(self.openerpUrl + 'object')
  105. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, 'search', params)
  106. d.addCallback(self.__handleCollectionAnswer, request, uid, pwd)
  107. return d
  108. def __handleCollectionAnswer(self, val, request, uid, pwd):
  109. hello()
  110. def createFeed(items, request):
  111. # build a feed
  112. # TODO: add the feed url; will currently break the test
  113. feed = pyatom.AtomFeed(title=self.model+" items",
  114. id=str(request.URLPath()),
  115. #feed_url=str(request.URLPath())
  116. )
  117. for item in items:
  118. if not item['name']:
  119. item['name'] = "None"
  120. if item.has_key('user_id') and item['user_id']:
  121. feed.add(title=item['name'],
  122. url="%s/%s" % (request.URLPath(), item['id']),
  123. updated=localTimeStringToUtcDatetime(item['__last_update']),
  124. author=[{'name': item['user_id'][1]}])
  125. else:
  126. feed.add(title=item['name'],
  127. url="%s/%s" % (request.URLPath(), item['id']),
  128. updated=localTimeStringToUtcDatetime(item['__last_update']),
  129. author=[{'name': 'None'}])
  130. request.setHeader("Content-Type", "application/atom+xml")
  131. request.write(str(feed.to_string().encode('utf-8')))
  132. request.finish()
  133. proxy = Proxy(self.openerpUrl + 'object')
  134. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, 'read', val, ['name', '__last_update', 'user_id'])
  135. d.addCallback(createFeed, request)
  136. return d
  137. ### get __last_update of a collection item
  138. def __getLastItemUpdate(self, uid, request, pwd, modelId):
  139. hello()
  140. # make sure we're dealing with an integer id
  141. try:
  142. modelId = int(modelId)
  143. except:
  144. modelId = -1
  145. proxy = Proxy(self.openerpUrl + 'object')
  146. def handleLastItemUpdateAnswer(updateAnswer):
  147. return (uid, updateAnswer[0]['__last_update'])
  148. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, 'read', [modelId], ['__last_update'])
  149. d.addCallback(handleLastItemUpdateAnswer)
  150. return d
  151. ### list the default values for an item
  152. def __updateDefaults(self, uid, pwd):
  153. hello()
  154. if not self.defaults:
  155. # update type description
  156. proxy = Proxy(self.openerpUrl + 'object')
  157. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, 'default_get', self.desc.keys(), {})
  158. d.addCallback(self.__handleDefaultsAnswer, uid)
  159. return d
  160. else:
  161. return uid
  162. def __handleDefaultsAnswer(self, val, uid):
  163. hello()
  164. log.msg("updating default values for "+self.model)
  165. self.defaults = val
  166. return uid
  167. def __getItemDefaults(self, uid, request, pwd):
  168. hello()
  169. # set correct headers
  170. request.setHeader("Content-Type", "application/atom+xml")
  171. # compose answer
  172. request.write(self.__mkDefaultXml(str(request.URLPath()), self.desc, self.defaults))
  173. request.finish()
  174. def __mkDefaultXml(self, path, desc, item):
  175. xml = '''<?xml version="1.0" encoding="utf-8"?>
  176. <entry xmlns="http://www.w3.org/2005/Atom">
  177. <title type="text">Defaults for %s</title>
  178. <id>%s</id>
  179. <updated>%s</updated>
  180. <link href="%s" rel="self" />
  181. <author>
  182. <name>%s</name>
  183. </author>
  184. <content type="application/vnd.openerp+xml">
  185. <%s xmlns="%s">
  186. <id />
  187. ''' % (self.model,
  188. path+"/defaults",
  189. datetime.datetime.utcnow().isoformat()[:-7]+'Z',
  190. path+"/defaults",
  191. 'None',
  192. self.model.replace('.', '_'),
  193. '/'.join(path.split("/") + ["schema"]),
  194. )
  195. # loop over the fields of the current object
  196. for key in desc.iterkeys():
  197. value = item.has_key(key) and item[key] or ""
  198. # key is the name of the field, value is the content,
  199. # e.g. key="email", value="me@privacy.net"
  200. fieldtype = desc[key]['type']
  201. # if we have an empty field, we display a closed tag
  202. # (except if this is a boolean field)
  203. if not value and fieldtype in ('many2one', 'one2many', 'many2many'):
  204. xml += (" <%s type='%s' relation='%s' />\n" % (
  205. key,
  206. fieldtype,
  207. '/'.join(path.split("/")[:-1] + [self.desc[key]["relation"]]))
  208. )
  209. elif not value and fieldtype != "boolean":
  210. xml += (" <%s type='%s' />\n" % (
  211. key,
  212. fieldtype)
  213. )
  214. # display URIs for many2one fields
  215. elif fieldtype == 'many2one':
  216. xml += (" <%s type='%s' relation='%s'>\n <link href='%s' />\n </%s>\n" % (
  217. key,
  218. fieldtype,
  219. '/'.join(path.split("/")[:-1] + [self.desc[key]["relation"]]),
  220. '/'.join(path.split("/")[:-1] + [self.desc[key]["relation"], str(value)]),
  221. key)
  222. )
  223. # display URIs for *2many fields, wrapped by <item>
  224. elif fieldtype in ('one2many', 'many2many'):
  225. xml += (" <%s type='%s' relation='%s'>%s</%s>\n" % (
  226. key,
  227. fieldtype,
  228. '/'.join(path.split("/")[:-1] + [self.desc[key]["relation"]]),
  229. ''.join(
  230. ['\n <link href="' + '/'.join(path.split("/")[:-1] + [desc[key]["relation"], str(v)]) + '" />' for v in value]
  231. ) + '\n ',
  232. key)
  233. )
  234. # for other fields, just output the data
  235. elif fieldtype == 'boolean':
  236. xml += (" <%s type='%s'>%s</%s>\n" % (
  237. key,
  238. fieldtype,
  239. xmlescape(str(value and "True" or "False")),
  240. key)
  241. )
  242. else:
  243. xml += (" <%s type='%s'>%s</%s>\n" % (
  244. key,
  245. fieldtype,
  246. xmlescape(str(value)),
  247. key)
  248. )
  249. xml += (" </%s>\n </content>\n</entry>" % self.model.replace('.', '_'))
  250. return xml
  251. ### list one particular item of a collection
  252. def getParamsFromRequest(self, request):
  253. params = {}
  254. for key, vals in request.args.iteritems():
  255. try:
  256. val = int(vals[0])
  257. except:
  258. val = vals[0]
  259. params[key] = val
  260. if key == "active_id":
  261. params["active_ids"] = [val]
  262. return params
  263. def __getItem(self, (uid, updateTime), request, pwd, modelId):
  264. hello()
  265. # make sure we're dealing with an integer id
  266. try:
  267. modelId = int(modelId)
  268. except:
  269. modelId = -1
  270. # we add 'context' parameters, like 'lang' or 'tz'
  271. params = self.getParamsFromRequest(request)
  272. # issue the request
  273. proxy = Proxy(self.openerpUrl + 'object')
  274. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, 'read', [modelId], [], params)
  275. d.addCallback(self.__handleItemAnswer, request, localTimeStringToUtcDatetime(updateTime))
  276. return d
  277. def __handleItemAnswer(self, val, request, lastModified):
  278. hello()
  279. # val should be a one-element-list with a dictionary describing the current object
  280. try:
  281. item = val[0]
  282. except IndexError:
  283. request.setResponseCode(404)
  284. request.write("No such resource.")
  285. request.finish()
  286. return
  287. # set correct headers
  288. request.setHeader("Last-Modified", httpdate(lastModified))
  289. request.setHeader("Content-Type", "application/atom+xml")
  290. # compose answer
  291. path = str(request.URLPath())+"/"+str(item['id'])
  292. xmlHead = u'''<?xml version="1.0" encoding="utf-8"?>
  293. <entry xmlns="http://www.w3.org/2005/Atom">
  294. <title type="text">%s</title>
  295. <id>%s</id>
  296. <updated>%s</updated>
  297. <link href="%s" rel="self" />
  298. <author>
  299. <name>%s</name>
  300. </author>
  301. <content type="application/vnd.openerp+xml">
  302. <%s xmlns="%s">
  303. ''' % (item.has_key('name') and item['name'] or "None",
  304. path,
  305. lastModified.isoformat()[:-13]+'Z',
  306. path,
  307. 'None', # TODO: insert author, if present
  308. self.model.replace('.', '_'),
  309. '/'.join(str(request.URLPath()).split("/") + ["schema"]),
  310. )
  311. request.write(xmlHead.encode('utf-8'))
  312. # loop over the fields of the current object
  313. for key, value in item.iteritems():
  314. # key is the name of the field, value is the content,
  315. # e.g. key="email", value="me@privacy.net"
  316. if self.desc.has_key(key):
  317. fieldtype = self.desc[key]['type']
  318. # if we have an empty field, we display a closed tag
  319. # (except if this is a boolean field)
  320. if not value and fieldtype in ('many2one', 'one2many', 'many2many'):
  321. request.write(" <%s type='%s' relation='%s'><!-- %s --></%s>\n" % (
  322. key,
  323. fieldtype,
  324. '/'.join(str(request.URLPath()).split("/")[:-1] + [self.desc[key]["relation"]]),
  325. value,
  326. key)
  327. )
  328. elif not value and fieldtype != "boolean":
  329. request.write(" <%s type='%s'><!-- %s --></%s>\n" % (
  330. key,
  331. fieldtype,
  332. value,
  333. key)
  334. )
  335. # display URIs for many2one fields
  336. elif fieldtype == 'many2one':
  337. request.write(" <%s type='%s' relation='%s'>\n <link href='%s' />\n </%s>\n" % (
  338. key,
  339. fieldtype,
  340. '/'.join(str(request.URLPath()).split("/")[:-1] + [self.desc[key]["relation"]]),
  341. '/'.join(str(request.URLPath()).split("/")[:-1] + [self.desc[key]["relation"], str(value[0])]),
  342. key)
  343. )
  344. # display URIs for *2many fields, wrapped by <item>
  345. elif fieldtype in ('one2many', 'many2many'):
  346. request.write(" <%s type='%s' relation='%s'>%s</%s>\n" % (
  347. key,
  348. fieldtype,
  349. '/'.join(str(request.URLPath()).split("/")[:-1] + [self.desc[key]["relation"]]),
  350. ''.join(
  351. ['\n <link href="' + '/'.join(str(request.URLPath()).split("/")[:-1] + [self.desc[key]["relation"], str(v)]) + '" />' for v in value]
  352. ) + '\n ',
  353. key)
  354. )
  355. # for other fields, just output the data
  356. else:
  357. request.write(" <%s type='%s'>%s</%s>\n" % (
  358. key,
  359. fieldtype,
  360. xmlescape(unicode(value).encode('utf-8')),
  361. key)
  362. )
  363. else: # no type given or no self.desc present
  364. request.write(" <%s>%s</%s>\n" % (
  365. key,
  366. xmlescape(unicode(value).encode('utf-8')),
  367. key)
  368. )
  369. request.write(" </%s>\n" % self.model.replace('.', '_'))
  370. for button in self.workflowDesc:
  371. if button.attrib.has_key("name") and \
  372. (not item.has_key("state") or not button.attrib.has_key("states") or item["state"] in button.attrib['states'].split(",")) \
  373. and not self.__is_number(button.attrib["name"]):
  374. request.write(" <link rel='%s' href='%s' title='%s' />\n" % \
  375. (button.attrib['name'], path+"/"+button.attrib['name'], button.attrib['string']))
  376. request.write(" </content>\n</entry>")
  377. request.finish()
  378. def __is_number(self, n):
  379. try:
  380. x = int(n)
  381. return True
  382. except:
  383. return False
  384. ### handle inserts into collection
  385. def __addToCollection(self, uid, request, pwd):
  386. """This is called after successful login to add an items
  387. to a certain collection, e.g. a new res.partner."""
  388. hello()
  389. if not self.desc:
  390. raise xmlrpclib.Fault("warning -- Object Error", "no such collection")
  391. # check whether we got well-formed XML
  392. parser = etree.XMLParser(remove_comments=True)
  393. try:
  394. doc = etree.fromstring(request.content.read(), parser=parser)
  395. except Exception as e:
  396. request.setResponseCode(400)
  397. request.write("malformed XML: "+str(e))
  398. request.finish()
  399. return
  400. # check whether we got valid XML with the given schema
  401. ns = str(request.URLPath()) + "/schema"
  402. schemaxml = self.__desc2relaxNG(str(request.URLPath()), self.desc)
  403. schema = etree.fromstring(schemaxml)
  404. relaxng = etree.RelaxNG(schema)
  405. # to validate doc, we need to set "id" to a numeric value
  406. try:
  407. doc.find("{%s}id" % ns).text = "-1"
  408. except:
  409. pass
  410. if not relaxng.validate(doc):
  411. request.setResponseCode(400)
  412. err = relaxng.error_log
  413. request.write("invalid XML:\n"+str(err))
  414. request.finish()
  415. return
  416. # get default values for this model
  417. defaultDocRoot = etree.fromstring(self.__mkDefaultXml(str(request.URLPath()), self.desc, self.defaults), parser=parser)
  418. defaultDoc = defaultDocRoot.find("{http://www.w3.org/2005/Atom}content").find("{%s}%s" % (ns, self.model.replace(".", "_")))
  419. stripNsRe = re.compile(r'^{%s}(.+)$' % ns)
  420. whitespaceRe = re.compile(r'\s+')
  421. # collect all fields with non-default values
  422. fields = {}
  423. for c in doc.getchildren():
  424. if c.tag == "{%s}id" % ns or c.tag == "{%s}create_date" % ns:
  425. # will not update id or create_date
  426. continue
  427. elif whitespaceRe.sub(" ", etree.tostring(c, pretty_print=True).strip()) == whitespaceRe.sub(" ", etree.tostring(defaultDoc.find(c.tag), pretty_print=True).strip()):
  428. # c has default value
  429. continue
  430. # we can assume the regex will match due to validation beforehand
  431. tagname = stripNsRe.search(c.tag).group(1)
  432. if c.attrib["type"] in ("char", "selection", "text", "datetime"):
  433. fields[tagname] = c.text
  434. elif c.attrib["type"] == "float":
  435. fields[tagname] = float(c.text)
  436. elif c.attrib["type"] == "integer":
  437. fields[tagname] = int(c.text)
  438. elif c.attrib["type"] == "boolean":
  439. fields[tagname] = (c.text == "True")
  440. elif c.attrib["type"] == "many2one":
  441. assert c.attrib['relation'] == defaultDoc.find(c.tag).attrib['relation']
  442. uris = [link.attrib['href'] for link in c.getchildren()]
  443. ids = [int(u[u.rfind('/')+1:]) for u in uris if u.startswith(c.attrib['relation'])]
  444. if ids:
  445. fields[tagname] = ids[0]
  446. elif c.attrib["type"] in ("many2many", "one2many"):
  447. assert c.attrib['relation'] == defaultDoc.find(c.tag).attrib['relation']
  448. uris = [link.attrib['href'] for link in c.getchildren()]
  449. ids = [int(u[u.rfind('/')+1:]) for u in uris if u.startswith(c.attrib['relation'])]
  450. if ids:
  451. fields[tagname] = [(6, 0, ids)]
  452. else:
  453. # TODO: date, many2one (we can't really set many2many and one2many here, can we?)
  454. raise NotImplementedError("don't know how to handle element "+c.tag+" of type "+c.attrib["type"])
  455. # compose the XML-RPC call from them
  456. proxy = Proxy(self.openerpUrl + 'object')
  457. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, 'create', fields)
  458. d.addCallback(self.__handleAddCollectionAnswer, request)
  459. return d
  460. def __handleAddCollectionAnswer(self, object_id, request):
  461. hello()
  462. loc = str(request.URLPath()) + "/" + str(object_id)
  463. request.setResponseCode(201)
  464. request.setHeader("Location", loc)
  465. request.finish()
  466. ### handle workflows
  467. def __prepareWorkflow(self, uid, request, pwd, modelId, workflow):
  468. hello()
  469. modelId = int(modelId)
  470. # first, get information about the item
  471. proxy = Proxy(self.openerpUrl + 'object')
  472. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, 'read', [modelId], [])
  473. d.addCallback(self.__executeWorkflow, uid, request, pwd, modelId, workflow)
  474. return d
  475. def __executeWorkflow(self, val, uid, request, pwd, modelId, workflow):
  476. hello()
  477. # val should be a one-element-list with a dictionary describing the current object
  478. try:
  479. item = val[0]
  480. except IndexError:
  481. request.setResponseCode(404)
  482. request.write("No such resource.")
  483. request.finish()
  484. return
  485. # also, the given workflow should be valid for the current state
  486. for button in self.workflowDesc:
  487. if button.attrib.has_key("name") and \
  488. (not item.has_key("state") or item["state"] in button.attrib['states'].split(",")) \
  489. and not self.__is_number(button.attrib["name"]) and workflow == button.attrib['name']:
  490. currentAction = button
  491. break
  492. else:
  493. request.setResponseCode(400)
  494. request.write("Workflow '%s' not allowed in state '%s'." % \
  495. (workflow, (item.has_key("state") and item["state"]) or ''))
  496. request.finish()
  497. return
  498. # here, the workflow is allowed for the current object
  499. if currentAction.attrib.has_key("type") and currentAction.attrib['type'] == "object":
  500. # get a URL from the POST body and extract model and id
  501. myPath = str(request.URLPath())
  502. objRe = re.compile(myPath[:myPath.find(self.model)] + r'(.+)/([0-9]+)$')
  503. body = request.content.read()
  504. match = objRe.match(body)
  505. if not match:
  506. raise NotImplementedError("don't know how to handle input '%s' for workflow '%s'" % (body, workflow))
  507. # set parameters fro request
  508. params = {"active_model": match.group(1), "active_id": int(match.group(2)), "active_ids": [int(match.group(2))]}
  509. proxy = Proxy(self.openerpUrl + 'object')
  510. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, workflow, [modelId], params)
  511. d.addCallback(self.__handleWorkflowAnswer, request, modelId, workflow)
  512. return d
  513. elif currentAction.attrib.has_key("type"):
  514. raise NotImplementedError("don't know how to handle workflow '%s'" % workflow)
  515. proxy = Proxy(self.openerpUrl + 'object')
  516. d = proxy.callRemote('exec_workflow', self.dbname, uid, pwd, self.model, workflow, modelId)
  517. d.addCallback(self.__handleWorkflowAnswer, request, modelId, workflow)
  518. return d
  519. def __handleWorkflowAnswer(self, result, request, modelId, workflow):
  520. request.setResponseCode(204)
  521. loc = str(request.URLPath()) + "/" + str(modelId)
  522. request.setHeader("Location", loc)
  523. request.finish()
  524. ### handle login
  525. def __handleLoginAnswer(self, uid):
  526. hello()
  527. if not uid:
  528. raise xmlrpclib.Fault("AccessDenied", "login failed")
  529. else:
  530. return uid
  531. ### update the model information
  532. def __updateTypedesc(self, uid, pwd):
  533. hello()
  534. if not self.desc:
  535. # update type description
  536. proxy = Proxy(self.openerpUrl + 'object')
  537. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, 'fields_get', [])
  538. d.addCallback(self.__handleTypedescAnswer, uid)
  539. d.addErrback(self.__handleTypedescError, uid)
  540. return d
  541. else:
  542. return uid
  543. def __handleTypedescAnswer(self, val, uid):
  544. hello()
  545. log.msg("updating schema for "+self.model)
  546. if val.has_key("id"):
  547. del val["id"]
  548. self.desc = val
  549. return uid
  550. def __handleTypedescError(self, err, uid):
  551. hello()
  552. # if an error appears while updating the type description
  553. return uid
  554. def __updateWorkflowDesc(self, uid, pwd):
  555. hello()
  556. if not self.workflowDesc:
  557. # update type description
  558. proxy = Proxy(self.openerpUrl + 'object')
  559. d = proxy.callRemote('execute', self.dbname, uid, pwd, self.model, 'fields_view_get', [])
  560. d.addCallback(self.__handleWorkflowDescAnswer, uid)
  561. d.addErrback(self.__handleWorkflowDescError, uid)
  562. return d
  563. else:
  564. return uid
  565. def __handleWorkflowDescAnswer(self, val, uid):
  566. hello()
  567. log.msg("updating workflow description for "+self.model)
  568. self.workflowDesc = etree.fromstring(val['arch']).findall(".//button")
  569. return uid
  570. def __handleWorkflowDescError(self, err, uid):
  571. hello()
  572. # if an error appears while updating the type description
  573. return uid
  574. def __desc2relaxNG(self, path, desc):
  575. ns = path + "/schema"
  576. xml = '''<?xml version="1.0" encoding="utf-8"?>
  577. <element name="%s" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes" ns="%s">
  578. <interleave>
  579. <element name="id"><data type="decimal" /></element>
  580. ''' % (self.model.replace(".", "_"), ns)
  581. for key, val in desc.iteritems():
  582. fieldtype = val['type']
  583. required = val.has_key('required') and val['required'] or False
  584. xml += (' <element name="%s">\n <attribute name="type" />' % key)
  585. if fieldtype in ('many2one', 'many2many', 'one2many'):
  586. xml += '\n <attribute name="relation" />'
  587. if fieldtype in ('many2many', 'one2many'):
  588. elemName = required and "oneOrMore" or "zeroOrMore"
  589. xml += ('\n <%s><element name="link"><attribute name="href" /></element></%s>\n ' % (elemName, elemName))
  590. else:
  591. output = "\n "
  592. # select the correct field type
  593. if fieldtype == "many2one":
  594. s = '<element name="link"><attribute name="href" /></element>'
  595. output += required and s or "<optional>"+s+"</optional>"
  596. elif fieldtype == "float":
  597. s = '<data type="double" />'
  598. output += required and s or "<optional>"+s+"</optional>"
  599. elif fieldtype == "boolean":
  600. s = '<choice><value>True</value><value>False</value></choice>'
  601. output += required and s or "<optional>"+s+"</optional>"
  602. elif fieldtype == "integer":
  603. s = '<data type="decimal" />'
  604. output += required and s or "<optional>"+s+"</optional>"
  605. else:
  606. s = required and '<data type="string"><param name="minLength">1</param></data>' or \
  607. "<optional><text /></optional>"
  608. output += s
  609. xml += (output+'\n ')
  610. xml += '</element>\n'
  611. xml += '</interleave>\n</element>'
  612. return xml
  613. def __getSchema(self, uid, request):
  614. hello()
  615. if not self.desc:
  616. request.setResponseCode(404)
  617. request.write("Schema description not found")
  618. request.finish()
  619. return
  620. else:
  621. request.write(self.__desc2relaxNG(str(request.URLPath()), self.desc))
  622. request.finish()
  623. ### error handling
  624. def __cleanup(self, err, request):
  625. hello()
  626. log.msg("cleanup: "+str(err))
  627. request.setHeader("Content-Type", "text/plain")
  628. e = err.value
  629. if err.check(xmlrpclib.Fault):
  630. if e.faultCode == "AccessDenied":
  631. request.setResponseCode(403)
  632. request.write("Bad credentials.")
  633. elif e.faultCode.startswith("warning -- AccessError") or e.faultCode.startswith("warning -- ZugrifffFehler"):
  634. # oh good, OpenERP spelling goodness...
  635. request.setResponseCode(404)
  636. request.write("No such resource.")
  637. elif e.faultCode.startswith("warning -- Object Error"):
  638. request.setResponseCode(404)
  639. request.write("No such collection.")
  640. else:
  641. request.setResponseCode(500)
  642. request.write("An XML-RPC error occured:\n"+e.faultCode.encode("utf-8"))
  643. elif e.__class__ in (InvalidParameter, PostNotPossible, NoChildResources):
  644. request.setResponseCode(e.code)
  645. request.write(str(e))
  646. else:
  647. request.setResponseCode(500)
  648. request.write("An error occured:\n"+str(e))
  649. request.finish()
  650. def __raiseAnError(self, *params):
  651. """This function is necessary as errors are only caught by errbacks
  652. if they are thrown from within callbacks, not directly from render_GET.
  653. It only throws the given exception."""
  654. e = params[-1]
  655. raise e
  656. ### HTTP request handling
  657. def render_GET(self, request):
  658. hello()
  659. user = request.getUser()
  660. pwd = request.getPassword()
  661. # login to OpenERP
  662. proxyCommon = Proxy(self.openerpUrl + 'common')
  663. d = proxyCommon.callRemote('login', self.dbname, user, pwd)
  664. d.addCallback(self.__handleLoginAnswer)
  665. d.addCallback(self.__updateTypedesc, pwd)
  666. d.addCallback(self.__updateWorkflowDesc, pwd)
  667. d.addCallback(self.__updateDefaults, pwd)
  668. # if uri is sth. like /[dbname]/res.partner,
  669. # give a list of all objects in this collection:
  670. if not request.postpath:
  671. d.addCallback(self.__getCollection, request, pwd)
  672. # if URI is sth. like /[dbname]/res.partner/schema,
  673. # list this particular schema
  674. elif len(request.postpath) == 1 and request.postpath[0] == "schema":
  675. d.addCallback(self.__getSchema, request)
  676. # if URI is sth. like /[dbname]/res.partner/defaults,
  677. # list this particular schema
  678. elif len(request.postpath) == 1 and request.postpath[0] == "defaults":
  679. d.addCallback(self.__getItemDefaults, request, pwd)
  680. # if URI is sth. like /[dbname]/res.partner/7,
  681. # list this particular item
  682. elif len(request.postpath) == 1:
  683. d.addCallback(self.__getLastItemUpdate, request, pwd, request.postpath[0])
  684. d.addCallback(self.__getItem, request, pwd, request.postpath[0])
  685. # if URI is sth. like /[dbname]/res.partner/7/something,
  686. # return 404
  687. else: # len(request.postpath) > 1
  688. d.addCallback(self.__raiseAnError,
  689. NoChildResources("/" + '/'.join([self.dbname, self.model, request.postpath[0]])))
  690. d.addErrback(self.__cleanup, request)
  691. return NOT_DONE_YET
  692. def render_POST(self, request):
  693. hello()
  694. user = request.getUser()
  695. pwd = request.getPassword()
  696. # login to OpenERP
  697. proxyCommon = Proxy(self.openerpUrl + 'common')
  698. d = proxyCommon.callRemote('login', self.dbname, user, pwd)
  699. d.addCallback(self.__handleLoginAnswer)
  700. d.addCallback(self.__updateTypedesc, pwd)
  701. d.addCallback(self.__updateWorkflowDesc, pwd)
  702. d.addCallback(self.__updateDefaults, pwd)
  703. # if uri is sth. like /[dbname]/res.partner,
  704. # POST creates an entry in this collection:
  705. if not request.postpath:
  706. d.addCallback(self.__addToCollection, request, pwd)
  707. # if uri is sth. like /[dbname]/res.partner/27/something,
  708. # POST executes a workflow on this object
  709. elif len(request.postpath) == 2 and self.__is_number(request.postpath[0]):
  710. d.addCallback(self.__prepareWorkflow, request, pwd, *request.postpath)
  711. # if URI is sth. like /[dbname]/res.partner/something,
  712. # return 400, cannot POST here
  713. else:
  714. d.addCallback(self.__raiseAnError,
  715. PostNotPossible("/" + '/'.join([self.dbname, self.model, request.postpath[0]])))
  716. d.addErrback(self.__cleanup, request)
  717. return NOT_DONE_YET
  718. class InvalidParameter(Exception):
  719. code = 400
  720. def __init__(self, param):
  721. self.param = param
  722. def __str__(self):
  723. return "Invalid parameter: "+str(self.param)
  724. class PostNotPossible(Exception):
  725. code = 400
  726. def __init__(self, res):
  727. self.res = res
  728. def __str__(self):
  729. return "You cannot POST to "+str(self.res)
  730. class NoChildResources(Exception):
  731. code = 404
  732. def __init__(self, res):
  733. self.res = res
  734. def __str__(self):
  735. return str(self.res) + " has no child resources"
  736. if __name__ == "__main__":
  737. # read config
  738. config = ConfigParser.RawConfigParser()
  739. config.read('restful-openerp.cfg')
  740. openerpUrl = config.get("OpenERP", "url")
  741. try:
  742. port = config.getint("Proxy Settings", "port")
  743. except:
  744. port = 8068
  745. # go
  746. log.startLogging(sys.stdout)
  747. root = OpenErpDispatcher(openerpUrl)
  748. factory = Site(root)
  749. reactor.listenTCP(port, factory)
  750. reactor.run()