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.

339 lines
14 KiB

10 years ago
  1. # Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
  2. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  3. import logging
  4. import base64
  5. import time
  6. import copy
  7. from urllib.parse import urlparse
  8. from urllib.parse import parse_qsl
  9. from werkzeug.routing import Map, Rule
  10. from lxml import etree as ET
  11. from odoo import models, fields, api, exceptions
  12. from odoo.tools.translate import _
  13. from odoo.addons.web.controllers.main import Binary
  14. from odoo.addons.website.controllers.main import WebsiteBinary
  15. _logger = logging.getLogger(__name__)
  16. PAGE_PREFIX_PARAMETER = 'help_online_page_prefix'
  17. TEMPLATE_PREFIX_PARAMETER = 'help_online_template_prefix'
  18. AUTOBACKUP_PARAMETER = 'help_online_autobackup_path'
  19. HELP_ONLINE_SNIPPET_IMAGE_PATH = '/help_online/static/src/'\
  20. 'img/snippet/snippet_thumbs.png'
  21. class ExportHelpWizard(models.TransientModel):
  22. _name = "export.help.wizard"
  23. _description = 'Export Help Online'
  24. data = fields.Binary('XML', readonly=True)
  25. export_filename = fields.Char('Export XML Filename', size=128)
  26. binary = Binary()
  27. websiteBinary = WebsiteBinary()
  28. img_url_map = Map([
  29. Rule('/web/image'),
  30. Rule('/web/image/<string:xmlid>'),
  31. Rule('/web/image/<string:xmlid>/<string:filename>'),
  32. Rule('/web/image/<string:xmlid>/<int:width>x<int:height>'),
  33. Rule('/web/image/<string:xmlid>/<int:width>x<int:height>/'
  34. '<string:filename>'),
  35. Rule('/web/image/<string:model>/<int:id>/<string:field>'),
  36. Rule('/web/image/<string:model>/<int:id>/<string:field>/'
  37. '<string:filename>'),
  38. Rule('/web/image/<string:model>/<int:id>/<string:field>/'
  39. '<int:width>x<int:height>'),
  40. Rule('/web/image/<string:model>/<int:id>/<string:field>/'
  41. '<int:width>x<int:height>/<string:filename>'),
  42. Rule('/web/image/<int:id>'),
  43. Rule('/web/image/<int:id>/<string:filename>'),
  44. Rule('/web/image/<int:id>/<int:width>x<int:height>'),
  45. Rule('/web/image/<int:id>/<int:width>x<int:height>/<string:filename>'),
  46. Rule('/web/image/<int:id>-<string:unique>'),
  47. Rule('/web/image/<int:id>-<string:unique>/<string:filename>'),
  48. Rule('/web/image/<int:id>-<string:unique>/<int:width>x<int:height>'),
  49. Rule('/web/image/<int:id>-<string:unique>/<int:width>x<int:height>'
  50. '/<string:filename>'),
  51. Rule('/website/image'),
  52. Rule('/website/image/<xmlid>'),
  53. Rule('/website/image/<xmlid>/<int:width>x<int:height>'),
  54. Rule('/website/image/<xmlid>/<field>'),
  55. Rule('/website/image/<xmlid>/<field>/<int:width>x<int:height>'),
  56. Rule('/website/image/<model>/<id>/<field>'),
  57. Rule('/website/image/<model>/<id>/<field>/<int:width>x<int:height>')
  58. ])
  59. def _manage_images_on_page(self, page_node, data_node, exported_resources):
  60. """
  61. - Extract images from page and generate an xml node
  62. - Replace db id in url with xml id
  63. """
  64. img_model = 'ir.attachment'
  65. urls = self.img_url_map.bind("dummy.org", "/")
  66. for img_elem in page_node.iter('img'):
  67. img_src = img_elem.get('src')
  68. parse_result = urlparse(img_src)
  69. path = parse_result.path
  70. query_args = parse_result.query
  71. if urls.test(parse_result.path, "GET"):
  72. endpoint, kwargs = urls.match(path, "GET",
  73. query_args=query_args)
  74. kwargs.update(dict(parse_qsl(query_args)))
  75. image = None
  76. # get the binary object
  77. xml_id = kwargs.get('xmlid')
  78. if xml_id:
  79. image = self.env.ref(xml_id, False)
  80. else:
  81. _id = kwargs.get('id')
  82. model = kwargs.get('model', 'ir.attachment')
  83. if _id and model:
  84. _id, _, unique = str(_id).partition('_')
  85. image = self.env[model].browse(int(_id))
  86. if (not image or
  87. not image.exists() or
  88. image._name != img_model):
  89. raise exceptions.UserError(
  90. _('Only images from ir.attachment are supported when '
  91. 'exporting help pages'))
  92. exported_data = image.export_data(
  93. ['id',
  94. 'datas',
  95. 'datas_fname',
  96. 'name',
  97. 'res_model',
  98. 'mimetype'],
  99. raw_data=False)['datas'][0]
  100. xml_id = exported_data[0]
  101. new_src = '/web/image/%s' % xml_id
  102. img_elem.attrib['src'] = new_src
  103. if xml_id in exported_resources:
  104. continue
  105. img_node = ET.SubElement(
  106. data_node,
  107. 'record',
  108. attrib={'id': xml_id,
  109. 'model': image._name})
  110. field_node = ET.SubElement(img_node,
  111. 'field',
  112. attrib={'name': 'datas'})
  113. field_node.text = str(exported_data[1])
  114. field_node = ET.SubElement(img_node,
  115. 'field',
  116. attrib={'name': 'datas_fname'})
  117. field_node.text = exported_data[2]
  118. field_node = ET.SubElement(img_node,
  119. 'field',
  120. attrib={'name': 'name'})
  121. field_node.text = exported_data[3]
  122. field_node = ET.SubElement(img_node,
  123. 'field',
  124. attrib={'name': 'res_model'})
  125. field_node.text = exported_data[4]
  126. field_node = ET.SubElement(img_node,
  127. 'field',
  128. attrib={'name': 'mimetype'})
  129. field_node.text = exported_data[5]
  130. data_node.append(img_node)
  131. exported_resources.add(xml_id)
  132. def _clean_href_urls(self, page_node, page_prefix, template_prefix):
  133. """
  134. Remove host address for href urls
  135. """
  136. for a_elem in page_node.iter('a'):
  137. if not a_elem.get('href'):
  138. continue
  139. href = a_elem.get('href')
  140. if not href.startswith('http'):
  141. continue
  142. page_url = '/page/%s' % page_prefix
  143. template_url = '/page/%s' % template_prefix
  144. if page_url not in href and template_url not in href:
  145. continue
  146. elif page_url in href and template_url not in href:
  147. pass
  148. elif page_url not in href and template_url in href:
  149. page_url = template_url
  150. else:
  151. if page_prefix in template_prefix:
  152. page_url = template_url
  153. else:
  154. pass
  155. if page_url:
  156. trail = href.split(page_url, 1)[1]
  157. a_elem.attrib['href'] = page_url + trail
  158. def _generate_snippet_from_template(self, page_node,
  159. template_id, template_prefix):
  160. """
  161. Generate a website snippet from a template
  162. """
  163. page = copy.deepcopy(page_node)
  164. snippet = ET.Element('template')
  165. snippet.attrib['id'] = template_id + '_snippet'
  166. snippet.attrib['inherit_id'] = 'website.snippets'
  167. snippet.attrib['name'] = page_node.attrib['name']
  168. xpath = ET.SubElement(snippet,
  169. 'xpath',
  170. attrib={'expr': "//div[@id='snippet_structure']",
  171. 'position': 'inside'})
  172. main_div = ET.SubElement(xpath,
  173. 'div')
  174. thumbnail = ET.SubElement(main_div,
  175. 'div',
  176. attrib={'class': 'oe_snippet_thumbnail'})
  177. ET.SubElement(thumbnail,
  178. 'img',
  179. attrib={'class': 'oe_snippet_thumbnail_img',
  180. 'src': HELP_ONLINE_SNIPPET_IMAGE_PATH})
  181. span = ET.SubElement(thumbnail,
  182. 'span',
  183. attrib={'class': 'oe_snippet_thumbnail_title'})
  184. span.text = page_node.attrib['name'].replace(template_prefix, '')
  185. body = ET.SubElement(main_div,
  186. 'section',
  187. attrib={'class': 'oe_snippet_body '
  188. 'mt_simple_snippet'})
  189. template = page.find(".//div[@id='wrap']")
  190. for node in template.getchildren():
  191. body.append(node)
  192. return snippet
  193. def _get_qweb_views_data(self):
  194. parameter_model = self.env['ir.config_parameter']
  195. page_prefix = parameter_model.get_param(PAGE_PREFIX_PARAMETER,
  196. False)
  197. template_prefix = parameter_model.get_param(TEMPLATE_PREFIX_PARAMETER,
  198. False)
  199. if not page_prefix or not template_prefix:
  200. return False
  201. domain = [('type', '=', 'qweb'),
  202. ('page_ids', '!=', False),
  203. '|',
  204. ('name', 'like', '%s%%' % page_prefix),
  205. ('name', 'like', '%s%%' % template_prefix)]
  206. ir_ui_views = self.env['ir.ui.view'].search(domain, order='name')
  207. xml_to_export = ET.Element('odoo')
  208. data_node = ET.SubElement(xml_to_export, 'data')
  209. exported_resources = set()
  210. for ir_ui_view in ir_ui_views:
  211. parser = ET.XMLParser(remove_blank_text=True)
  212. root = ET.XML(ir_ui_view.arch, parser=parser)
  213. root.tag = 'template'
  214. xml_id = self._get_ir_ui_view_xml_id(
  215. ir_ui_view, root.attrib.pop('t-name'))
  216. root.attrib['name'] = ir_ui_view.name.replace('website.', '')
  217. root.attrib['id'] = xml_id
  218. root.attrib['page_ids'] = 'True'
  219. root.attrib['key'] = ir_ui_view.key
  220. self._manage_images_on_page(root, data_node, exported_resources)
  221. self._clean_href_urls(root, page_prefix, template_prefix)
  222. data_node.append(root)
  223. if root.attrib['name'].startswith(template_prefix):
  224. snippet = self._generate_snippet_from_template(root,
  225. xml_id,
  226. template_prefix)
  227. data_node.append(snippet)
  228. if len(ir_ui_views) > 0:
  229. return ET.tostring(xml_to_export, encoding='utf-8',
  230. xml_declaration=True,
  231. pretty_print=True)
  232. else:
  233. return False
  234. @api.model
  235. def _get_ir_ui_view_xml_id(self, ir_ui_view, template_name):
  236. """This method check if an xml_id exists for the given ir.ui.view
  237. If no xml_id exists, a new one is created with template name as
  238. value to ensure that the import of the generated file will update
  239. the existing view in place of creating new copies.
  240. """
  241. ir_model_data = self.sudo().env['ir.model.data']
  242. data = ir_model_data.search([('model', '=', ir_ui_view._name),
  243. ('res_id', '=', ir_ui_view.id)])
  244. if data:
  245. if data[0].module:
  246. return '%s.%s' % (data[0].module, data[0].name)
  247. else:
  248. return data[0].name
  249. else:
  250. module, name = template_name.split('.')
  251. # always use __export__ as module by convention to
  252. # avoid the removal by odoo of the exported pages on system
  253. # update (-u )
  254. module = "__export__"
  255. postfix = ir_model_data.search_count(
  256. [('module', '=', module),
  257. ('name', 'like', name)])
  258. if postfix:
  259. name = '%s_%s' % (name, postfix)
  260. ir_model_data.create({
  261. 'model': ir_ui_view._name,
  262. 'res_id': ir_ui_view.id,
  263. 'module': module,
  264. 'name': name,
  265. })
  266. return module + '.' + name
  267. @api.multi
  268. def export_help(self):
  269. """
  270. Export all Qweb views related to help online in a Odoo
  271. data XML file
  272. """
  273. xml_data = self._get_qweb_views_data()
  274. if not xml_data:
  275. raise exceptions.Warning(_('No data to export !'))
  276. out = base64.encodestring(xml_data)
  277. self.write({'data': out,
  278. 'export_filename': 'help_online_data.xml'})
  279. return {
  280. 'name': _('Export Help'),
  281. 'type': 'ir.actions.act_window',
  282. 'res_model': self._name,
  283. 'view_mode': 'form',
  284. 'view_type': 'form',
  285. 'res_id': self.id,
  286. 'views': [(False, 'form')],
  287. 'target': 'new',
  288. }
  289. @api.model
  290. def auto_backup(self):
  291. """
  292. Export data to a file on home directory of user
  293. """
  294. parameter_model = self.env['ir.config_parameter']
  295. autobackup_path = parameter_model.get_param(AUTOBACKUP_PARAMETER,
  296. False)
  297. if autobackup_path:
  298. xml_data = self._get_qweb_views_data()
  299. try:
  300. timestr = time.strftime("%Y%m%d-%H%M%S")
  301. filename = '%s/help_online_backup-%s.xml' % (autobackup_path,
  302. timestr)
  303. backup_file = open(filename,
  304. 'w')
  305. backup_file.write(xml_data)
  306. backup_file.close()
  307. except:
  308. _logger.warning(_('Unable to write autobackup file '
  309. 'in given directory: %s'
  310. % autobackup_path))