FormPrintout.py 40.8 KB
Newer Older
Nicolas Delaby's avatar
Nicolas Delaby committed
1
# -*- coding: utf-8 -*-
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
##############################################################################
#
# Copyright (c) 2009 Nexedi KK and Contributors. All Rights Reserved.
#                    Tatuya Kamada <tatuya@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
26 27
# Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301,
# USA.
28 29
##############################################################################
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
30
from Products.CMFCore.utils import _checkPermission
31
from Products.ERP5Type import PropertySheet, Permissions
32 33
from Products.ERP5Form.ListBox import ListBox
from Products.ERP5Form.FormBox import FormBox
34
from Products.ERP5Form.ReportBox import ReportBox
35
from Products.ERP5Form.ImageField import ImageField
36
from Products.ERP5OOo.OOoUtils import OOoBuilder
37
from Products.CMFCore.exceptions import AccessControl_Unauthorized
Tatuya Kamada's avatar
Tatuya Kamada committed
38
from Acquisition import Implicit, aq_base
39
from Products.ERP5Type.Globals import InitializeClass, DTMLFile, Persistent
40 41
from AccessControl import ClassSecurityInfo
from AccessControl.Role import RoleManager
42
from OFS.SimpleItem import Item
Tatuya Kamada's avatar
Tatuya Kamada committed
43
from urllib import quote, quote_plus
44 45 46 47
from copy import deepcopy
from lxml import etree
from zLOG import LOG, DEBUG, INFO, WARNING
from mimetypes import guess_extension
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
48
from DateTime import DateTime
Tatuya Kamada's avatar
Tatuya Kamada committed
49
from decimal import Decimal
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
50
from xml.sax.saxutils import escape
Tatuya Kamada's avatar
Tatuya Kamada committed
51
import re
52 53 54 55 56 57 58

try:
  from webdav.Lockable import ResourceLockedError
  SUPPORTS_WEBDAV_LOCKS = 1
except ImportError:
  SUPPORTS_WEBDAV_LOCKS = 0

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75

DRAW_URI = 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'
TEXT_URI = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'
XLINK_URI = 'http://www.w3.org/1999/xlink'
SVG_URI = 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'
TABLE_URI = 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'


NSMAP = {
          'draw': DRAW_URI,
          'text': TEXT_URI,
          'xlink': XLINK_URI,
          'svg': SVG_URI,
          'table': TABLE_URI,
        }


76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
# Constructors
manage_addFormPrintout = DTMLFile("dtml/FormPrintout_add", globals())

def addFormPrintout(self, id, title="", form_name='', template='', REQUEST=None):
  """Add form printout to folder.

  Keyword arguments:
  id     -- the id of the new form printout to add
  title  -- the title of the form printout to add
  form_name -- the name of a form which contains data to printout
  template -- the name of a template which describes printout layout
  """
  # add actual object
  id = self._setObject(id, FormPrintout(id, title, form_name, template))
  # respond to the add_and_edit button if necessary
  add_and_edit(self, id, REQUEST)
  return ''

def add_and_edit(self, id, REQUEST):
  """Helper method to point to the object's management screen if
  'Add and Edit' button is pressed.

  Keyword arguments:
99
  id -- the id of the object we just added
100 101 102 103 104 105 106 107 108 109
  """
  if REQUEST is None:
    return
  try:
    u = self.DestinationURL()
  except AttributeError:
    u = REQUEST['URL1']
  if REQUEST['submit'] == " Add and Edit ":
    u = "%s/%s" % (u, quote(id))
  REQUEST.RESPONSE.redirect(u+'/manage_main')
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
110

111 112 113
class FormPrintout(Implicit, Persistent, RoleManager, Item):
  """Form Printout

Fabien Morin's avatar
Fabien Morin committed
114 115 116
  FormPrintout is one of a reporting system in ERP5.
  It enables to create a Printout, using an Open Document Format(ODF)
  document as its design, an ERP5Form as its contents.
117

Tatuya Kamada's avatar
Tatuya Kamada committed
118
  The functions status:
Fabien Morin's avatar
Fabien Morin committed
119

Tatuya Kamada's avatar
Tatuya Kamada committed
120 121
  Fields -> Paragraphs:      supported
  ListBox -> Table:          supported
122 123
  Report Section
      -> Frames or Sections: supported
Tatuya Kamada's avatar
Tatuya Kamada committed
124 125 126 127
  FormBox -> Frame:          experimentally supported
  ImageField -> Photo:       supported
  styles.xml:                supported
  meta.xml:                  not supported yet
128
  """
Fabien Morin's avatar
Fabien Morin committed
129

130
  meta_type = "ERP5 Form Printout"
131
  icon = "www/form_printout_icon.png"
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146

  # Declarative Security
  security = ClassSecurityInfo()

  # Declarative properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.SimpleItem)

  # Constructors
  constructors =   (manage_addFormPrintout, addFormPrintout)

  # Tabs in ZMI
  manage_options = ((
    {'label':'Edit', 'action':'manage_editFormPrintout'},
    {'label':'View', 'action': '' }, ) + Item.manage_options)
Nicolas Delaby's avatar
Nicolas Delaby committed
147

148 149
  security.declareProtected('View management screens', 'manage_editFormPrintout')
  manage_editFormPrintout = PageTemplateFile('www/FormPrintout_manageEdit', globals(),
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
150
                                             __name__='manage_editFormPrintout')
151
  manage_editFormPrintout._owner = None
152 153 154 155

  # alias definition to do 'add_and_edit'
  security.declareProtected('View management screens', 'manage_main')
  manage_main = manage_editFormPrintout
Nicolas Delaby's avatar
Nicolas Delaby committed
156

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
  # default attributes
  template = None
  form_name = None

  def __init__(self, id, title='', form_name='', template=''):
    """Initialize id, title, form_name, template.

    Keyword arguments:
    id -- the id of a form printout
    title -- the title of a form printout
    form_name -- the name of a form which as a document content
    template -- the name of a template which as a document layout
    """
    self.id = id
    self.title = title
    self.form_name = form_name
    self.template = template

Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
175
  security.declareProtected('View', 'index_html')
176
  def index_html(self, REQUEST, icon=0, preview=0, width=None, height=None, RESPONSE=None):
177
    """Render and view a printout document."""
Nicolas Delaby's avatar
Nicolas Delaby committed
178

179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
    obj = getattr(self, 'aq_parent', None)
    if obj is not None:
      container = obj.aq_inner.aq_parent
      if not _checkPermission(Permissions.View, obj):
        raise AccessControl_Unauthorized('This document is not authorized for view.')
      else:
        container = None
    form = getattr(obj, self.form_name)
    if self.template is None or self.template == '':
      raise ValueError, 'Can not create a ODF Document without a printout template'
    printout_template = getattr(obj, self.template)

    report_method = None
    if hasattr(form, 'report_method'):
      report_method = getattr(obj, form.report_method)
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
194 195 196 197 198
    extra_context = dict(container=container,
                         printout_template=printout_template,
                         report_method=report_method,
                         form=form,
                         here=obj)
Tatuya Kamada's avatar
Tatuya Kamada committed
199
    # set property to do aquisition
200
    content_type = printout_template.content_type
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
201
    self.strategy = self._createStrategy(content_type)
202
    printout = self.strategy.render(extra_context=extra_context)
203 204
    return self._oooConvertByFormat(printout, content_type,
                                    extra_context, REQUEST)
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
205

Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
206 207
  security.declareProtected('View', '__call__')
  __call__ = index_html
Nicolas Delaby's avatar
Nicolas Delaby committed
208

209 210 211 212 213 214 215 216 217 218 219 220 221 222
  security.declareProtected('Manage properties', 'doSettings')
  def doSettings(self, REQUEST, title='', form_name='', template=''):
    """Change title, form_name, template."""
    if SUPPORTS_WEBDAV_LOCKS and self.wl_isLocked():
      raise ResourceLockedError, "File is locked via WebDAV"
    self.form_name = form_name
    self.template = template
    self.title = title
    message = "Saved changes."
    if getattr(self, '_v_warnings', None):
      message = ("<strong>Warning:</strong> <i>%s</i>"
                % '<br>'.join(self._v_warnings))
    return self.manage_editFormPrintout(manage_tabs_message=message)

Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
223
  def _createStrategy(slef, content_type=''):
224 225
    if guess_extension(content_type) == '.odt':
      return ODTStrategy()
226 227 228
    if guess_extension(content_type) == '.odg':
      return ODGStrategy()
    raise ValueError, 'Template type: %s is not supported' % content_type
229

230
  def _oooConvertByFormat(self, printout, content_type, extra_context, REQUEST):
231 232 233 234 235 236 237 238 239 240 241
    """
    Convert the ODF document into the given format.

    Keyword arguments:
    printout -- ODF document
    content_type -- the content type of the printout
    extra_context -- extra_context including a format
    REQUEST -- Request object
    """
    format = None
    if REQUEST is not None:
242
      format = REQUEST.get('format', None)
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
    if format is None:
      if REQUEST is not None:
        REQUEST.RESPONSE.setHeader('Content-Type','%s; charset=utf-8' % content_type)
        REQUEST.RESPONSE.setHeader('Content-disposition',
                                   'inline;filename="%s%s"' % (self.title_or_id(), guess_extension(content_type)))
      return printout
    from Products.ERP5Type.Document import newTempOOoDocument
    tmp_ooo = newTempOOoDocument(self, self.title_or_id())
    tmp_ooo.edit(base_data=printout,
                 fname=self.title_or_id(),
                 source_reference=self.title_or_id(),
                 base_content_type=content_type)
    tmp_ooo.oo_data = printout
    mime, data = tmp_ooo.convert(format)
    if REQUEST is not None:
      REQUEST.RESPONSE.setHeader('Content-type', mime)
      REQUEST.RESPONSE.setHeader('Content-disposition',
          'attachment;filename="%s.%s"' % (self.title_or_id(), format))
    return data

263 264 265 266
InitializeClass(FormPrintout)

class ODFStrategy(Implicit):
  """ODFStrategy creates a ODF Document. """
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
267

Tatuya Kamada's avatar
Tatuya Kamada committed
268
  odf_existent_name_list = []
Nicolas Delaby's avatar
Nicolas Delaby committed
269

270
  def render(self, extra_context={}):
271
    """Render a odf document, form as a content, template as a template.
272 273

    Keyword arguments:
274
    extra_context -- a dictionary, expected:
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
275 276 277 278
      'here' : where it call
      'printout_template' : the template object, tipically a OOoTemplate
      'container' : the object which has a form printout object
      'form' : the form as a content
279 280 281 282 283
    """
    here = extra_context['here']
    if here is None:
      raise ValueError, 'Can not create a ODF Document without a parent acquisition context'
    form = extra_context['form']
284 285
    if not extra_context.has_key('printout_template') or \
        extra_context['printout_template'] is None:
286 287 288
      raise ValueError, 'Can not create a ODF Document without a printout template'

    odf_template = extra_context['printout_template']
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
289

290
    # First, render the Template if it has a pt_render method
291 292 293 294
    ooo_document = None
    if hasattr(odf_template, 'pt_render'):
      ooo_document = odf_template.pt_render(here, extra_context=extra_context)
    else:
295
      # File object can be a template
296
      ooo_document = odf_template
297 298 299

    # Create a new builder instance
    ooo_builder = OOoBuilder(ooo_document)
Tatuya Kamada's avatar
Tatuya Kamada committed
300
    self.odf_existent_name_list = ooo_builder.getNameList()
Nicolas Delaby's avatar
Nicolas Delaby committed
301

302
    # content.xml
303
    self._replaceContentXml(ooo_builder, extra_context)
304
    # styles.xml
305
    self._replaceStylesXml(ooo_builder, extra_context)
306
    # meta.xml is not supported yet
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
307 308
    # ooo_builder = self._replaceMetaXml(ooo_builder=ooo_builder, extra_context=extra_context)

309 310 311 312 313 314
    # Update the META informations
    ooo_builder.updateManifest()

    ooo = ooo_builder.render(name=odf_template.title or odf_template.id)
    return ooo

315
  def _replaceContentXml(self, ooo_builder, extra_context):
316 317 318
    """
    Replace the content.xml in an ODF document using an ERP5Form data.
    """
319
    content_xml = ooo_builder.extract('content.xml')
320 321 322 323
    # mapping ERP5Form to ODF
    form = extra_context['form']
    here = getattr(self, 'aq_parent', None)

324
    content_element_tree = etree.XML(content_xml)
325 326
    self._replaceXmlByForm(content_element_tree, form, here, extra_context,
                           ooo_builder)
327
    # mapping ERP5Report report method to ODF
328
    report_method=extra_context.get('report_method')
329 330 331
    base_name = getattr(report_method, '__name__', None)
    self._replaceXmlByReportSection(content_element_tree, extra_context,
                                    report_method, base_name, ooo_builder)
Nicolas Delaby's avatar
Nicolas Delaby committed
332

333
    content_xml = etree.tostring(content_element_tree, encoding='utf-8')
334
    # Replace content.xml in master openoffice template
335
    ooo_builder.replace('content.xml', content_xml)
336 337

  # this method not supported yet
338
  def _replaceStylesXml(self, ooo_builder, extra_context):
339
    """
340
    Replace the styles.xml file in an ODF document.
341
    """
342
    styles_xml = ooo_builder.extract('styles.xml')
343 344
    form = extra_context['form']
    here = getattr(self, 'aq_parent', None)
345
    styles_element_tree = etree.XML(styles_xml)
346 347
    self._replaceXmlByForm(styles_element_tree, form, here, extra_context,
                           ooo_builder)
Tatuya Kamada's avatar
Tatuya Kamada committed
348
    styles_xml = etree.tostring(styles_element_tree, encoding='utf-8')
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
349

350
    ooo_builder.replace('styles.xml', styles_xml)
351 352

  # this method not implemented yet
353
  def _replaceMetaXml(self, ooo_builder, extra_context):
354
    """
355
    Replace meta.xml file in an ODF document.
356 357 358
    """
    return ooo_builder

359 360
  def _replaceXmlByForm(self, element_tree, form, here, extra_context,
                        ooo_builder, iteration_index=0):
361 362 363 364 365 366 367 368 369 370
    """
    Replace an element_tree object using an ERP5 form.

    Keyword arguments:
    element_tree -- the element_tree of a XML file in an ODF document.
    form -- an ERP5 form
    here -- called context
    extra_context -- extra_context
    ooo_builder -- the OOoBuilder object which have an ODF document.
    iteration_index -- the index which is used when iterating the group of items using ReportSection.
Nicolas Delaby's avatar
Nicolas Delaby committed
371

372
    Need to be overloaded in OD?Strategy Class
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
373
    """
374
    raise NotImplementedError
Nicolas Delaby's avatar
Nicolas Delaby committed
375

376 377
  def _replaceXmlByReportSection(self, element_tree, extra_context, report_method,
                                 base_name, ooo_builder):
378 379 380 381 382
    """
    Replace xml using ERP5Report ReportSection.
    Keyword arguments:
    element_tree -- the element tree object which have an xml document in an ODF document.
    extra_context -- the extra context
Fabien Morin's avatar
Fabien Morin committed
383 384
    report_method -- the report method object which is used in an ReportBox
    base_name -- the name of a ReportBox field which is used to specify the target
385 386
    ooo_builder -- the OOo Builder object which has ODF document.
    """
387
    if report_method is None:
388
      return
389 390
    report_section_list = report_method()
    portal_object = self.getPortalObject()
Tatuya Kamada's avatar
Tatuya Kamada committed
391

392
    target_tuple = self._pickUpTargetSection(base_name=base_name,
393 394 395
                                             report_section_list=report_section_list,
                                             element_tree=element_tree)
    if target_tuple is None:
396
      return
397 398 399 400
    target_xpath, original_target = target_tuple
    office_body = original_target.getparent()
    target_index = office_body.index(original_target)
    temporary_element_tree = deepcopy(original_target)
401
    for (index, report_item) in enumerate(report_section_list):
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
402
      report_item.pushReport(portal_object, render_prefix=None)
403 404 405
      here = report_item.getObject(portal_object)
      form_id = report_item.getFormId()
      form = getattr(here, form_id)
Nicolas Delaby's avatar
Nicolas Delaby committed
406

407
      target_element_tree = deepcopy(temporary_element_tree)
408
      # remove original target in the ODF template
409
      if index == 0:
410
        office_body.remove(original_target)
Tatuya Kamada's avatar
Tatuya Kamada committed
411
      else:
412
        self._setUniqueElementName(base_name=base_name,
413
                                   iteration_index=index,
414 415 416
                                   xpath=target_xpath,
                                   element_tree=target_element_tree)

417 418
      self._replaceXmlByForm(target_element_tree, form, here, extra_context,
                             ooo_builder, iteration_index=index)
419 420
      office_body.insert(target_index, target_element_tree)
      target_index += 1
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
421
      report_item.popReport(portal_object, render_prefix=None)
422

423
  def _pickUpTargetSection(self, base_name='', report_section_list=[], element_tree=None):
424
    """pick up a ODF target object to iterate ReportSection
425
    base_name -- the target name to replace in an ODF document
426 427 428
    report_section_list -- ERP5Form ReportSection List which was created by a report method
    element_tree -- XML ElementTree object
    """
429
    frame_xpath = '//draw:frame[@draw:name="%s"]' % base_name
430 431
    frame_list = element_tree.xpath(frame_xpath, namespaces=element_tree.nsmap)
    # <text:section text:style-name="Sect2" text:name="Section2">
432
    section_xpath = '//text:section[@text:name="%s"]' % base_name
433
    section_list = element_tree.xpath(section_xpath, namespaces=element_tree.nsmap)
Nicolas Delaby's avatar
Nicolas Delaby committed
434 435
    if not frame_list and not section_list:
      return
436 437 438 439

    office_body = None
    original_target = None
    target_xpath = ''
Nicolas Delaby's avatar
Nicolas Delaby committed
440
    if frame_list:
441 442 443
      frame = frame_list[0]
      original_target = frame.getparent()
      target_xpath = frame_xpath
Nicolas Delaby's avatar
Nicolas Delaby committed
444
    elif section_list:
445 446 447 448
      original_target = section_list[0]
      target_xpath = section_xpath
    office_body = original_target.getparent()
    # remove if no report section
Nicolas Delaby's avatar
Nicolas Delaby committed
449
    if not report_section_list:
450
      office_body.remove(original_target)
Nicolas Delaby's avatar
Nicolas Delaby committed
451
      return
Nicolas Delaby's avatar
Nicolas Delaby committed
452

453
    return (target_xpath, original_target)
Nicolas Delaby's avatar
Nicolas Delaby committed
454

455 456 457 458 459 460 461 462 463
  def _setUniqueElementName(self, base_name='', iteration_index=0, xpath='', element_tree=None):
    """create a unique element name and set it to the element tree

    Keyword arguments:
    base_name -- the base name of the element
    iteration_index -- iteration index
    xpath -- xpath expression which was used to search the element
    element_tree -- element tree
    """
464
    if iteration_index == 0:
Tatuya Kamada's avatar
Tatuya Kamada committed
465
      return
466
    def getNameAttribute(target_element):
467 468 469 470 471 472 473
      attrib = target_element.attrib
      for key in attrib.keys():
        if key.endswith("}name"):
          return key
      return None
    odf_element_name =  "%s_%s" % (base_name, iteration_index)
    result_list = element_tree.xpath(xpath, namespaces=element_tree.nsmap)
474
    if not result_list:
475 476 477
      return
    target_element = result_list[0]
    name_attribute = getNameAttribute(target_element)
Nicolas Delaby's avatar
Nicolas Delaby committed
478
    if name_attribute:
479
      target_element.set(name_attribute, odf_element_name)
Nicolas Delaby's avatar
Nicolas Delaby committed
480

481 482
  def _replaceXmlByFormbox(self, element_tree, field, form, extra_context,
                           ooo_builder, iteration_index=0):
483 484
    """
    Replace an ODF frame using an ERP5Form form box field.
485

486
    Note: This method is incompleted yet. This function is intended to
Fabien Morin's avatar
Fabien Morin committed
487
    make an frame hide/show. But it has not such a feature currently.
488
    """
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
489 490
    field_id = field.id
    enabled = field.get_value('enabled')
491
    draw_xpath = '//draw:frame[@draw:name="%s"]/draw:text-box/*' % field_id
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
492
    text_list = element_tree.xpath(draw_xpath, namespaces=element_tree.nsmap)
493 494
    if not text_list:
      return
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
495 496 497 498 499
    target_element = text_list[0]
    frame_paragraph = target_element.getparent()
    office_body = frame_paragraph.getparent()
    if not enabled:
      office_body.remove(frame_paragraph)
500
      return
501
    # set when using report section
502 503 504 505 506
    self._setUniqueElementName(field_id, iteration_index, draw_xpath, element_tree)
    self._replaceXmlByForm(frame_paragraph, form, extra_context['here'], extra_context,
                           ooo_builder, iteration_index=iteration_index)

  def _replaceXmlByImageField(self, element_tree, image_field, ooo_builder, iteration_index=0):
507 508 509
    """
    Replace an ODF draw:frame using an ERP5Form image field.
    """
510 511
    alt = image_field.get_value('description') or image_field.get_value('title')
    image_xpath = '//draw:frame[@draw:name="%s"]/*' % image_field.id
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
512
    image_list = element_tree.xpath(image_xpath, namespaces=element_tree.nsmap)
513 514
    if not image_list:
      return
Tatuya Kamada's avatar
Tatuya Kamada committed
515
    path = image_field.get_value('default')
516 517
    image_node = image_list[0]
    image_frame = image_node.getparent()
Tatuya Kamada's avatar
Tatuya Kamada committed
518 519
    if path is not None:
      path = path.encode()
Tatuya Kamada's avatar
Tatuya Kamada committed
520
    picture = self.getPortalObject().restrictedTraverse(path)
Tatuya Kamada's avatar
Tatuya Kamada committed
521
    picture_data = getattr(aq_base(picture), 'data', None)
522 523 524
    if picture_data is None:
      image_frame = image_node.getparent()
      image_frame.remove(image_node)
525
      return
Tatuya Kamada's avatar
Tatuya Kamada committed
526 527 528
    picture_type = picture.getContentType()
    picture_path = self._createOdfUniqueFileName(path=path, picture_type=picture_type)
    ooo_builder.addFileEntry(picture_path, media_type=picture_type, content=picture_data)
529 530 531 532
    width, height = self._getPictureSize(picture, image_frame)
    image_node.set('{%s}href' % XLINK_URI, picture_path)
    image_frame.set('{%s}width' % SVG_URI, str(width))
    image_frame.set('{%s}height' % SVG_URI, str(height))
533
    # set when using report section
534
    self._setUniqueElementName(image_field.id, iteration_index, image_xpath, element_tree)
535

Tatuya Kamada's avatar
Tatuya Kamada committed
536 537
  def _createOdfUniqueFileName(self, path='', picture_type=''):
    extension = guess_extension(picture_type)
538 539 540
    # here, it's needed to use quote_plus to escape '/' caracters to make valid
    # paths in the odf archive.
    picture_path = 'Pictures/%s%s' % (quote_plus(path), extension)
Tatuya Kamada's avatar
Tatuya Kamada committed
541 542 543 544
    if picture_path not in self.odf_existent_name_list:
      return picture_path
    number = 0
    while True:
545
      picture_path = 'Pictures/%s_%s%s' % (quote_plus(path), number, extension)
Tatuya Kamada's avatar
Tatuya Kamada committed
546 547 548 549
      if picture_path not in self.odf_existent_name_list:
        return picture_path
      number += 1

550 551 552 553
  # XXX this method should not be used anymore. This should be made by the
  # render_od*
  def _getPictureSize(self, picture=None, draw_frame_node=None):
    if picture is None or draw_frame_node is None:
Tatuya Kamada's avatar
Tatuya Kamada committed
554
      return ('0cm', '0cm')
555 556
    svg_width = draw_frame_node.attrib.get('{%s}width' % SVG_URI)
    svg_height = draw_frame_node.attrib.get('{%s}height' % SVG_URI)
Tatuya Kamada's avatar
Tatuya Kamada committed
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576
    if svg_width is None or svg_height is None:
      return ('0cm', '0cm')
    # if not match causes exception
    width_tuple = re.match("(\d[\d\.]*)(.*)", svg_width).groups()
    height_tuple = re.match("(\d[\d\.]*)(.*)", svg_height).groups()
    unit = width_tuple[1]
    w = Decimal(width_tuple[0])
    h = Decimal(height_tuple[0])
    aspect_ratio = 1
    try: # try image properties
      aspect_ratio = Decimal(picture.width) / Decimal(picture.height)
    except (TypeError, ZeroDivisionError):
      try: # try ERP5.Document.Image API
        height = Decimal(picture.getHeight())
        if height:
          aspect_ratio = Decimal(picture.getWidth()) / height
      except AttributeError: # fallback to Photo API
        height = float(picture.height())
        if height:
          aspect_ratio = Decimal(picture.width()) / height
Tatuya Kamada's avatar
Tatuya Kamada committed
577 578 579 580 581 582
    resize_w = h * aspect_ratio
    resize_h = w / aspect_ratio
    if resize_w < w:
      w = resize_w
    elif resize_h < h:
      h = resize_h
Tatuya Kamada's avatar
Tatuya Kamada committed
583
    return (str(w) + unit, str(h) + unit)
Nicolas Delaby's avatar
Nicolas Delaby committed
584 585


586
  def _appendTableByListbox(self, element_tree, listbox, REQUEST, iteration_index=0):
587 588 589
    """
    Append a ODF table using an ERP5 Form listbox.
    """
590 591 592
    table_id = listbox.id
    table_xpath = '//table:table[@table:name="%s"]' % table_id
    # this list should be one item list
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
593
    target_table_list = element_tree.xpath(table_xpath, namespaces=element_tree.nsmap)
594
    if not target_table_list:
595
      return element_tree
596 597 598

    target_table = target_table_list[0]
    newtable = deepcopy(target_table)
599

600 601
    table_header_rows_xpath = '%s/table:table-header-rows' % table_xpath
    table_row_xpath = '%s/table:table-row' % table_xpath
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
602
    table_header_rows_list = newtable.xpath(table_header_rows_xpath,  namespaces=element_tree.nsmap)
Nicolas Delaby's avatar
Nicolas Delaby committed
603
    table_row_list = newtable.xpath(table_row_xpath, namespaces=element_tree.nsmap)
604 605 606

    # copy row styles from ODF Document
    has_header_rows = len(table_header_rows_list) > 0
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
607 608
    (row_top, row_middle, row_bottom) = self._copyRowStyle(table_row_list,
                                                           has_header_rows=has_header_rows)
609 610
    # create style-name and table-row dictionary if a reference name is set
    style_name_row_dictionary = self._createStyleNameRowDictionary(table_row_list)
Fabien Morin's avatar
Fabien Morin committed
611
    # clear original table
612
    parent_paragraph = target_table.getparent()
Fabien Morin's avatar
Fabien Morin committed
613
    # clear rows
Nicolas Delaby's avatar
Nicolas Delaby committed
614
    [newtable.remove(table_row) for table_row in table_row_list]
615 616 617

    listboxline_list = listbox.get_value('default',
                                         render_format='list',
Fabien Morin's avatar
Fabien Morin committed
618
                                         REQUEST=REQUEST,
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
619
                                         render_prefix=None)
620 621
    # if ODF table has header rows, does not update the header rows
    # if does not have header rows, insert the listbox title line
622
    is_top = True
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
623
    last_index = len(listboxline_list) - 1
624 625
    for (index, listboxline) in enumerate(listboxline_list):
      listbox_column_list = listboxline.getColumnItemList()
626
      row_style_name = listboxline.getRowCSSClassName()
627 628
      if listboxline.isTitleLine() and not has_header_rows:
        row = deepcopy(row_top)
Nicolas Delaby's avatar
Nicolas Delaby committed
629
        self._updateColumnValue(row, listbox_column_list)
630
        newtable.append(row)
631
        is_top = False
632
      elif listboxline.isDataLine() and is_top:
Nicolas Delaby's avatar
Nicolas Delaby committed
633 634
        row = deepcopy(style_name_row_dictionary.get(row_style_name, row_top))
        self._updateColumnValue(row, listbox_column_list)
635 636
        newtable.append(row)
        is_top = False
Tatuya Kamada's avatar
Tatuya Kamada committed
637
      elif listboxline.isStatLine() or (index is last_index and listboxline.isDataLine()):
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
638
        row = deepcopy(row_bottom)
Nicolas Delaby's avatar
Nicolas Delaby committed
639
        self._updateColumnStatValue(row, listbox_column_list, row_middle)
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
640
        newtable.append(row)
641
      elif index > 0 and listboxline.isDataLine():
Nicolas Delaby's avatar
Nicolas Delaby committed
642 643
        row = deepcopy(style_name_row_dictionary.get(row_style_name, row_middle))
        self._updateColumnValue(row, listbox_column_list)
644 645
        newtable.append(row)

646
    self._setUniqueElementName(table_id, iteration_index, table_xpath, newtable)
Nicolas Delaby's avatar
Nicolas Delaby committed
647
    parent_paragraph.replace(target_table, newtable)
Nicolas Delaby's avatar
Nicolas Delaby committed
648

649
  def _copyRowStyle(self, table_row_list=None, has_header_rows=False):
650 651 652
    """
    Copy ODF table row styles.
    """
653 654
    if table_row_list is None:
      table_row_list = []
655 656
    def removeOfficeAttribute(row):
      if row is None or has_header_rows: return
657
      odf_cell_list = row.findall("{%s}table-cell" % TABLE_URI)
658 659
      for odf_cell in odf_cell_list:
        self._removeColumnValue(odf_cell)
Nicolas Delaby's avatar
Nicolas Delaby committed
660

661 662 663
    row_top = None
    row_middle = None
    row_bottom = None
Nicolas Delaby's avatar
Nicolas Delaby committed
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680
    len_table_row_list = len(table_row_list)
    if len_table_row_list == 1:
      row_top = deepcopy(table_row_list[0])
      row_middle = deepcopy(table_row_list[0])
      row_bottom = deepcopy(table_row_list[0])
    elif len_table_row_list == 2 and has_header_rows:
      row_top = deepcopy(table_row_list[0])
      row_middle = deepcopy(table_row_list[0])
      row_bottom = deepcopy(table_row_list[-1])
    elif len_table_row_list == 2 and not has_header_rows:
      row_top = deepcopy(table_row_list[0])
      row_middle = deepcopy(table_row_list[1])
      row_bottom = deepcopy(table_row_list[-1])
    elif len_table_row_list >= 2:
      row_top = deepcopy(table_row_list[0])
      row_middle = deepcopy(table_row_list[1])
      row_bottom = deepcopy(table_row_list[-1])
681

Fabien Morin's avatar
Fabien Morin committed
682
    # remove office attribute if create a new header row
683
    removeOfficeAttribute(row_top)
684
    return (row_top, row_middle, row_bottom)
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
685

686

Nicolas Delaby's avatar
Nicolas Delaby committed
687
  def _createStyleNameRowDictionary(self, table_row_list):
688 689 690
    """create stylename and table row dictionary if a style name reference is set"""
    style_name_row_dictionary = {}
    for table_row in table_row_list:
Nicolas Delaby's avatar
Nicolas Delaby committed
691
      reference_element = table_row.find('.//*[@%s]' % self._name_attribute_name)
692
      if reference_element is not None:
Nicolas Delaby's avatar
Nicolas Delaby committed
693
        name = reference_element.attrib[self._name_attribute_name]
694 695
        style_name_row_dictionary[name] = deepcopy(table_row)
    return style_name_row_dictionary
Nicolas Delaby's avatar
Nicolas Delaby committed
696

Nicolas Delaby's avatar
Nicolas Delaby committed
697
  def _updateColumnValue(self, row, listbox_column_list):
698
    odf_cell_list = row.findall("{%s}table-cell" % TABLE_URI)
699 700 701 702 703 704
    odf_cell_list_size = len(odf_cell_list)
    listbox_column_size = len(listbox_column_list)
    for (column_index, column) in enumerate(odf_cell_list):
      if column_index >= listbox_column_size:
        break
      value = listbox_column_list[column_index][1]
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
705
      self._setColumnValue(column, value)
706

Nicolas Delaby's avatar
Nicolas Delaby committed
707
  def _updateColumnStatValue(self, row, listbox_column_list, row_middle):
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
708
    """stat line is capable of column span setting"""
709
    if row_middle is None:
Nicolas Delaby's avatar
Nicolas Delaby committed
710
      return
711
    odf_cell_list = row.findall("{%s}table-cell" % TABLE_URI)
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
712
    odf_column_span_list = self._getOdfColumnSpanList(row_middle)
713 714 715 716 717 718
    listbox_column_size = len(listbox_column_list)
    listbox_column_index = 0
    for (column_index, column) in enumerate(odf_cell_list):
      if listbox_column_index >= listbox_column_size:
        break
      value = listbox_column_list[listbox_column_index][1]
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
719 720 721 722 723
      self._setColumnValue(column, value)
      column_span = self._getColumnSpanSize(column)
      listbox_column_index = self._nextListboxColumnIndex(column_span,
                                                          listbox_column_index,
                                                          odf_column_span_list)
724

Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
725 726
  def _setColumnValue(self, column, value):
    self._clearColumnValue(column)
727
    if value is None:
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
728
      self._removeColumnValue(column)
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
729
    column_value, table_content = self._translateValueIntoColumnContent(value, column)
Nicolas Delaby's avatar
Nicolas Delaby committed
730
    [column.remove(child) for child in column]
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
731 732 733 734 735 736 737 738 739
    if table_content is not None:
      column.append(table_content)
    value_attribute = self._getColumnValueAttribute(column)
    if value_attribute is not None and column_value is not None:
       column.set(value_attribute, column_value)

  def _translateValueIntoColumnContent(self, value, column):
    """translate a value as a table content"""
    table_content = None
Nicolas Delaby's avatar
Nicolas Delaby committed
740 741
    if len(column):
      table_content = deepcopy(column[0])
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
742 743 744 745 746
    # create a tempolaly etree object to generate a content paragraph
    fragment = self._valueAsOdfXmlElement(value=value, element_tree=column)
    column_value = None
    if table_content is not None:
      table_content.text = fragment.text
Nicolas Delaby's avatar
Nicolas Delaby committed
747
      for element in fragment:
Nicolas Delaby's avatar
Nicolas Delaby committed
748 749
        table_content.append(element)
      column_value = " ".join(table_content.itertext())
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
750 751 752 753
    return (column_value, table_content)

  def _valueAsOdfXmlElement(self, value=None, element_tree=None):
    """values as ODF XML element
Nicolas Delaby's avatar
Nicolas Delaby committed
754

Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
755 756 757 758 759 760 761 762 763 764 765
    replacing:
      \t -> tabs
      \n -> line-breaks
      DateTime -> Y-m-d
    """
    if value is None:
      value = ''
    translated_value = str(value)
    if isinstance(value, DateTime):
      translated_value = value.strftime('%Y-%m-%d')
    translated_value = escape(translated_value)
766 767
    tab_element_str = '<text:tab xmlns:text="%s"/>' % TEXT_URI
    line_break_element_str ='<text:line-break xmlns:text="%s"/>' % TEXT_URI
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
768 769 770 771 772 773
    translated_value = translated_value.replace('\t', tab_element_str)
    translated_value = translated_value.replace('\r', '')
    translated_value = translated_value.replace('\n', line_break_element_str)
    translated_value = unicode(str(translated_value),'utf-8')
    # create a paragraph
    template = '<text:p xmlns:text="%s">%s</text:p>'
774
    fragment_element_tree = etree.XML(template % (TEXT_URI, translated_value))
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
775
    return fragment_element_tree
Nicolas Delaby's avatar
Nicolas Delaby committed
776

Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
777
  def _removeColumnValue(self, column):
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
778 779 780 781 782
    # to eliminate a default value, remove "office:*" attributes.
    # if remaining these attribetes, the column shows its default value,
    # such as '0.0', '$0'
    attrib = column.attrib
    for key in attrib.keys():
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
783
      if key.startswith("{%s}" % column.nsmap['office']):
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
784
        del attrib[key]
Nicolas Delaby's avatar
Nicolas Delaby committed
785 786
    column.text = None
    [column.remove(child) for child in column]
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
787

Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
788
  def _clearColumnValue(self, column):
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
789 790
    attrib = column.attrib
    for key in attrib.keys():
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
791
      value_attribute = self._getColumnValueAttribute(column)
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
792 793
      if value_attribute is not None:
        column.set(value_attribute, '')
Nicolas Delaby's avatar
Nicolas Delaby committed
794 795
    column.text = None
    for child in column:
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
796
      # clear data except style
Nicolas Delaby's avatar
Nicolas Delaby committed
797
      style_value = child.attrib.get(self._style_attribute_name)
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
798
      child.clear()
Nicolas Delaby's avatar
Nicolas Delaby committed
799
      if style_value:
Nicolas Delaby's avatar
Nicolas Delaby committed
800
        child.set(self._style_attribute_name, style_value)
Nicolas Delaby's avatar
Nicolas Delaby committed
801

Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
802
  def _getColumnValueAttribute(self, column):
803 804 805 806 807
    attrib = column.attrib
    for key in attrib.keys():
      if key.endswith("value"):
        return key
    return None
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
808 809

  def _getColumnSpanSize(self, column=None):
810
    span_attribute = "{%s}number-columns-spanned" % TABLE_URI
Nicolas Delaby's avatar
Nicolas Delaby committed
811
    return int(column.attrib.get(span_attribute, 1))
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
812 813

  def _nextListboxColumnIndex(self, span=0, current_index=0, column_span_list=[]):
814 815 816 817 818 819 820
    hops = 0
    index = current_index
    while hops < span:
      column_span = column_span_list[index]
      hops += column_span
      index += 1
    return index
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
821 822

  def _getOdfColumnSpanList(self, row_middle=None):
823 824
    if row_middle is None:
      return []
825
    odf_cell_list = row_middle.findall("{%s}table-cell" % TABLE_URI)
826 827
    column_span_list = []
    for column in odf_cell_list:
Tatuya Kamada's avatar
* Fix  
Tatuya Kamada committed
828
      column_span = self._getColumnSpanSize(column)
829 830 831
      column_span_list.append(column_span)
    return column_span_list

Tatuya Kamada's avatar
Tatuya Kamada committed
832 833
  def _toUnicodeString(self, field_value = None):
    value = ''
Tatuya Kamada's avatar
Tatuya Kamada committed
834 835 836
    if isinstance(field_value, unicode):
      value = field_value
    elif field_value is not None:
Tatuya Kamada's avatar
Tatuya Kamada committed
837 838 839
      value = unicode(str(field_value), 'utf-8')
    return value

840 841
class ODTStrategy(ODFStrategy):
  """ODTStrategy create a ODT Document from a form and a ODT template"""
Nicolas Delaby's avatar
Nicolas Delaby committed
842 843

  _style_attribute_name = '{urn:oasis:names:tc:opendocument:xmlns:text:1.0}style-name'
Nicolas Delaby's avatar
Nicolas Delaby committed
844
  _name_attribute_name = '{urn:oasis:names:tc:opendocument:xmlns:text:1.0}name'
Nicolas Delaby's avatar
Nicolas Delaby committed
845

846 847
  def _replaceXmlByForm(self, element_tree, form, here, extra_context,
                        ooo_builder, iteration_index=0):
848 849 850 851 852 853 854 855 856 857 858
    """
    Replace an element_tree object using an ERP5 form.

    Keyword arguments:
    element_tree -- the element_tree of a XML file in an ODF document.
    form -- an ERP5 form
    here -- called context
    extra_context -- extra_context
    ooo_builder -- the OOoBuilder object which have an ODF document.
    iteration_index -- the index which is used when iterating the group of items using ReportSection.
    """
Fabien Morin's avatar
Fabien Morin committed
859
    field_list = form.get_fields(include_disabled=1)
860
    REQUEST = here.REQUEST
861 862
    for (count, field) in enumerate(field_list):
      if isinstance(field, ListBox):
863 864
        self._appendTableByListbox(element_tree, field, REQUEST,
                                   iteration_index=iteration_index)
865 866 867 868
      elif isinstance(field, FormBox):
        if not hasattr(here, field.get_value('formbox_target_id')):
          continue
        sub_form = getattr(here, field.get_value('formbox_target_id'))
869 870
        content = self._replaceXmlByFormbox(element_tree, field, sub_form,
                                            extra_context, ooo_builder,
871 872 873
                                            iteration_index=iteration_index)
      elif isinstance(field, ReportBox):
         report_method = getattr(field, field.get_value('report_method'), None)
874 875
         self._replaceXmlByReportSection(element_tree, extra_context,
                                         report_method, field.id, ooo_builder)
876
      elif isinstance(field, ImageField):
877 878
        self._replaceXmlByImageField(element_tree, field,
                                     ooo_builder, iteration_index=iteration_index)
879
      else:
880
        self._replaceNodeViaReference(element_tree, field)
881

882
  def _replaceNodeViaReference(self, element_tree, field):
883
    """replace nodes (e.g. paragraphs) via ODF reference"""
884 885
    self._replaceNodeViaRangeReference(element_tree, field)
    self._replaceNodeViaPointReference(element_tree, field)
886
    self._replaceNodeViaFormName(element_tree, field)
887

888
  def _replaceNodeViaPointReference(self, element_tree, field, iteration_index=0):
889 890 891 892 893 894 895 896
    """Replace text node via an ODF point reference.

    point reference example:
     <text:reference-mark text:name="invoice-date"/>
    """
    field_id = field.id
    reference_xpath = '//text:reference-mark[@text:name="%s"]' % field_id
    reference_list = element_tree.xpath(reference_xpath, namespaces=element_tree.nsmap)
Nicolas Delaby's avatar
Nicolas Delaby committed
897 898 899 900 901 902
    for target_node in reference_list:
      node_to_replace = target_node.xpath('ancestor::text:p[1]', namespaces=element_tree.nsmap)[0]
      attr_dict = {}
      style_value = node_to_replace.attrib.get(self._style_attribute_name)
      if style_value:
        attr_dict.update({self._style_attribute_name: style_value})
903
      new_node = field.render_odt(as_string=False, attr_dict=attr_dict)
Nicolas Delaby's avatar
Nicolas Delaby committed
904
      node_to_replace.getparent().replace(node_to_replace, new_node)
905 906 907 908 909 910
    # set when using report section
    self._setUniqueElementName(base_name=field.id,
                               iteration_index=iteration_index,
                               xpath=reference_xpath,
                               element_tree=element_tree)

911
  def _replaceNodeViaRangeReference(self, element_tree, field, iteration_index=0):
912 913 914 915 916
    """Replace text node via an ODF ranged reference.

    range reference example:
    <text:reference-mark-start text:name="week"/>Monday<text:reference-mark-end text:name="week"/>
    or
Nicolas Delaby's avatar
Nicolas Delaby committed
917 918
    <text:reference-mark-start text:name="my_title"/>
      <text:span text:style-name="T1">title</text:span>
919 920 921
    <text:reference-mark-end text:name="my_title"/>

    """
Nicolas Delaby's avatar
Nicolas Delaby committed
922
    field_id = field.id
923 924
    range_reference_xpath = '//text:reference-mark-start[@text:name="%s"]' % (field_id,)
    node_to_remove_list_xpath = '//text:reference-mark-start[@text:name="%s"]/'\
Nicolas Delaby's avatar
Nicolas Delaby committed
925 926
                            'following-sibling::*[node()/'\
                            'following::text:reference-mark-end[@text:name="%s"]]' % (field_id, field_id)
927
    node_to_remove_list = element_tree.xpath(node_to_remove_list_xpath, namespaces=element_tree.nsmap)
928
    reference_list = element_tree.xpath(range_reference_xpath, namespaces=element_tree.nsmap)
Nicolas Delaby's avatar
Nicolas Delaby committed
929
    if not reference_list:
930
      return element_tree
931
    referenced_node = reference_list[0]
932
    referenced_node.tail = None
933
    parent_node = referenced_node.getparent()
934
    text_reference_position = parent_node.index(referenced_node)
Nicolas Delaby's avatar
Nicolas Delaby committed
935 936 937 938

    #Delete all contents between <text:reference-mark-start/> and <text:reference-mark-end/>
    #Try to fetch style-name
    attr_dict = {}
939
    [(attr_dict.update(target_node.attrib), parent_node.remove(target_node)) for target_node in node_to_remove_list]
940 941
    new_node = field.render_odt(local_name='span', attr_dict=attr_dict,
                                as_string=False)
Nicolas Delaby's avatar
Nicolas Delaby committed
942
    parent_node.insert(text_reference_position+1, new_node)
943 944 945 946 947
    # set when using report section
    self._setUniqueElementName(base_name=field.id,
                               iteration_index=iteration_index,
                               xpath=range_reference_xpath,
                               element_tree=element_tree)
948

949 950 951 952 953 954 955 956 957 958
  def _replaceNodeViaFormName(self, element_tree, field, iteration_index=0):
    """
    Used to replace field in ODT document like checkboxes
    """
    field_id = field.id
    reference_xpath = '//*[@form:name = "%s"]' % field_id
    reference_list = element_tree.xpath(reference_xpath, namespaces=element_tree.nsmap)
    for target_node in reference_list:
      attr_dict = {}
      attr_dict.update(target_node.attrib)
959
      new_node = field.render_odt(as_string=False, attr_dict=attr_dict)
960 961
      target_node.getparent().replace(target_node, new_node)

962 963 964
class ODGStrategy(ODFStrategy):
  """ODGStrategy create a ODG Document from a form and a ODG template"""

965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996
  def _recursiveGetAttributeDict(self, node, attr_dict):
    '''return a dictionnary corresponding with node attributes. Tag as key
       and a list corresponding to the atributes values by apparence order.
       Example, for a listbox, you will have something like :
       { tabe.tag: [table.attrib,],
         row.tag: [row.attrib,
                   row.attrib],
         cell.tag: [cell.attrib,
                    cell.attrib,
                    cell.attrib,
                    cell.attrib,
                    cell.attrib,
                    cell.attrib,],

    '''
    attr_dict.setdefault(node.tag, []).append(dict(node.attrib))
    for child in node:
      self._recursiveGetAttributeDict(child, attr_dict)

  def _recursiveApplyAttributeDict(self, node, attr_dict):
    '''recursively apply given attributes to node
    '''
    image_tag_name = '{%s}%s' % (DRAW_URI, 'image')
    if len(attr_dict[node.tag]):
      attribute_to_update_dict = attr_dict[node.tag].pop(0)
      # in case of images, we don't want to update image path
      # because they were calculated by render_odg
      if node.tag != image_tag_name:
        node.attrib.update(attribute_to_update_dict)
    for child in node:
      self._recursiveApplyAttributeDict(child, attr_dict)

Fabien Morin's avatar
Fabien Morin committed
997 998
  def _replaceXmlByForm(self, element_tree, form, here, extra_context,
                        ooo_builder, iteration_index=0):
999 1000
    field_list = form.get_fields(include_disabled=1)
    for (count, field) in enumerate(field_list):
1001
      text_xpath = '//draw:frame[@draw:name="%s"]' % field.id
1002
      node_list = element_tree.xpath(text_xpath, namespaces=element_tree.nsmap)
1003
      value = field.get_value('default')
1004 1005
      if isinstance(value, str):
        value = value.decode('utf-8')
Fabien Morin's avatar
Fabien Morin committed
1006
      for target_node in node_list:
1007
        # render the field in odg xml node format
1008 1009 1010 1011 1012
        attr_dict = {}
        self._recursiveGetAttributeDict(target_node, attr_dict)
        new_node = field.render_odg(value=value, as_string=False, ooo_builder=ooo_builder,
            REQUEST=self.REQUEST, attr_dict=attr_dict)

1013
        if new_node is not None:
1014
          # replace the target node by the generated node
1015 1016 1017 1018
          target_node.getparent().replace(target_node, new_node)
        else:
          # if the render return None, remove the node
          target_node.getparent().remove(target_node)