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.

523 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 = ('4.2', '5.0', 'trunk')
  81. download_links = []
  82. for ver in versions:
  83. link = 'http://www.openerp.com/download/modules/%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. .. tip:: This module is part of the OpenERP software,
  118. the leading Open Source
  119. """,
  120. """ enterprise management system. If you want to discover OpenERP,
  121. check our
  122. """,
  123. """ `screencasts <http://openerp.tv>`_ or download """,
  124. """ `OpenERP <http://openerp.com>`_ directly.""",
  125. "",
  126. ".. raw:: html",
  127. "",
  128. """
  129. <div class="js-kit-rating"
  130. title="" permalink="" standalone="yes" path="/%s"></div>
  131. """ % (dico['name'],),
  132. """ <script src="http://js-kit.com/ratings.js"></script>""",
  133. "",
  134. "%(title)s",
  135. "%(title_underline)s",
  136. ":Module: %(name)s",
  137. ":Name: %(shortdesc)s",
  138. ":Version: %(latest_version)s",
  139. ":Author: %(author)s",
  140. ":Directory: %(name)s",
  141. ":Web: %(website)s",
  142. "",
  143. "Description",
  144. "-----------",
  145. "",
  146. "::",
  147. "",
  148. "%(description)s",
  149. "",
  150. "Download links",
  151. "--------------",
  152. "",
  153. "You can download this module as a zip file in following version:",
  154. "",
  155. "%(download_links)s",
  156. "",
  157. ""]
  158. return '\n'.join(sl) % (dico)
  159. def _write_reports(self):
  160. sl = ["",
  161. "Reports",
  162. "-------"]
  163. reports = self.dico['report_list']
  164. if reports:
  165. for report in reports:
  166. if report:
  167. sl.append("")
  168. sl.append(" * %s" % report)
  169. else:
  170. sl.extend(["", "None", ""])
  171. sl.append("")
  172. return '\n'.join(sl)
  173. def _write_menus(self):
  174. sl = ["",
  175. "Menus",
  176. "-------",
  177. ""]
  178. menus = self.dico['menu_list']
  179. if menus:
  180. for menu in menus:
  181. if menu:
  182. sl.append(" * %s" % menu)
  183. else:
  184. sl.extend(["", "None", ""])
  185. sl.append("")
  186. return '\n'.join(sl)
  187. def _write_views(self):
  188. sl = ["",
  189. "Views",
  190. "-----",
  191. ""]
  192. views = self.dico['view_list']
  193. if views:
  194. for view in views:
  195. if view:
  196. sl.append(" * %s" % view)
  197. else:
  198. sl.extend(["", "None", ""])
  199. sl.append("")
  200. return '\n'.join(sl)
  201. def _write_depends(self):
  202. sl = ["",
  203. "Dependencies",
  204. "------------",
  205. ""]
  206. depends = self.dico['depends']
  207. if depends:
  208. for dependency in depends:
  209. sl.append(" * :mod:`%s`" % (dependency.name))
  210. else:
  211. sl.extend(["", "None", ""])
  212. sl.append("")
  213. return '\n'.join(sl)
  214. def _write_objects(self):
  215. def write_field(field_def):
  216. if not isinstance(field_def, tuple):
  217. logger = netsvc.Logger()
  218. msg = "Error on Object %s: field_def: %s [type: %s]" % (
  219. obj_name.encode('utf8'),
  220. field_def.encode('utf8'),
  221. type(field_def)
  222. )
  223. logger.notifyChannel(
  224. "base_module_doc_rst", netsvc.LOG_ERROR, msg
  225. )
  226. return ""
  227. field_name = field_def[0]
  228. field_dict = field_def[1]
  229. field_required = field_dict.get('required', '') and ', required'
  230. field_readonly = field_dict.get('readonly', '') and ', readonly'
  231. field_help_s = field_dict.get('help', '')
  232. if field_help_s:
  233. field_help_s = "*%s*" % (field_help_s)
  234. field_help = '\n'.join(
  235. [
  236. ' %s' % line.strip() for
  237. line in
  238. field_help_s.split('\n')
  239. ]
  240. )
  241. else:
  242. field_help = ''
  243. sl = [
  244. "",
  245. ":%s: %s, %s%s%s" % (field_name,
  246. field_dict.get('string', 'Unknown'),
  247. field_dict['type'],
  248. field_required,
  249. field_readonly),
  250. "",
  251. field_help,
  252. ]
  253. return '\n'.join(sl)
  254. sl = ["",
  255. "",
  256. "Objects",
  257. "-------"]
  258. if self.objects:
  259. for obj in self.objects:
  260. obj_name = obj['object'].name
  261. obj_model = obj['object'].model
  262. title = "Object: %s (%s)" % (obj_name, obj_model)
  263. slo = [
  264. "",
  265. title,
  266. '#' * len(title),
  267. "",
  268. ]
  269. for field in obj['fields']:
  270. slf = [
  271. "",
  272. write_field(field),
  273. "",
  274. ]
  275. slo.extend(slf)
  276. sl.extend(slo)
  277. else:
  278. sl.extend(["", "None", ""])
  279. return u'\n'.join([a.decode('utf8') for a in sl])
  280. def _write_relationship_graph(self, module_name=False):
  281. sl = ["",
  282. "Relationship Graph",
  283. "------------------",
  284. "",
  285. ".. figure:: %s_module.png" % (module_name, ),
  286. " :scale: 50",
  287. " :align: center",
  288. ""]
  289. sl.append("")
  290. return '\n'.join(sl)
  291. def write(self, module_name=False):
  292. s = ''
  293. s += self._write_header()
  294. s += self._write_depends()
  295. s += self._write_reports()
  296. s += self._write_menus()
  297. s += self._write_views()
  298. s += self._write_objects()
  299. if module_name:
  300. s += self._write_relationship_graph(module_name)
  301. return s
  302. class WizardTechGuideRst(orm.TransientModel):
  303. _name = "tech.guide.rst"
  304. _columns = {
  305. 'rst_file': fields.binary('File', required=True, readonly=True),
  306. }
  307. def _generate(self, cr, uid, context):
  308. module_model = self.pool.get('ir.module.module')
  309. module_ids = context['active_ids']
  310. module_index = []
  311. # create a temporary gzipped tarfile:
  312. tgz_tmp_filename = tempfile.mktemp('_rst_module_doc.tgz')
  313. try:
  314. tarf = tarfile.open(tgz_tmp_filename, 'w:gz')
  315. modules = module_model.browse(cr, uid, module_ids)
  316. for module in modules:
  317. index_dict = {
  318. 'name': module.name,
  319. 'shortdesc': module.shortdesc,
  320. }
  321. module_index.append(index_dict)
  322. objects = self._get_objects(cr, uid, module)
  323. module.test_views = self._get_views(
  324. cr, uid, module.id, context=context
  325. )
  326. rstdoc = RstDoc(module, objects)
  327. # Append Relationship Graph on rst
  328. graph_mod = False
  329. module_name = False
  330. if module.file_graph:
  331. graph_mod = base64.decodestring(module.file_graph)
  332. else:
  333. module_data = module_model.get_relation_graph(
  334. cr, uid, module.name, context=context
  335. )
  336. if module_data['module_file']:
  337. graph_mod = base64.decodestring(
  338. module_data['module_file']
  339. )
  340. if graph_mod:
  341. module_name = module.name
  342. try:
  343. tmp_file_graph = tempfile.NamedTemporaryFile()
  344. tmp_file_graph.write(graph_mod)
  345. tmp_file_graph.file.flush()
  346. tarf.add(
  347. tmp_file_graph.name,
  348. arcname=module.name + '_module.png'
  349. )
  350. finally:
  351. tmp_file_graph.close()
  352. out = rstdoc.write(module_name)
  353. try:
  354. tmp_file = tempfile.NamedTemporaryFile()
  355. tmp_file.write(out.encode('utf8'))
  356. tmp_file.file.flush() # write content to file
  357. tarf.add(tmp_file.name, arcname=module.name + '.rst')
  358. finally:
  359. tmp_file.close()
  360. # write index file:
  361. tmp_file = tempfile.NamedTemporaryFile()
  362. out = self._create_index(module_index)
  363. tmp_file.write(out.encode('utf8'))
  364. tmp_file.file.flush()
  365. tarf.add(tmp_file.name, arcname='index.rst')
  366. finally:
  367. tarf.close()
  368. f = open(tgz_tmp_filename, 'rb')
  369. out = f.read()
  370. f.close()
  371. if os.path.exists(tgz_tmp_filename):
  372. try:
  373. os.unlink(tgz_tmp_filename)
  374. except Exception, e:
  375. logger = netsvc.Logger()
  376. msg = "Temporary file %s could not be deleted. (%s)" % (
  377. tgz_tmp_filename, e
  378. )
  379. logger.notifyChannel("warning", netsvc.LOG_WARNING, msg)
  380. return base64.encodestring(out)
  381. def _get_views(self, cr, uid, module_id, context=None):
  382. module_module_obj = self.pool.get('ir.module.module')
  383. model_data_obj = self.pool.get('ir.model.data')
  384. view_obj = self.pool.get('ir.ui.view')
  385. report_obj = self.pool.get('ir.actions.report.xml')
  386. menu_obj = self.pool.get('ir.ui.menu')
  387. res = {}
  388. mlist = module_module_obj.browse(cr, uid, [module_id], context=context)
  389. mnames = {}
  390. for m in mlist:
  391. mnames[m.name] = m.id
  392. res[m.id] = {
  393. 'menus_by_module': [],
  394. 'reports_by_module': [],
  395. 'views_by_module': []
  396. }
  397. view_id = model_data_obj.search(
  398. cr,
  399. uid,
  400. [
  401. ('module', 'in', mnames.keys()),
  402. ('model', 'in', (
  403. 'ir.ui.view', 'ir.actions.report.xml', 'ir.ui.menu'
  404. ))
  405. ]
  406. )
  407. for data_id in model_data_obj.browse(cr, uid, view_id, context):
  408. # We use try except, because views or menus may not exist
  409. try:
  410. key = data_id['model']
  411. if key == 'ir.ui.view':
  412. v = view_obj.browse(cr, uid, data_id.res_id)
  413. v_dict = {
  414. 'name': v.name,
  415. 'inherit': v.inherit_id,
  416. 'type': v.type}
  417. res[mnames[data_id.module]]['views_by_module'].append(
  418. v_dict
  419. )
  420. elif key == 'ir.actions.report.xml':
  421. res[mnames[data_id.module]]['reports_by_module'].append(
  422. report_obj.browse(cr, uid, data_id.res_id).name
  423. )
  424. elif key == 'ir.ui.menu':
  425. res[mnames[data_id.module]]['menus_by_module'].append(
  426. menu_obj.browse(cr, uid, data_id.res_id).complete_name
  427. )
  428. except (KeyError, ):
  429. pass
  430. return res
  431. def _create_index(self, module_index):
  432. sl = ["",
  433. ".. _module-technical-guide-link:",
  434. "",
  435. "Module Technical Guide: Introspection report on objects",
  436. "=======================================================",
  437. "",
  438. ".. toctree::",
  439. " :maxdepth: 1",
  440. "",
  441. ]
  442. for mod in module_index:
  443. sl.append(" %s" % mod['name'])
  444. sl.append("")
  445. return '\n'.join(sl)
  446. def _get_objects(self, cr, uid, module):
  447. res = []
  448. objects = self._object_find(cr, uid, module)
  449. for obj in objects:
  450. fields = self._fields_find(cr, uid, obj.model)
  451. dico = {
  452. 'object': obj,
  453. 'fields': fields
  454. }
  455. res.append(dico)
  456. return res
  457. def _object_find(self, cr, uid, module):
  458. ir_model_data = self.pool.get('ir.model.data')
  459. ids2 = ir_model_data.search(
  460. cr, uid, [('module', '=', module.name), ('model', '=', 'ir.model')]
  461. )
  462. ids = []
  463. for mod in ir_model_data.browse(cr, uid, ids2):
  464. ids.append(mod.res_id)
  465. return self.pool.get('ir.model').browse(cr, uid, ids)
  466. def _fields_find(self, cr, uid, obj):
  467. modobj = self.pool.get(obj)
  468. if modobj:
  469. res = modobj.fields_get(cr, uid).items()
  470. return res
  471. else:
  472. logger = netsvc.Logger()
  473. msg = "Object %s not found" % (obj)
  474. logger.notifyChannel("base_module_doc_rst", netsvc.LOG_ERROR, msg)
  475. return ""
  476. _defaults = {
  477. 'rst_file': _generate,
  478. }