Form.py 43.5 KB
Newer Older
1
#############################################################################
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#
# 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
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

29 30
from copy import deepcopy

Jean-Paul Smets's avatar
Jean-Paul Smets committed
31 32
from Products.Formulator.Form import Form, BasicForm, ZMIForm
from Products.Formulator.Form import manage_addForm, manage_add, initializeForm
33
from Products.Formulator.Errors import FormValidationError, ValidationError
Jean-Paul Smets's avatar
Jean-Paul Smets committed
34 35 36
from Products.Formulator.DummyField import fields
from Products.Formulator.XMLToForm import XMLToForm
from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
37
from Products.CMFCore.utils import _checkPermission, getToolByName
38 39
from Products.CMFCore.exceptions import AccessControl_Unauthorized
from Products.ERP5Type import PropertySheet, Permissions
Jean-Paul Smets's avatar
Jean-Paul Smets committed
40 41

from urllib import quote
42
from Globals import InitializeClass, PersistentMapping, DTMLFile, get_request
Jean-Paul Smets's avatar
Jean-Paul Smets committed
43
from AccessControl import Unauthorized, getSecurityManager, ClassSecurityInfo
Yoshinori Okuji's avatar
Yoshinori Okuji committed
44
from ZODB.POSException import ConflictError
45
from Acquisition import aq_base
46
from Products.PageTemplates.Expressions import SecureModuleImporter
Jean-Paul Smets's avatar
Jean-Paul Smets committed
47 48
from Products.ERP5Type.Utils import UpperCase

49
from Products.ERP5Type.PsycoWrapper import psyco
50
import sys
Jean-Paul Smets's avatar
Jean-Paul Smets committed
51

52 53 54 55
_field_value_cache = {}
def purgeFieldValueCache():
  _field_value_cache.clear()

Jean-Paul Smets's avatar
Jean-Paul Smets committed
56 57 58
# Patch the fiels methods to provide improved namespace handling

from Products.Formulator.Field import Field
59
from Products.Formulator.MethodField import Method, BoundMethod
60
from Products.Formulator.TALESField import TALESMethod
Jean-Paul Smets's avatar
Jean-Paul Smets committed
61

62
from zLOG import LOG, PROBLEM
Jean-Paul Smets's avatar
Jean-Paul Smets committed
63

64

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
def isCacheable(value):
  value = aq_base(value)
  if type(value) is BoundMethod:
    return False

  jar = getattr(value, '_p_jar', None)
  if jar is not None:
    return False

  dic = getattr(value, '__dict__', None)
  if dic is not None:
    for i in dic.values():
      jar = getattr(i, '_p_jar', None)
      if jar is not None:
        return False
  return True


83 84 85 86 87 88 89 90
def copyMethod(value):
    if type(aq_base(value)) is Method:
      value = Method(value.method_name)
    elif type(aq_base(value)) is TALESMethod:
      value = TALESMethod(value._text)
    return value


91 92 93 94 95 96 97 98
class StaticValue:
  """
    Encapsulated a static value in a class
    (quite heavy, would be faster to store the
    value as is)
  """
  def __init__(self, value):
    self.value = value
99

100 101
  def __call__(self, field, id, **kw):
    return self.returnValue(field, id, self.value)
102

103
  def returnValue(self, field, id, value):
104 105
    # if normal value is a callable itself, wrap it
    if callable(value):
106 107
      value = value.__of__(field)
      #value=value() # Mising call ??? XXX Make sure compatible with listbox methods
108 109

    if id == 'default':
110 111 112 113 114
      # We make sure we convert values to empty strings
      # for most fields (so that we do not get a 'value'
      # message on screen)
      # This can be overriden by using TALES in the field
      if value is None: value = ''
115

116 117
    return value

118 119 120 121 122 123 124 125 126 127
class TALESValue(StaticValue):
  def __init__(self, tales_expr):
    self.tales_expr = tales_expr

  def __call__(self, field, id, **kw):
    REQUEST = get_request()
    if REQUEST is not None:
      # Proxyfield stores the "real" field in the request. Look if the
      # corresponding field exists in request, and use it as field in the
      # TALES context 
128 129 130
      field = REQUEST.get(
        'field__proxyfield_%s_%s_%s' % (field.id, field._p_oid, id),
        field)
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172

    kw['field'] = field

    form = field.aq_parent # XXX (JPS) form for default is wrong apparently in listbox - double check
    obj = getattr(form, 'aq_parent', None)
    if obj is not None:
        container = obj.aq_inner.aq_parent
    else:
        container = None

    kw['form'] = form
    kw['request'] = REQUEST
    kw['here'] = obj
    kw['context'] = obj
    kw['modules'] = SecureModuleImporter
    kw['container'] = container
    try :
      kw['preferences'] = obj.getPortalObject().portal_preferences
    except AttributeError :
      LOG('ERP5Form', PROBLEM,
          'portal_preferences not put in TALES context (not installed?)')
    # This allows to pass some pointer to the local object
    # through the REQUEST parameter. Not very clean.
    # Used by ListBox to render different items in a list
    if kw.has_key('REQUEST') and kw.get('cell', None) is None:
      if getattr(kw['REQUEST'],'cell', None) is not None:
        kw['cell'] = getattr(kw['REQUEST'],'cell')
      else:
        kw['cell'] = kw['REQUEST']
    elif kw.get('cell', None) is None:
      if getattr(REQUEST, 'cell', None) is not None:
        kw['cell'] = getattr(REQUEST, 'cell')
    try:
      value = self.tales_expr.__of__(field)(**kw)
    except (ConflictError, RuntimeError):
      raise
    except:
      # We add this safety exception to make sure we always get
      # something reasonable rather than generate plenty of errors
      LOG('ERP5Form', PROBLEM,
          'Field.get_value ( %s/%s [%s]), exception on tales_expr: ' %
          ( form.getId(), field.getId(), id), error=sys.exc_info())
173 174 175 176 177
      # field may be ProxyField
      try:
        value = field.get_recursive_orig_value(id)
      except AttributeError:
        value = field.get_orig_value(id)
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197

    return self.returnValue(field, id, value)

class OverrideValue(StaticValue):
  def __init__(self, override):
    self.override = override

  def __call__(self, field, id, **kw):
    return self.returnValue(field, id, self.override.__of__(field)())

class DefaultValue(StaticValue):
  def __init__(self, field_id, value):
    self.key = field_id[3:]
    self.value = value

  def __call__(self, field, id, **kw):
    try:
      form = field.aq_parent
      ob = getattr(form, 'aq_parent', None)
      value = self.value
198 199 200 201 202 203 204 205 206 207 208 209 210
      try:
        if value not in (None, ''):
          # If a default value is defined on the field, it has precedence
          value = ob.getProperty(self.key, d=value)
        else:
          # else we should give a chance to the accessor to provide
          # a default value (including None)
          value = ob.getProperty(self.key)
      except Unauthorized:
        value = ob.getProperty(self.key, d=value, checked_permission='View')
        REQUEST = get_request()
        if REQUEST is not None:
          REQUEST.set('read_only_%s' % self.key, 1)
211 212 213 214
    except (KeyError, AttributeError):
      value = None
    return self.returnValue(field, id, value)

215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
class DefaultCheckBoxValue(DefaultValue):
  def __call__(self, field, id, **kw):
    try:
      form = field.aq_parent
      ob = getattr(form, 'aq_parent', None)
      value = self.value
      try:
        value = ob.getProperty(self.key)
      except Unauthorized:
        value = ob.getProperty(self.key, d=value, checked_permission='View')
        REQUEST = get_request()
        if REQUEST is not None:
          REQUEST.set('read_only_%s' % self.key, 1)
    except (KeyError, AttributeError):
      value = None
    return self.returnValue(field, id, value)

232 233 234 235 236 237 238 239 240 241
class EditableValue(StaticValue):

  def __call__(self, field, id, **kw):
    # By default, pages are editable and
    # fields are editable if they are set to editable mode
    # However, if the REQUEST defines editable_mode to 0
    # then all fields become read only.
    # This is useful to render ERP5 content as in a web site (ECommerce)
    # editable_mode should be set for example by the page template
    # which defines the current layout
242
    if kw.get('REQUEST', None) is not None:
243
      if not getattr(kw['REQUEST'], 'editable_mode', 1):
244
        return 0
245 246 247 248
    return self.value

def getFieldValue(self, field, id, **kw):
  """
249
    Return a callable expression and cacheable boolean flag
250 251 252 253 254 255
  """
  tales_expr = self.tales.get(id, "")
  if tales_expr:
    # TALESMethod is persistent object, so that we cannot cache original one.
    # Becase if connection which original talesmethod uses is closed,
    # RuntimeError must occurs in __setstate__.
256 257
    tales_expr = copyMethod(tales_expr)
    return TALESValue(tales_expr), isCacheable(tales_expr)
258 259 260

  override = self.overrides.get(id, "")
  if override:
261 262
    override = copyMethod(override)
    return OverrideValue(override), isCacheable(override)
263 264 265

  # Get a normal value.
  value = self.get_orig_value(id)
266 267
  value = copyMethod(value)
  cacheable = isCacheable(value)
268 269 270 271

  field_id = field.id

  if id == 'default' and field_id.startswith('my_'):
272 273 274 275
    if field.meta_type == 'ProxyField' and \
        field.getRecursiveTemplateField().meta_type == 'CheckBoxField' or \
        self.meta_type == 'CheckBoxField':
      return DefaultCheckBoxValue(field_id, value), cacheable
276
    return DefaultValue(field_id, value), cacheable
277 278 279

  # For the 'editable' value, we try to get a default value
  if id == 'editable':
280
    return EditableValue(value), cacheable
281

Jean-Paul Smets's avatar
Jean-Paul Smets committed
282
  # Return default value in callable mode
283
  if callable(value):
284
    return StaticValue(value), cacheable
285 286

  # Return default value in non callable mode
287 288
  return_value = StaticValue(value)(field, id, **kw)
  return return_value, isCacheable(return_value)
289 290 291 292

def get_value(self, id, **kw):
  REQUEST = get_request()
  if REQUEST is not None:
293 294 295
    field = REQUEST.get(
      'field__proxyfield_%s_%s_%s' % (self.id, self._p_oid, id),
      self)
296 297 298
  else:
    field = self

299 300 301 302 303 304 305 306
  # If field is not stored in zodb, then must use original get_value instead.
  # Because field which is not stored in zodb must be used for editing field
  # in ZMI and field value cache sometimes break these field settings at
  # initialization. As the result, we will see broken field editing screen
  # in ZMI.
  if self._p_oid is None:
    return self._original_get_value(id, **kw)

307 308 309 310
  cache_id = ('Form.get_value',
              self._p_oid,
              field._p_oid,
              id)
311

312 313 314 315 316 317 318 319
  try:
    value = _field_value_cache[cache_id]
  except KeyError:
    # either returns non callable value (ex. "Title")
    # or a FieldValue instance of appropriate class
    value, cacheable = getFieldValue(self, field, id, **kw)
    if cacheable:
      _field_value_cache[cache_id] = value
320 321 322 323 324

  if callable(value):
    return value(field, id, **kw)
  return value

325 326 327 328 329 330 331 332 333 334 335 336 337
psyco.bind(get_value)

def om_icons(self):
    """Return a list of icon URLs to be displayed by an ObjectManager"""
    icons = ({'path': self.icon,
              'alt': self.meta_type, 'title': self.meta_type},)
    return icons


def _get_default(self, key, value, REQUEST):
    if value is not None:
        return value
    try:
338
        value = self._get_user_input_value(key, REQUEST)
339 340
    except (KeyError, AttributeError):
        # fall back on default
341
        return self.get_value('default', REQUEST=REQUEST) # It was missing on Formulator
342

343 344 345 346 347 348 349 350 351
    # if we enter a string value while the field expects unicode,
    # convert to unicode first
    # this solves a problem when re-rendering a sticky form with
    # values from request
    if (self.has_value('unicode') and self.get_value('unicode') and
        type(value) == type('')):
        return unicode(value, self.get_form_encoding())
    else:
        return value
352 353


Jean-Paul Smets's avatar
Jean-Paul Smets committed
354
# Dynamic Patch
355
original_get_value = Field.get_value
356
Field.get_value = get_value
357
Field._original_get_value = original_get_value
358 359
Field._get_default = _get_default
Field.om_icons = om_icons
Jean-Paul Smets's avatar
Jean-Paul Smets committed
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385

# Constructors

manage_addForm = DTMLFile("dtml/form_add", globals())

def addERP5Form(self, id, title="", REQUEST=None):
    """Add form to folder.
    id     -- the id of the new form to add
    title  -- the title of the form to add
    Result -- empty string
    """
    # add actual object
    id = self._setObject(id, ERP5Form(id, title))
    # 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.
    id -- id of the object we just added
    """
    if REQUEST is None:
        return
    try:
        u = self.DestinationURL()
Yoshinori Okuji's avatar
Yoshinori Okuji committed
386
    except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
387 388 389 390 391
        u = REQUEST['URL1']
    if REQUEST['submit'] == " Add and Edit ":
        u = "%s/%s" % (u, quote(id))
    REQUEST.RESPONSE.redirect(u+'/manage_main')

392
def initializeForm(field_registry, form_class=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
393 394
    """Sets up ZMIForm with fields from field_registry.
    """
395
    if form_class is None: form_class = ERP5Form
396

Jean-Paul Smets's avatar
Jean-Paul Smets committed
397 398 399 400 401 402 403 404
    meta_types = []
    for meta_type, field in field_registry.get_field_classes().items():
        # don't set up in form if this is a field for internal use only
        if field.internal_field:
            continue

        # set up individual add dictionaries for meta_types
        dict = { 'name': field.meta_type,
405
                 'permission': 'Add Formulator Fields',
Jean-Paul Smets's avatar
Jean-Paul Smets committed
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
                 'action':
                 'manage_addProduct/Formulator/manage_add%sForm' % meta_type }
        meta_types.append(dict)
        # set up add method
        setattr(form_class,
                'manage_add%sForm' % meta_type,
                DTMLFile('dtml/fieldAdd', globals(), fieldname=meta_type))

    # set up meta_types that can be added to form
    form_class._meta_types = tuple(meta_types)

    # set up settings form
    form_class.settings_form._realize_fields()

# Special Settings

def create_settings_form():
    """Create settings form for ZMIForm.
    """
    form = BasicForm('manage_settings')

    title = fields.StringField('title',
                               title="Title",
                               required=0,
                               default="")
431 432 433 434
    description = fields.TextAreaField('description',
                               title="Description",
                               required=0,
                               default="")
Jean-Paul Smets's avatar
Jean-Paul Smets committed
435
    row_length = fields.IntegerField('row_length',
436 437 438
                                     title='Number of groups in row (in order tab)',
                                     required=1,
                                     default=4)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
439 440 441 442 443 444 445 446 447 448 449 450
    name = fields.StringField('name',
                              title="Form name",
                              required=0,
                              default="")
    pt = fields.StringField('pt',
                              title="Page Template",
                              required=0,
                              default="")
    action = fields.StringField('action',
                                title='Form action',
                                required=0,
                                default="")
451 452 453 454
    update_action = fields.StringField('update_action',
                                title='Form update action',
                                required=0,
                                default="")
Jean-Paul Smets's avatar
Jean-Paul Smets committed
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
    method = fields.ListField('method',
                              title='Form method',
                              items=[('POST', 'POST'),
                                     ('GET', 'GET')],
                              required=1,
                              size=1,
                              default='POST')
    enctype = fields.ListField('enctype',
                               title='Form enctype',
                               items=[('No enctype', ""),
                                      ('application/x-www-form-urlencoded',
                                       'application/x-www-form-urlencoded'),
                                      ('multipart/form-data',
                                       'multipart/form-data')],
                               required=0,
                               size=1,
                               default=None)
472 473 474 475 476 477
    encoding = fields.StringField('encoding',
                                  title='Encoding of pages the form is in',
                                  default="UTF-8",
                                  required=1)
    stored_encoding = fields.StringField('stored_encoding',
                                      title='Encoding of form properties',
478
                                      default='UTF-8',
479
                                      required=1)
480 481 482 483
    unicode_mode = fields.CheckBoxField('unicode_mode',
                                        title='Form properties are unicode',
                                        default=0,
                                        required=1)
484 485 486
    edit_order = fields.LinesField('edit_order',
                                   title='Setters for these properties should be'
                                   '<br /> called by edit() in the defined order')
487

488
    form.add_fields([title, description, row_length, name, pt, action, update_action, method,
489
                     enctype, encoding, stored_encoding, unicode_mode, edit_order])
Jean-Paul Smets's avatar
Jean-Paul Smets committed
490 491 492 493 494 495 496 497 498 499 500 501 502
    return form

class ERP5Form(ZMIForm, ZopePageTemplate):
    """
        A Formulator form with a built-in rendering parameter based
        on page templates or DTML.
    """
    meta_type = "ERP5 Form"
    icon = "www/Form.png"

    # Declarative Security
    security = ClassSecurityInfo()

503 504
    # Tabs in ZMI
    manage_options = (ZMIForm.manage_options[:5] +
505 506 507
                      ({'label':'Proxify', 'action':'formProxify'},
                       {'label':'UnProxify', 'action':'formUnProxify'}
                      )+
508 509
                      ZMIForm.manage_options[5:])

Jean-Paul Smets's avatar
Jean-Paul Smets committed
510 511 512 513 514 515 516
    # Declarative properties
    property_sheets = ( PropertySheet.Base
                      , PropertySheet.SimpleItem)

    # Constructors
    constructors =   (manage_addForm, addERP5Form)

517 518 519 520
    # This is a patched dtml formOrder
    security.declareProtected('View management screens', 'formOrder')
    formOrder = DTMLFile('dtml/formOrder', globals())

521 522 523 524
    # Proxify form
    security.declareProtected('View management screens', 'formProxify')
    formProxify = DTMLFile('dtml/formProxify', globals())

525 526 527 528
    # Proxify form
    security.declareProtected('View management screens', 'formUnProxify')
    formUnProxify = DTMLFile('dtml/formUnProxify', globals())

Jean-Paul Smets's avatar
Jean-Paul Smets committed
529 530
    # Default Attributes
    pt = 'form_view'
531
    update_action = ''
532
    edit_order = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
533 534 535 536

    # Special Settings
    settings_form = create_settings_form()

537
    def __init__(self, id, title, unicode_mode=0, encoding='UTF-8',
Romain Courteaud's avatar
Romain Courteaud committed
538
                 stored_encoding='UTF-8'):
539 540 541 542 543 544 545 546 547 548
        """Initialize form.
        id    -- id of form
        title -- the title of the form
        """
        ZMIForm.inheritedAttribute('__init__')(self, "", "POST", "", id,
                                               encoding, stored_encoding,
                                               unicode_mode)
        self.id = id
        self.title = title
        self.row_length = 4
549
        self.group_list = ["left", "right", "center", "bottom", "hidden"]
550 551 552 553
        groups = {}
        for group in self.group_list:
          groups[group] = []
        self.groups = groups
554

Jean-Paul Smets's avatar
Jean-Paul Smets committed
555 556
    # Proxy method to PageTemplate
    def __call__(self, *args, **kwargs):
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577
        # Security
        #
        # The minimal action consists in checking that
        # we have View permission on the current object
        # before rendering a form. Otherwise, object with
        # AccessContentInformation can be viewed by invoking
        # a form directly.
        #
        # What would be better is to prevent calling certain
        # forms to render objects. This can not be done
        # through actions since we are using sometimes forms
        # to render the results of a report dialog form.
        # An a appropriate solutions could consist in adding
        # a permission field to the form. Another solutions
        # is the use of REFERER in the rendering process.
        #
        # Both solutions are not perfect if the goal is, for
        # example, to prevent displaying private information of
        # staff. The only real solution is to use a special
        # permission (ex. AccessPrivateInformation) for those
        # properties which are sensitive.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
578 579 580
        if not kwargs.has_key('args'):
            kwargs['args'] = args
        form = self
581 582 583 584 585
        obj = getattr(form, '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.')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
586 587 588
        else:
          container = None
        pt = getattr(self,self.pt)
589 590 591 592 593
        extra_context = dict( container=container,
                              template=self,
                              form=self,
                              options=kwargs,
                              here=obj )
Jean-Paul Smets's avatar
Jean-Paul Smets committed
594 595 596 597 598 599
        return pt.pt_render(extra_context=extra_context)

    def _exec(self, bound_names, args, kw):
        pt = getattr(self,self.pt)
        return pt._exec(self, bound_names, args, kw)

600 601 602 603 604 605 606 607 608 609 610 611 612
    def manage_renameObject(self, id, new_id, REQUEST=None):
      # overriden to keep the order of a field after rename
      groups = deepcopy(self.groups)
      ret = ZMIForm.manage_renameObject(self, id, new_id, REQUEST=REQUEST)
      for group_id, field_id_list in groups.items():
        if id in field_id_list:
          index = field_id_list.index(id)
          field_id_list.pop(index)
          field_id_list.insert(index, new_id)
          groups[group_id] = field_id_list
      self.groups = groups
      return ret

Jean-Paul Smets's avatar
Jean-Paul Smets committed
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629
    # Utilities
    def ErrorFields(self, validation_errors):
        """
            Create a dictionnary of validation_errors
            with field id as key
        """
        ef = {}
        for e in validation_errors.errors:
            ef[e.field_id] = e
        return ef

    def om_icons(self):
        """Return a list of icon URLs to be displayed by an ObjectManager"""
        icons = ({'path': 'misc_/ERP5Form/Form.png',
                  'alt': self.meta_type, 'title': self.meta_type},)
        return icons

630 631 632 633 634 635 636 637 638
    # Pached validate_all to support ListBox validation
    security.declareProtected('View', 'validate_all')
    def validate_all(self, REQUEST):
        """Validate all enabled fields in this form, catch any ValidationErrors
        if they occur and raise a FormValidationError in the end if any
        Validation Errors occured.
        """
        result = {}
        errors = []
639 640
        for group in self.get_groups():
            if group.lower() == 'hidden':
641
                continue
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664
            for field in self.get_fields_in_group(group):
                # skip any field we don't need to validate
                if not field.need_validate(REQUEST):
                    continue
                if not (field.get_value('editable',REQUEST=REQUEST)):
                    continue
                try:
                    value = field.validate(REQUEST)
                    # store under id
                    result[field.id] = value
                    # store as alternate name as well if necessary
                    alternate_name = field.get_value('alternate_name')
                    if alternate_name:
                        result[alternate_name] = value
                except FormValidationError, e: # XXX JPS Patch for listbox
                    #LOG('validate_all', 0, 'FormValidationError: field = %s, errors=%s' % (repr(field), repr(errors)))
                    errors.extend(e.errors)
                    result.update(e.result)
                except ValidationError, err:
                    #LOG('validate_all', 0, 'ValidationError: field.id = %s, err=%s' % (repr(field.id), repr(err)))
                    errors.append(err)
                except KeyError, err:
                    LOG('ERP5Form/Form.py:validate_all', 0, 'KeyError : %s' % (err, ))
665
                
666 667 668 669
        if len(errors) > 0:
            raise FormValidationError(errors, result)
        return result

Jean-Paul Smets's avatar
Jean-Paul Smets committed
670 671 672 673 674 675 676 677 678 679 680 681
    # FTP/DAV Access
    manage_FTPget = ZMIForm.get_xml

    def PUT(self, REQUEST, RESPONSE):
        """Handle HTTP PUT requests."""
        self.dav__init(REQUEST, RESPONSE)
        self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1)
        body=REQUEST.get('BODY', '')
        # Empty the form (XMLToForm is unable to empty things before reopening)
        for k in self.get_field_ids():
          try:
            self._delObject(k)
Yoshinori Okuji's avatar
Yoshinori Okuji committed
682
          except AttributeError:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
683 684 685 686 687 688 689 690 691 692 693
            pass
        self.groups = {}
        self.group_list = []
        # And reimport
        XMLToForm(body, self)
        self.ZCacheable_invalidate()
        RESPONSE.setStatus(204)
        return RESPONSE

    manage_FTPput = PUT

694
    #Methods for Proxify tab.
695 696 697 698
    security.declareProtected('View management screens', 'getFormFieldList')
    def getFormFieldList(self):
        """
        find fields and forms which name ends with 'FieldLibrary' in
699
        the same business template or in erp5_core.
700
        """
701 702 703
        form_list = []
        def iterate(obj):
            for i in obj.objectValues():
704
                if (i.meta_type=='ERP5 Form' and
705 706
                    i.id.startswith('Base_view') and
                    i.id.endswith('FieldLibrary') and 
707
                    '_view' in i.getId()):
708 709 710 711 712 713 714
                    form_id = i.getId()
                    form_path = '%s.%s' % (obj.getId(), form_id)
                    field_list = []
                    form_list.append({'form_path':form_path,
                                      'form_id':form_id,
                                      'field_list':field_list})
                    for field in i.objectValues():
715 716 717
                        field_type, proxy_flag = get_field_meta_type_and_proxy_flag(field)
                        if proxy_flag:
                            field_type = '%s(Proxy)' % field_type
718 719 720 721 722
                        field_list.append({'field_object':field,
                                           'field_type':field_type,
                                           'proxy_flag':proxy_flag})
                if i.meta_type=='Folder':
                    iterate(i)
723 724 725 726 727 728 729 730 731 732

        skins_tool = self.portal_skins
        folder_id = self.aq_parent.id
        # Find a business template which manages the context skin folder.
        for template in self.portal_templates.getInstalledBusinessTemplateList():
          if folder_id in template.getTemplateSkinIdList():
            for skin_folder_id in template.getTemplateSkinIdList():
              iterate(getattr(skins_tool, skin_folder_id))
              break
        iterate(skins_tool.erp5_core)
733 734 735 736 737
        return form_list

    security.declareProtected('View management screens', 'getProxyableFieldList')
    def getProxyableFieldList(self, field, form_field_list=None):
        """"""
738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756
        def extract_keyword(name):
            return [i for i in name.split('_') if not i in ('my', 'default')]

        def check_keyword_list(name, keyword_list):
            count = 0
            for i in keyword_list:
                if i in name:
                    count += 1
            return count/float(len(keyword_list))

        def match(field_data):
            if not field_data['field_type'].startswith(field.meta_type):
                return 0
            field_object = field_data['field_object']
            if field_object.aq_base is field.aq_base:
                return 0
            field_id = field_object.getId()
            if id_.startswith('my_') and not field_id.startswith('my_'):
                return 0
757 758 759 760 761
            # XXX keyword match is not useful anymore.Need different approach.
            keyword_match_rate = check_keyword_list(field_id, extract_keyword(id_))
            if keyword_match_rate>0.5:
                return keyword_match_rate
            else:
Yusei Tahara's avatar
Yusei Tahara committed
762 763
                def split(string):
                    result = []
764
                    temporary = []
Yusei Tahara's avatar
Yusei Tahara committed
765 766 767 768 769 770 771 772 773 774 775 776 777
                    for char in string:
                        if char.isupper():
                            if temporary:
                                result.append(''.join(temporary))
                            temporary = []
                        temporary.append(char)
                    result.append(''.join(temporary))
                    return result

                if ''.join(field_id.split('_')[1:]).startswith(
                    split(field.meta_type)[0].lower()):
                    # At least it seems a generic template field of the meta_type.
                    return 0.1
778 779 780 781 782 783 784 785 786 787 788 789 790 791

        def make_dict_list_append_function(dic, order_list):
            def append(key, item):
                if not key in order_list:
                    order_list.append(key)
                    dic[key] = []
                dic[key].append(item)
            return append

        def add_default_field_library():
            portal_url = getToolByName(self, 'portal_url')
            portal = portal_url.getPortalObject()
            portal_skins = getToolByName(self, 'portal_skins')

Jérome Perrin's avatar
Jérome Perrin committed
792 793 794
            default_field_library_path = portal.getProperty(
                                  'erp5_default_field_library_path',
                                  'erp5_core.Base_viewFieldLibrary')
795 796 797 798 799 800 801 802 803 804
            if (not default_field_library_path or
                len(default_field_library_path.split('.'))!=2):
                return

            skinfolder_id, form_id = default_field_library_path.split('.')

            skinfolder = getattr(portal_skins, skinfolder_id, None)
            default_field_library = getattr(skinfolder, form_id, None)
            if default_field_library is None:
                return
Jérome Perrin's avatar
Jérome Perrin committed
805 806 807 808 809 810 811 812 813 814 815
            for i in default_field_library.objectValues():
                field_meta_type, proxy_flag = get_field_meta_type_and_proxy_flag(i)
                if meta_type==field_meta_type:
                    if proxy_flag:
                        field_meta_type = '%s(Proxy)' % field_meta_type
                    matched_item = {'form_id':form_id,
                                    'field_type':field_meta_type,
                                    'field_object':i,
                                    'proxy_flag':proxy_flag,
                                    'matched_rate':0
                                    }
816 817 818 819 820 821 822

                    if not i in [item['field_object']
                                 for item in matched.get(default_field_library_path, ())]:
                      matched_append(default_field_library_path, matched_item)
                    if not i in [item['field_object']
                                 for item in perfect_matched.get(default_field_library_path, ())]:
                      perfect_matched_append(default_field_library_path, matched_item)
823 824

        id_ = field.getId()
825
        meta_type = field.meta_type
826

827 828
        matched = {}
        form_order = []
829 830 831 832 833
        matched_append = make_dict_list_append_function(matched, form_order)

        perfect_matched = {}
        perfect_matched_form_order = []
        perfect_matched_append = make_dict_list_append_function(perfect_matched, perfect_matched_form_order)
834 835

        if form_field_list is None:
836
            form_field_list = self.getFormFieldList()
837 838 839

        for i in form_field_list:
            for data in i['field_list']:
840 841
                tmp = []
                matched_rate = match(data)
842
                if matched_rate>0:
843 844 845 846 847
                    form_path = i['form_path']
                    form_id = i['form_id']
                    field_type = data['field_type']
                    field_object = data['field_object']
                    proxy_flag = data['proxy_flag']
848 849 850 851 852 853 854 855 856 857 858 859 860 861

                    matched_item = {'form_id':form_id,
                                    'field_type':field_type,
                                    'field_object':field_object,
                                    'proxy_flag':proxy_flag,
                                    'matched_rate':matched_rate
                                    }
                    if matched_rate==1:
                        perfect_matched_append(form_path, matched_item)
                    elif not perfect_matched:
                        matched_append(form_path, matched_item)

        if perfect_matched:
            perfect_matched_form_order.sort()
Jérome Perrin's avatar
Jérome Perrin committed
862
            add_default_field_library()
863 864
            return perfect_matched_form_order, perfect_matched

865
        form_order.sort()
866
        add_default_field_library()
867 868
        return form_order, matched

869 870 871 872 873 874 875
    security.declareProtected('View management screens', 'getUnProxyableFieldList')
    def getUnProxyableFieldList(self):
      """
      Return ProxyFields
      """
      return [f for f in self.objectValues() if f.meta_type == 'ProxyField']

876
    security.declareProtected('Change Formulator Forms', 'proxifyField')
877 878 879 880 881 882
    def proxifyField(self, field_dict=None, force_delegate=False, REQUEST=None):
        """Convert fields to proxy fields
        If the field value is different from the proxyfield value, the value is
        kept on the proxyfield, otherwise it is delegated. If you specify
        force_delegate, values will be delegated even if they are different
        """
883 884
        from Products.ERP5Form.ProxyField import ProxyWidget

Jérome Perrin's avatar
Jérome Perrin committed
885
        def copy(_dict):
886
            new_dict = {}
Jérome Perrin's avatar
Jérome Perrin committed
887
            for key, value in _dict.items():
888 889
                if value=='':
                    continue
890 891
                if isinstance(aq_base(value), (Method, TALESMethod)):
                    value = copyMethod(value)
Jérome Perrin's avatar
Jérome Perrin committed
892
                elif value is not None and not isinstance(value,
893 894
                        (str, unicode, int, long, float, bool, list, tuple, dict)):
                    raise ValueError, '%s:%s' % (type(value), repr(value))
895 896 897 898 899 900 901 902
                new_dict[key] = value
            return new_dict

        def is_equal(a, b):
            type_a = type(a)
            type_b = type(b)
            if type_a is not type_b:
                return False
903
            elif type_a is Method:
904 905 906 907 908 909 910 911 912
                return a.method_name==b.method_name
            elif type_a is TALESMethod:
                return a._text==b._text
            else:
                return a==b

        def remove_same_value(new_dict, target_dict):
            for key, value in new_dict.items():
                target_value = target_dict.get(key)
913
                if force_delegate or is_equal(value, target_value):
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
                    del new_dict[key]
            return new_dict

        def get_group_and_position(field_id):
            for i in self.groups.keys():
                if field_id in self.groups[i]:
                    return i, self.groups[i].index(field_id)

        def set_group_and_position(group, position, field_id):
            self.field_removed(field_id)
            self.groups[group].insert(position, field_id)
            # Notify changes explicitly.
            self.groups = self.groups

        if field_dict is None:
            return

        for field_id in field_dict.keys():
            target = field_dict[field_id]
            target_form_id, target_field_id = target.split('.')

            # keep current group and position.
            group, position = get_group_and_position(field_id)

            # create proxy field
            old_field = getattr(self, field_id)
            self.manage_delObjects(field_id)
            self.manage_addField(id=field_id, title='', fieldname='ProxyField')
            proxy_field = getattr(self, field_id)
            proxy_field.values['form_id'] = target_form_id
            proxy_field.values['field_id'] = target_field_id

946
            target_field = proxy_field.getTemplateField()
947 948 949 950 951 952 953

            # copy data
            new_values = remove_same_value(copy(old_field.values),
                                           target_field.values)
            new_tales = remove_same_value(copy(old_field.tales),
                                          target_field.tales)

954 955
            if target_field.meta_type=='ProxyField':
                for i in new_values.keys():
956 957 958 959 960 961 962 963 964 965 966
                    if not i in target_field.delegated_list:
                        # obsolete variable check
                        try:
                            target_field.get_recursive_orig_value(i)
                        except KeyError:
                            # then `i` is obsolete!
                            del new_values[i]
                        else:
                            if is_equal(target_field.get_recursive_orig_value(i),
                                        new_values[i]):
                                del new_values[i]
Jérome Perrin's avatar
Jérome Perrin committed
967
                for i in new_tales.keys():
968 969 970 971 972 973 974 975 976 977 978
                    if not i in target_field.delegated_list:
                        # obsolete variable check
                        try:
                            target_field.get_recursive_tales(i)
                        except KeyError:
                            # then `i` is obsolete!
                            del new_tales[i]
                        else:
                            if is_equal(target_field.get_recursive_tales(i),
                                        new_tales[i]):
                                del new_tales[i]
979

980 981 982 983 984 985 986 987 988 989 990
            delegated_list = []
            for i in (new_values.keys()+new_tales.keys()):
                if not i in delegated_list:
                    delegated_list.append(i)
            proxy_field.values.update(new_values)
            proxy_field.tales.update(new_tales)
            proxy_field.delegated_list = delegated_list

            # move back to the original group and position.
            set_group_and_position(group, position, field_id)

991 992
        if REQUEST is not None:
            return self.formProxify(manage_tabs_message='Changed')
993

Jean-Paul Smets's avatar
Jean-Paul Smets committed
994 995 996
    psyco.bind(__call__)
    psyco.bind(_exec)

997 998
    security.declareProtected('Change Formulator Forms', 'unProxifyField')
    def unProxifyField(self, field_dict=None, REQUEST=None):
999 1000 1001
        """
        Convert proxy fields to fields
        """
Nicolas Delaby's avatar
Nicolas Delaby committed
1002 1003 1004
        def copy(field, value_type):
            mapping_dict = {'values' : 'get_recursive_orig_value',
                            'tales'  : 'get_recursive_tales'}
1005
            new_dict = {}
Nicolas Delaby's avatar
Nicolas Delaby committed
1006 1007 1008 1009
            key_list = getattr(field.getRecursiveTemplateField(), value_type).keys()
            for key in key_list:
                method_id = mapping_dict[value_type]
                value = getattr(field, method_id)(key)
1010 1011 1012 1013 1014
                if value=='':
                    continue
                if isinstance(aq_base(value), (Method, TALESMethod)):
                    value = copyMethod(value)
                elif value is not None and not isinstance(value,
1015 1016
                        (str, unicode, int, long, float, bool, list, tuple, dict)):
                    raise ValueError, '%s:%s' % (type(value), repr(value))
1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067
                new_dict[key] = value
            return new_dict

        def is_equal(a, b):
            type_a = type(a)
            type_b = type(b)
            if type_a is not type_b:
                return False
            elif type_a is Method:
                return a.method_name==b.method_name
            elif type_a is TALESMethod:
                return a._text==b._text
            else:
                return a==b

        def remove_same_value(new_dict, target_dict):
            for key, value in new_dict.items():
                target_value = target_dict.get(key)
                if is_equal(value, target_value):
                    del new_dict[key]
            return new_dict

        def get_group_and_position(field_id):
            for i in self.groups.keys():
                if field_id in self.groups[i]:
                    return i, self.groups[i].index(field_id)

        def set_group_and_position(group, position, field_id):
            self.field_removed(field_id)
            self.groups[group].insert(position, field_id)
            # Notify changes explicitly.
            self.groups = self.groups

        if field_dict is None:
            return

        for field_id in field_dict.keys():
            # keep current group and position.
            group, position = get_group_and_position(field_id)

            # create field
            old_proxy_field = getattr(self, field_id)
            delegated_field = old_proxy_field.getRecursiveTemplateField()
            if delegated_field is None:
              break
            self.manage_delObjects(field_id)
            self.manage_addField(id=field_id,
                                 title='',
                                 fieldname=delegated_field.meta_type)
            field = getattr(self, field_id)
            # copy data
Nicolas Delaby's avatar
Nicolas Delaby committed
1068
            new_values = remove_same_value(copy(old_proxy_field, 'values'),
1069
                                           field.values)
Nicolas Delaby's avatar
Nicolas Delaby committed
1070
            new_tales = remove_same_value(copy(old_proxy_field, 'tales'),
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
                                          field.tales)

            field.values.update(new_values)
            field.tales.update(new_tales)

            # move back to the original group and position.
            set_group_and_position(group, position, field_id)

        if REQUEST is not None:
            return self.formUnProxify(manage_tabs_message='Changed')

1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112
    # Overload of the Form method
    #   Use the include_disabled parameter since
    #   we should consider all fields to render the group tab
    #   moreoever, listbox rendering fails whenever enabled
    #   is based on the cell parameter.
    security.declareProtected('View', 'get_largest_group_length')
    def get_largest_group_length(self):
        """Get the largest group length available; necessary for
        'order' screen user interface.
        XXX - Copyright issue
        """
        max = 0
        for group in self.get_groups(include_empty=1):
            fields = self.get_fields_in_group(group, include_disabled=1)
            if len(fields) > max:
                max = len(fields)
        return max

    security.declareProtected('View', 'get_groups')
    def get_groups(self, include_empty=0):
        """Get a list of all groups, in display order.

        If include_empty is false, suppress groups that do not have
        enabled fields.
        XXX - Copyright issue
        """
        if include_empty:
            return self.group_list
        return [group for group in self.group_list
                if self.get_fields_in_group(group, include_disabled=1)]

1113 1114 1115 1116
    # Find support in ZMI. This is useful for development.
    def PrincipiaSearchSource(self):
      return str((self.pt, self.name, self.action, self.update_action,
                  self.encoding, self.stored_encoding, self.enctype))
1117 1118 1119 1120

# utility function
def get_field_meta_type_and_proxy_flag(field):
    if field.meta_type=='ProxyField':
1121 1122 1123
        try:
            return field.getRecursiveTemplateField().meta_type, True
        except AttributeError:
1124 1125 1126
            raise AttributeError, 'The proxy target of %s.%s field does not '\
                  'exists. Please check the field setting.' % \
                  (field.aq_parent.id, field.getId())
1127 1128 1129 1130
    else:
        return field.meta_type, False


Jean-Paul Smets's avatar
Jean-Paul Smets committed
1131 1132
# More optimizations
#psyco.bind(ERP5Field)
1133
# XXX Not useful, as we patch those methods in FormulatorPatch
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
psyco.bind(Field.render)
psyco.bind(Field._render_helper)
psyco.bind(Field.get_value)

#from Products.PageTemplates.PageTemplate import PageTemplate
#from TAL import TALInterpreter
#psyco.bind(TALInterpreter.TALInterpreter)
#psyco.bind(TALInterpreter.TALInterpreter.interpret)
#psyco.bind(PageTemplate.pt_render)
#psyco.bind(PageTemplate.pt_macros)

#from Products.CMFCore.ActionsTool import ActionsTool
1146
#psyco.bind(ActionsTool.listFilteredActionsFor)
1147 1148 1149

# install interactor
# we need to install interactor after to apply get_value patch.
1150 1151
from Products.ERP5Type.Interactor import field_value_interactor
field_value_interactor.install()