You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

514 lines
17 KiB

  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # OpenERP, Open Source Management Solution
  5. # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as
  9. # published by the Free Software Foundation, either version 3 of the
  10. # License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. from openerp.osv import orm, fields
  22. from openerp import netsvc
  23. import base64
  24. import tempfile
  25. import tarfile
  26. import httplib
  27. import os
  28. class RstDoc(object):
  29. def __init__(self, module, objects):
  30. self.dico = {
  31. 'name': module.name,
  32. 'shortdesc': module.shortdesc,
  33. 'latest_version': module.latest_version,
  34. 'website': module.website,
  35. 'description': self._handle_text(
  36. module.description.strip() or 'None'
  37. ),
  38. 'report_list': self._handle_list_items(module.reports_by_module),
  39. 'menu_list': self._handle_list_items(module.menus_by_module),
  40. 'view_list': self._handle_list_items(module.views_by_module),
  41. 'depends': module.dependencies_id,
  42. 'author': module.author,
  43. }
  44. self.objects = objects
  45. self.module = module
  46. def _handle_list_items(self, list_item_as_string):
  47. list_item_as_string = list_item_as_string.strip()
  48. if list_item_as_string:
  49. return [
  50. item.replace("*", r"\*") for
  51. item in
  52. list_item_as_string.split('\n')
  53. ]
  54. else:
  55. return []
  56. def _handle_text(self, txt):
  57. lst = [' %s' % line for line in txt.split('\n')]
  58. return '\n'.join(lst)
  59. def _get_download_links(self):
  60. def _is_connection_status_good(link):
  61. server = "openerp.com"
  62. status_good = False
  63. try:
  64. conn = httplib.HTTPConnection(server)
  65. conn.request("HEAD", link)
  66. res = conn.getresponse()
  67. if res.status in (200, ):
  68. status_good = True
  69. except (Exception, ), e:
  70. logger = netsvc.Logger()
  71. msg = """
  72. error connecting to server '%s' with link '%s'.
  73. Error message: %s
  74. """ % (server, link, str(e))
  75. logger.notifyChannel(
  76. "base_module_doc_rst", netsvc.LOG_ERROR, msg
  77. )
  78. status_good = False
  79. return status_good
  80. versions = ('7.0', '8.0', 'master')
  81. download_links = []
  82. for ver in versions:
  83. link = 'https://apps.odoo.com/loempia/download/%s/%s.zip' % (
  84. ver, self.dico['name']
  85. )
  86. if _is_connection_status_good(link):
  87. download_links.append(" * `%s <%s>`_" % (ver, link))
  88. if download_links:
  89. res = '\n'.join(download_links)
  90. else:
  91. res = "(No download links available)"
  92. return res
  93. def _write_header(self):
  94. dico = self.dico
  95. title = "%s (*%s*)" % (dico['shortdesc'], dico['name'])
  96. title_underline = "=" * len(title)
  97. dico['title'] = title
  98. dico['title_underline'] = title_underline
  99. dico['download_links'] = self._get_download_links()
  100. sl = [
  101. "",
  102. ".. module:: %(name)s",
  103. " :synopsis: %(shortdesc)s",
  104. " :noindex:",
  105. ".. ",
  106. "",
  107. ".. raw:: html",
  108. "",
  109. " <br />",
  110. """
  111. <link rel="stylesheet"
  112. href="../_static/hide_objects_in_sidebar.css"
  113. type="text/css" />
  114. """,
  115. "",
  116. "",
  117. ".. raw:: html",
  118. "",
  119. """
  120. <div class="js-kit-rating"
  121. title="" permalink="" standalone="yes" path="/%s"></div>
  122. """ % (dico['name'],),
  123. """ <script src="http://js-kit.com/ratings.js"></script>""",
  124. "",
  125. "%(title)s",
  126. "%(title_underline)s",
  127. ":Module: %(name)s",
  128. ":Name: %(shortdesc)s",
  129. ":Version: %(latest_version)s",
  130. ":Author: %(author)s",
  131. ":Directory: %(name)s",
  132. ":Web: %(website)s",
  133. "",
  134. "Description",
  135. "-----------",
  136. "",
  137. "::",
  138. "",
  139. "%(description)s",
  140. "",
  141. "Download links",
  142. "--------------",
  143. "",
  144. "You can download this module as a zip file in following version:",
  145. "",
  146. "%(download_links)s",
  147. "",
  148. ""]
  149. return '\n'.join(sl) % (dico)
  150. def _write_reports(self):
  151. sl = ["",
  152. "Reports",
  153. "-------"]
  154. reports = self.dico['report_list']
  155. if reports:
  156. for report in reports:
  157. if report:
  158. sl.append("")
  159. sl.append(" * %s" % report)
  160. else:
  161. sl.extend(["", "None", ""])
  162. sl.append("")
  163. return '\n'.join(sl)
  164. def _write_menus(self):
  165. sl = ["",
  166. "Menus",
  167. "-------",
  168. ""]
  169. menus = self.dico['menu_list']
  170. if menus:
  171. for menu in menus:
  172. if menu:
  173. sl.append(" * %s" % menu)
  174. else:
  175. sl.extend(["", "None", ""])
  176. sl.append("")
  177. return '\n'.join(sl)
  178. def _write_views(self):
  179. sl = ["",
  180. "Views",
  181. "-----",
  182. ""]
  183. views = self.dico['view_list']
  184. if views:
  185. for view in views:
  186. if view:
  187. sl.append(" * %s" % view)
  188. else:
  189. sl.extend(["", "None", ""])
  190. sl.append("")
  191. return '\n'.join(sl)
  192. def _write_depends(self):
  193. sl = ["",
  194. "Dependencies",
  195. "------------",
  196. ""]
  197. depends = self.dico['depends']
  198. if depends:
  199. for dependency in depends:
  200. sl.append(" * :mod:`%s`" % (dependency.name))
  201. else:
  202. sl.extend(["", "None", ""])
  203. sl.append("")
  204. return '\n'.join(sl)
  205. def _write_objects(self):
  206. def write_field(field_def):
  207. if not isinstance(field_def, tuple):
  208. logger = netsvc.Logger()
  209. msg = "Error on Object %s: field_def: %s [type: %s]" % (
  210. obj_name.encode('utf8'),
  211. field_def.encode('utf8'),
  212. type(field_def)
  213. )
  214. logger.notifyChannel(
  215. "base_module_doc_rst", netsvc.LOG_ERROR, msg
  216. )
  217. return ""
  218. field_name = field_def[0]
  219. field_dict = field_def[1]
  220. field_required = field_dict.get('required', '') and ', required'
  221. field_readonly = field_dict.get('readonly', '') and ', readonly'
  222. field_help_s = field_dict.get('help', '')
  223. if field_help_s:
  224. field_help_s = "*%s*" % (field_help_s)
  225. field_help = '\n'.join(
  226. [
  227. ' %s' % line.strip() for
  228. line in
  229. field_help_s.split('\n')
  230. ]
  231. )
  232. else:
  233. field_help = ''
  234. sl = [
  235. "",
  236. ":%s: %s, %s%s%s" % (field_name,
  237. field_dict.get('string', 'Unknown'),
  238. field_dict['type'],
  239. field_required,
  240. field_readonly),
  241. "",
  242. field_help,
  243. ]
  244. return '\n'.join(sl)
  245. sl = ["",
  246. "",
  247. "Objects",
  248. "-------"]
  249. if self.objects:
  250. for obj in self.objects:
  251. obj_name = obj['object'].name
  252. obj_model = obj['object'].model
  253. title = "Object: %s (%s)" % (obj_name, obj_model)
  254. slo = [
  255. "",
  256. title,
  257. '#' * len(title),
  258. "",
  259. ]
  260. for field in obj['fields']:
  261. slf = [
  262. "",
  263. write_field(field),
  264. "",
  265. ]
  266. slo.extend(slf)
  267. sl.extend(slo)
  268. else:
  269. sl.extend(["", "None", ""])
  270. return u'\n'.join([a.decode('utf8') for a in sl])
  271. def _write_relationship_graph(self, module_name=False):
  272. sl = ["",
  273. "Relationship Graph",
  274. "------------------",
  275. "",
  276. ".. figure:: %s_module.png" % (module_name, ),
  277. " :scale: 50",
  278. " :align: center",
  279. ""]
  280. sl.append("")
  281. return '\n'.join(sl)
  282. def write(self, module_name=False):
  283. s = ''
  284. s += self._write_header()
  285. s += self._write_depends()
  286. s += self._write_reports()
  287. s += self._write_menus()
  288. s += self._write_views()
  289. s += self._write_objects()
  290. if module_name:
  291. s += self._write_relationship_graph(module_name)
  292. return s
  293. class WizardTechGuideRst(orm.TransientModel):
  294. _name = "tech.guide.rst"
  295. _columns = {
  296. 'rst_file': fields.binary('File', required=True, readonly=True),
  297. }
  298. def _generate(self, cr, uid, context):
  299. module_model = self.pool.get('ir.module.module')
  300. module_ids = context['active_ids']
  301. module_index = []
  302. # create a temporary gzipped tarfile:
  303. tgz_tmp_filename = tempfile.mktemp('_rst_module_doc.tgz')
  304. try:
  305. tarf = tarfile.open(tgz_tmp_filename, 'w:gz')
  306. modules = module_model.browse(cr, uid, module_ids)
  307. for module in modules:
  308. index_dict = {
  309. 'name': module.name,
  310. 'shortdesc': module.shortdesc,
  311. }
  312. module_index.append(index_dict)
  313. objects = self._get_objects(cr, uid, module)
  314. module.test_views = self._get_views(
  315. cr, uid, module.id, context=context
  316. )
  317. rstdoc = RstDoc(module, objects)
  318. # Append Relationship Graph on rst
  319. graph_mod = False
  320. module_name = False
  321. if module.file_graph:
  322. graph_mod = base64.decodestring(module.file_graph)
  323. else:
  324. module_data = module_model.get_relation_graph(
  325. cr, uid, module.name, context=context
  326. )
  327. if module_data['module_file']:
  328. graph_mod = base64.decodestring(
  329. module_data['module_file']
  330. )
  331. if graph_mod:
  332. module_name = module.name
  333. try:
  334. tmp_file_graph = tempfile.NamedTemporaryFile()
  335. tmp_file_graph.write(graph_mod)
  336. tmp_file_graph.file.flush()
  337. tarf.add(
  338. tmp_file_graph.name,
  339. arcname=module.name + '_module.png'
  340. )
  341. finally:
  342. tmp_file_graph.close()
  343. out = rstdoc.write(module_name)
  344. try:
  345. tmp_file = tempfile.NamedTemporaryFile()
  346. tmp_file.write(out.encode('utf8'))
  347. tmp_file.file.flush() # write content to file
  348. tarf.add(tmp_file.name, arcname=module.name + '.rst')
  349. finally:
  350. tmp_file.close()
  351. # write index file:
  352. tmp_file = tempfile.NamedTemporaryFile()
  353. out = self._create_index(module_index)
  354. tmp_file.write(out.encode('utf8'))
  355. tmp_file.file.flush()
  356. tarf.add(tmp_file.name, arcname='index.rst')
  357. finally:
  358. tarf.close()
  359. f = open(tgz_tmp_filename, 'rb')
  360. out = f.read()
  361. f.close()
  362. if os.path.exists(tgz_tmp_filename):
  363. try:
  364. os.unlink(tgz_tmp_filename)
  365. except Exception, e:
  366. logger = netsvc.Logger()
  367. msg = "Temporary file %s could not be deleted. (%s)" % (
  368. tgz_tmp_filename, e
  369. )
  370. logger.notifyChannel("warning", netsvc.LOG_WARNING, msg)
  371. return base64.encodestring(out)
  372. def _get_views(self, cr, uid, module_id, context=None):
  373. module_module_obj = self.pool.get('ir.module.module')
  374. model_data_obj = self.pool.get('ir.model.data')
  375. view_obj = self.pool.get('ir.ui.view')
  376. report_obj = self.pool.get('ir.actions.report.xml')
  377. menu_obj = self.pool.get('ir.ui.menu')
  378. res = {}
  379. mlist = module_module_obj.browse(cr, uid, [module_id], context=context)
  380. mnames = {}
  381. for m in mlist:
  382. mnames[m.name] = m.id
  383. res[m.id] = {
  384. 'menus_by_module': [],
  385. 'reports_by_module': [],
  386. 'views_by_module': []
  387. }
  388. view_id = model_data_obj.search(
  389. cr,
  390. uid,
  391. [
  392. ('module', 'in', mnames.keys()),
  393. ('model', 'in', (
  394. 'ir.ui.view', 'ir.actions.report.xml', 'ir.ui.menu'
  395. ))
  396. ]
  397. )
  398. for data_id in model_data_obj.browse(cr, uid, view_id, context):
  399. # We use try except, because views or menus may not exist
  400. try:
  401. key = data_id['model']
  402. if key == 'ir.ui.view':
  403. v = view_obj.browse(cr, uid, data_id.res_id)
  404. v_dict = {
  405. 'name': v.name,
  406. 'inherit': v.inherit_id,
  407. 'type': v.type}
  408. res[mnames[data_id.module]]['views_by_module'].append(
  409. v_dict
  410. )
  411. elif key == 'ir.actions.report.xml':
  412. res[mnames[data_id.module]]['reports_by_module'].append(
  413. report_obj.browse(cr, uid, data_id.res_id).name
  414. )
  415. elif key == 'ir.ui.menu':
  416. res[mnames[data_id.module]]['menus_by_module'].append(
  417. menu_obj.browse(cr, uid, data_id.res_id).complete_name
  418. )
  419. except (KeyError, ):
  420. pass
  421. return res
  422. def _create_index(self, module_index):
  423. sl = ["",
  424. ".. _module-technical-guide-link:",
  425. "",
  426. "Module Technical Guide: Introspection report on objects",
  427. "=======================================================",
  428. "",
  429. ".. toctree::",
  430. " :maxdepth: 1",
  431. "",
  432. ]
  433. for mod in module_index:
  434. sl.append(" %s" % mod['name'])
  435. sl.append("")
  436. return '\n'.join(sl)
  437. def _get_objects(self, cr, uid, module):
  438. res = []
  439. objects = self._object_find(cr, uid, module)
  440. for obj in objects:
  441. fields = self._fields_find(cr, uid, obj.model)
  442. dico = {
  443. 'object': obj,
  444. 'fields': fields
  445. }
  446. res.append(dico)
  447. return res
  448. def _object_find(self, cr, uid, module):
  449. ir_model_data = self.pool.get('ir.model.data')
  450. ids2 = ir_model_data.search(
  451. cr, uid, [('module', '=', module.name), ('model', '=', 'ir.model')]
  452. )
  453. ids = []
  454. for mod in ir_model_data.browse(cr, uid, ids2):
  455. ids.append(mod.res_id)
  456. return self.pool.get('ir.model').browse(cr, uid, ids)
  457. def _fields_find(self, cr, uid, obj):
  458. modobj = self.pool.get(obj)
  459. if modobj:
  460. res = modobj.fields_get(cr, uid).items()
  461. return res
  462. else:
  463. logger = netsvc.Logger()
  464. msg = "Object %s not found" % (obj)
  465. logger.notifyChannel("base_module_doc_rst", netsvc.LOG_ERROR, msg)
  466. return ""
  467. _defaults = {
  468. 'rst_file': _generate,
  469. }