MatrixBox.py 18.6 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2
##############################################################################
#
Romain Courteaud's avatar
Romain Courteaud committed
3
# Copyright (c) 2002, 2006 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Romain Courteaud's avatar
Romain Courteaud committed
5
#                    Romain Courteaud <romain@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#
# 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.
#
##############################################################################

30
from AccessControl import ClassSecurityInfo
Jean-Paul Smets's avatar
Jean-Paul Smets committed
31 32
from Products.Formulator.DummyField import fields
from Products.Formulator import Widget, Validator
33
from Products.Formulator.Errors import FormValidationError, ValidationError
Jean-Paul Smets's avatar
Jean-Paul Smets committed
34
from Products.Formulator.Field import ZMIField
35 36 37 38
from Products.ERP5Type.Message import Message

def N_(message, **kw):
  return Message('erp5_ui', message, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
39 40 41

class MatrixBoxWidget(Widget.Widget):
    """
42
    An UI widget which displays a matrix
Jean-Paul Smets's avatar
Jean-Paul Smets committed
43

44
    A MatrixBoxWidget should be called 'matrixbox', if you don't do so, then
Romain Courteaud's avatar
Romain Courteaud committed
45 46
    you may have some errors, 
    or some strange problems, you have been Warned !!!!
Jean-Paul Smets's avatar
Jean-Paul Smets committed
47

48
    Don't forget that you can use tales expressions for every field, so this
Romain Courteaud's avatar
Romain Courteaud committed
49 50
    is really usefull if you want to use fonctions 
    instead of predefined variables.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
51 52 53 54 55 56 57 58 59

    A function is provided to

    - access a cell

    - modify a cell

    """
    property_names = Widget.Widget.property_names +\
60 61
                     ['cell_base_id', 'cell_portal_type',
                      'lines', 'columns', 'tabs', 'getter_method' ,
62
                      'editable_attributes' , 'global_attributes',
Jean-Paul Smets's avatar
Jean-Paul Smets committed
63 64 65 66 67 68 69 70 71 72 73 74 75 76
                      'update_cell_range'
                       ]

    default = fields.TextAreaField('default',
                                   title='Default',
                                   description=(
        "Default value of the text in the widget."),
                                   default="",
                                   width=20, height=3,
                                   required=0)

    columns = fields.ListTextAreaField('columns',
                                 title="Columns",
                                 description=(
Romain Courteaud's avatar
Romain Courteaud committed
77 78 79 80 81 82
      """This defines columnes of the matrixbox. 
      This should be a list of couples, 
      couple[0] is the variation, and couple[1] is the name displayed 
      to the user.
      For example (('color/bleu','bleu'),('color/red','red')),
      Deprecated"""),
Jean-Paul Smets's avatar
Jean-Paul Smets committed
83
                                 default=[],
84
                                 required=0)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
85 86 87 88

    lines = fields.ListTextAreaField('lines',
                                 title="Lines",
                                 description=(
Romain Courteaud's avatar
Romain Courteaud committed
89 90 91 92 93
      """This defines lines of the matrixbox. This should be a list of couples,
      couple[0] is the variation, and couple[1] is the name displayed 
      to the user.
      For example (('size/baby/02','baby/02'),('size/baby/03','baby/03')), 
      Deprecated"""),
Jean-Paul Smets's avatar
Jean-Paul Smets committed
94
                                 default=[],
95
                                 required=0)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
96 97 98 99

    tabs = fields.ListTextAreaField('tabs',
                                 title="Tabs",
                                 description=(
Romain Courteaud's avatar
Romain Courteaud committed
100 101 102 103
      """This defines tabs. You can use it with the same way as Lines 
      and Columns,
      This is used only if you have more than 2 kinds of variations. 
      Deprecated"""),
Jean-Paul Smets's avatar
Jean-Paul Smets committed
104 105 106
                                 default=[],
                                 required=0)

Romain Courteaud's avatar
Romain Courteaud committed
107 108 109 110 111 112 113 114 115
    # XXX ListTextAreaField ?
    cell_range = fields.ListTextAreaField('cell_range',
                                           title="Cell Range",
                                           description=(
                """
                This defines the range of the matrix.
                """),
                                           default=[],
                                           required=0)
116
    getter_method = fields.StringField('getter_method',
Jean-Paul Smets's avatar
Jean-Paul Smets committed
117
                                 title='Getter method',
118
                                 description=("""
119
        You can specify a specific method in order to retrieve the context.
Romain Courteaud's avatar
Romain Courteaud committed
120 121
        This field can be empty, if so the MatrixBox will use the default 
        context."""),
122 123 124 125 126 127 128 129

                                 default='',
                                 required=0)

    new_cell_method = fields.MethodField('new_cell_method',
                                 title='New Cell method',
                                 description=("""
        You can specify a specific method in order to create cells.
Romain Courteaud's avatar
Romain Courteaud committed
130 131
        This field can be empty, if so the MatrixBox will use the default 
        method :
132
        newCell."""),
133

Jean-Paul Smets's avatar
Jean-Paul Smets committed
134 135 136 137 138 139
                                 default='',
                                 required=0)

    editable_attributes = fields.ListTextAreaField('editable_attributes',
                                 title="Editable Properties",
                                 description=(
Romain Courteaud's avatar
Romain Courteaud committed
140 141
        """A list of attributes which are set by hidden fields called 
        matrixbox_attribute_name. This is used
142
        when we want to specify a value calculated for each cell"""),
Jean-Paul Smets's avatar
Jean-Paul Smets committed
143 144 145 146 147 148
                                 default=[],
                                 required=0)

    global_attributes = fields.ListTextAreaField('global_attributes',
                                 title="Global Properties",
                                 description=(
Romain Courteaud's avatar
Romain Courteaud committed
149 150
        """An optional list of globals attributes which are set by hidden 
        fields and which are applied to each cell. 
151
        This is used if we want to set the same value for every cell"""),
Jean-Paul Smets's avatar
Jean-Paul Smets committed
152 153 154 155 156
                                 default=[],
                                 required=0)

    cell_base_id = fields.StringField('cell_base_id',
                                 title='Base id for cells',
157
                                 description=("""
Romain Courteaud's avatar
Romain Courteaud committed
158 159
        The Base id for cells : this is the name used to store cells, 
        we usually,
160
        use names like : 'mouvement','path', ...."""),
Jean-Paul Smets's avatar
Jean-Paul Smets committed
161 162 163
                                 default='cell',
                                 required=0)

164 165 166
    cell_portal_type = fields.StringField('cell_portal_type',
                                 title='Portal Type for cells',
                                 description=("""
Romain Courteaud's avatar
Romain Courteaud committed
167 168
        The Portal Type for cells : This is the portal type used to 
        construct a new cell."""),
169 170 171
                                 default='Mapped Value',
                                 required=0)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
172 173 174 175 176 177
    update_cell_range = fields.CheckBoxField('update_cell_range',
                                  title="Update Cell Range",
                                  description=(
        "The cell range should be updated upon edit."),
                                  default=0)

178
    def render(self, field, key, value, REQUEST, render_format='html', render_prefix=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
179 180 181 182 183 184 185 186 187 188 189 190
        """
          This is where most things happen. This method renders a list
          of items
        """
        # First grasp the variables we may need
        here = REQUEST['here']
        form = field.aq_parent
        field_title = field.get_value('title')
        cell_base_id = field.get_value('cell_base_id')
        lines = field.get_value('lines')
        columns = field.get_value('columns')
        tabs = field.get_value('tabs')
191
        field_errors = REQUEST.get('field_errors', {})
192 193 194 195 196 197 198
        context = here
        getter_method_id = field.get_value('getter_method')
        if getter_method_id not in (None,''):
          context = getattr(here,getter_method_id)()
        if context is None:
          return ''
        cell_getter_method = context.getCell
Jean-Paul Smets's avatar
Jean-Paul Smets committed
199 200 201
        editable_attributes = field.get_value('editable_attributes')

        # This is required when we have no tabs
Romain Courteaud's avatar
Romain Courteaud committed
202 203
        if len(tabs) == 0: 
          tabs = [(None,None)]
204
        # This is required when we have no columns
Romain Courteaud's avatar
Romain Courteaud committed
205 206
        if len(columns) == 0: 
          columns = [(None,None)]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
207

Romain Courteaud's avatar
Romain Courteaud committed
208 209 210 211
        column_ids = [x[0] for x in columns]
        line_ids = [x[0] for x in lines]
        tab_ids = [x[0] for x in tabs]
        editable_attribute_ids = [x[0] for x in editable_attributes]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
212 213 214 215 216

        # THIS MUST BE REMOVED - WHY IS THIS BAD ?
        # IT IS BAD BECAUSE TAB_IDS DO NOT DEFINE A RANGE....
        # here.setCellRange(line_ids, column_ids, base_id=cell_base_id)

217 218 219
        # result for the list render
        list_result = []
            
Jean-Paul Smets's avatar
Jean-Paul Smets committed
220 221 222 223
        url = REQUEST.URL

        list_html = ''
        k = 0
Sebastien Robin's avatar
Sebastien Robin committed
224

Jean-Paul Smets's avatar
Jean-Paul Smets committed
225 226 227
        # Create one table per tab
        for tab in tabs:
          tab_id = tab[0]
Romain Courteaud's avatar
Romain Courteaud committed
228 229
          if (tab_id is not None) and \
             (not isinstance(tab_id, (list, tuple))):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
230
            tab_id = [tab_id]
231
            
232
          if render_format == 'list':
233
            list_result_tab = [[tab[1]]]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
234 235

          # Create the header of the table - this should probably become DTML
236
          first_tab = tab[1] or ''
Jean-Paul Smets's avatar
Jean-Paul Smets committed
237 238
          header = """\
  <!-- Matrix Content -->
239
  %s<br/>
240
  <div class="MatrixContent">
Jean-Paul Smets's avatar
Jean-Paul Smets committed
241
   <table cellpadding="0" cellspacing="0" border="0">
242
  """ % first_tab
Jean-Paul Smets's avatar
Jean-Paul Smets committed
243 244 245 246

          # Create the footer. This should be replaced by DTML
          # And work as some kind of parameter
          footer = """\
247
     <tr>
248 249
      <td colspan="%s" align="center" valign="middle"
          class="Data footer">
Jean-Paul Smets's avatar
Jean-Paul Smets committed
250 251 252 253
      </td>
     </tr>
    </table>
   </div>
254
  """ % len(columns)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
255 256

          list_header = """\
257
  <tr class="matrixbox_label_line"><td class=\"Data\"></td>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
258 259 260
  """

          for cname in columns:
261 262 263 264 265
            first_column = cname[1] or ''
            list_header = list_header + ("<td class=\"Data\">%s</td>\n" %
                                           first_column)
            if render_format == 'list':
              list_result_tab[0].append(cname[1])
266

Jean-Paul Smets's avatar
Jean-Paul Smets committed
267 268 269 270 271 272 273
          list_header = list_header + "</tr>"

          # Build Lines
          i = 0
          j = 0
          list_body = ''
          for l in lines:
274

Jean-Paul Smets's avatar
Jean-Paul Smets committed
275 276 277 278
            if not i % 2:
              td_css = 'DataA'
            else:
              td_css = 'DataB'
279
            list_body = list_body + '\n<tr class=\"%s\"><td class=\"matrixbox_label_column\">%s</td>' % (td_css, str(l[1]))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
280
            j = 0
281
            
282
            if render_format == 'list':
283
              list_result_lines = [ str(l[1]) ]
Sebastien Robin's avatar
Sebastien Robin committed
284

Jean-Paul Smets's avatar
Jean-Paul Smets committed
285
            for c in columns:
286
              has_error = 0
287
              column_id = c[0]
Romain Courteaud's avatar
Romain Courteaud committed
288 289
              if (column_id is not None) and \
                 (not isinstance(column_id, (list, tuple))):
290 291 292 293
                column_id = [column_id]
              if column_id is None:
                kw = [l[0]]
              elif tab_id is None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
294 295 296 297 298
                kw = [l[0], c[0]]
              else:
                kw = [l[0], c[0]] + tab_id
              kwd = {}
              kwd['base_id'] = cell_base_id
299
              cell = cell_getter_method(*kw, **kwd)
300
              REQUEST['cell'] = cell
301

Jean-Paul Smets's avatar
Jean-Paul Smets committed
302
              cell_body = ''
303

Jean-Paul Smets's avatar
Jean-Paul Smets committed
304 305 306 307
              for attribute_id in editable_attribute_ids:
                my_field_id = '%s_%s' % (field.id, attribute_id)
                if form.has_field(my_field_id):
                  my_field = form.get_field(my_field_id)
308
                  key = my_field.id + '_cell_%s_%s_%s' % (i,j,k)
309
                  if cell != None:
310 311
                    attribute_value = my_field.get_value('default',
                           cell=cell, cell_index=kw, cell_position = (i,j,k))
312

313
                    if render_format=='html':
314 315 316 317 318 319 320
                      display_value = attribute_value

                      if field_errors.has_key(key):
                        # Display previous value (in case of error)
                        display_value = REQUEST.get('field_%s' % key,
                                                  attribute_value)
                        has_error = 1
321
                        cell_body += '<span class="input">%s</span>%s' % (
322 323 324 325 326
                            my_field.render(value=display_value,
                                            REQUEST=REQUEST,
                                            key=key),
                            N_(field_errors[key].error_text))
                      else:
327 328
                        cell_body += '<span class="input">%s</span>' %\
                                         my_field.render(
329 330
                                            value=attribute_value,
                                            REQUEST=REQUEST,
331
                                            key=key)
332 333

                    elif render_format == 'list':
334 335 336 337
                      if not my_field.get_value('hidden'):
                        list_result_lines.append(attribute_value)

                  else:
338 339
                    attribute_value = my_field.get_value('default', cell=None,
                        cell_index=kw, cell_position=(i,j,k))
340 341 342 343 344 345 346 347
                    if render_format == 'html':
                      cell_body += str(my_field.render(value=attribute_value,
                                      REQUEST=REQUEST, key=key))
                    elif render_format == 'list':
                      list_result_lines.append(None)

              css = td_css
              if has_error :
348
                css = 'error'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
349
              list_body = list_body + \
350
                    ('<td class=\"%s\">%s</td>' % (css, cell_body))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
351
              j += 1
352

Jean-Paul Smets's avatar
Jean-Paul Smets committed
353 354
            list_body = list_body + '</tr>'
            i += 1
355 356 357
            
            if render_format == 'list':
              list_result_tab.append(list_result_lines)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
358 359 360 361 362

          list_html += header + list_header + \
                  list_body + footer
          k += 1

363 364 365 366 367 368
          if render_format == 'list':
            list_result.append(list_result_tab)
        
        if render_format == 'list':
          return list_result

Jean-Paul Smets's avatar
Jean-Paul Smets committed
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
        return list_html

MatrixBoxWidgetInstance = MatrixBoxWidget()

class MatrixBoxValidator(Validator.Validator):
    property_names = Validator.Validator.property_names

    def validate(self, field, key, REQUEST):
        form = field.aq_parent
        # We need to know where we get the getter from
        # This is coppied from ERP5 Form
        here = getattr(form, 'aq_parent', REQUEST)
        cell_base_id = field.get_value('cell_base_id')
        lines = field.get_value('lines')
        columns = field.get_value('columns')
        tabs = field.get_value('tabs')
        editable_attributes = field.get_value('editable_attributes')
386
        getter_method_id = field.get_value('getter_method')
387
        error_list = []
388 389 390
        context = here
        if getter_method_id not in (None,''):
          context = getattr(here,getter_method_id)()
391
          if context is None: return {}
392
        cell_getter_method = context.getCell
Jean-Paul Smets's avatar
Jean-Paul Smets committed
393 394 395

        # This is required when we have no tabs
        if len(tabs) == 0: tabs = [(None,None)]
396 397
        # This is required when we have no columns
        if len(columns) == 0: columns = [(None,None)]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
398

Romain Courteaud's avatar
Romain Courteaud committed
399 400 401 402 403
        # XXX Copy/Paste from render...
        column_ids = [x[0] for x in columns]
        line_ids = [x[0] for x in lines]
        tab_ids = [x[0] for x in tabs]
        editable_attribute_ids = [x[0] for x in editable_attributes]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
404 405 406 407 408

        k = 0
        result = {}
        # Create one table per tab
        for tab_id in tab_ids:
Romain Courteaud's avatar
Romain Courteaud committed
409 410
          if (tab_id is not None) and \
             (not isinstance(tab_id, (list, tuple))):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
411 412 413 414 415 416 417
            tab_id = [tab_id]

          i = 0
          j = 0
          for l in line_ids:
            j = 0
            for c in column_ids:
418 419 420
              if c is None:
                kw = [l]
              elif tab_id is None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
421 422 423 424 425 426
                kw = [l, c]
              else:
                kw = [l, c] + tab_id
              kw = tuple(kw)
              kwd = {}
              kwd['base_id'] = cell_base_id
427
              cell = cell_getter_method(*kw, **kwd)
428

Jean-Paul Smets's avatar
Jean-Paul Smets committed
429
              for attribute_id in editable_attribute_ids:
430

Jean-Paul Smets's avatar
Jean-Paul Smets committed
431 432 433
                my_field_id = '%s_%s' % (field.id, attribute_id)
                if form.has_field(my_field_id):
                  my_field = form.get_field(my_field_id)
434
                  if my_field.get_value('editable'):
435 436 437
                    key = 'field_' + my_field.id + '_cell_%s_%s_%s' % (i,j,k)
                    attribute_value = my_field.get_value('default',
                        cell=cell, cell_index=kw, cell_position = (i,j,k))
438
                    value = None
439 440 441 442 443 444 445 446 447 448
                    try :
                      value = my_field.validator.validate(
                                      my_field, key, REQUEST)
                    except ValidationError, err :
                      err.field_id = my_field.id + '_cell_%s_%s_%s' % (i,j,k)
                      error_list.append(err)

                    if (attribute_value != value or \
                        attribute_value not in ('',None,(),[])) \
                        and not my_field.get_value('hidden'):
449
                      # Only validate modified values from visible fields
450
                      result.setdefault(kw, {})
Jean-Paul Smets's avatar
Jean-Paul Smets committed
451
                      result[kw][attribute_id] = value
452 453 454
                    else:
                      if result.has_key(kw):
                        result[kw][attribute_id] = value
Jean-Paul Smets's avatar
Jean-Paul Smets committed
455 456 457
              j += 1
            i += 1
          k += 1
458 459
        if len(error_list):
          raise FormValidationError(error_list, {})
Jean-Paul Smets's avatar
Jean-Paul Smets committed
460 461 462 463 464 465 466 467 468 469
        return result

MatrixBoxValidatorInstance = MatrixBoxValidator()

class MatrixBox(ZMIField):
    meta_type = "MatrixBox"

    widget = MatrixBoxWidgetInstance
    validator = MatrixBoxValidatorInstance

470 471 472 473
    security = ClassSecurityInfo()

    security.declareProtected('Access contents information', 'get_value')
    def get_value(self, id, **kw):
Romain Courteaud's avatar
Romain Courteaud committed
474 475 476 477
      if id=='default' and kw.get('render_format') in ('list', ):
        return self.widget.render(self, self.generate_field_key(), None, 
                                  kw.get('REQUEST'), 
                                  render_format=kw.get('render_format'))
478 479
      else:
        return ZMIField.get_value(self, id, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
480 481

# Psyco
482
from Products.ERP5Type.PsycoWrapper import psyco
Jean-Paul Smets's avatar
Jean-Paul Smets committed
483 484
psyco.bind(MatrixBoxWidget.render)
psyco.bind(MatrixBoxValidator.validate)
485 486 487 488

# Register get_value
from Products.ERP5Form.ProxyField import registerOriginalGetValueClassAndArgument
registerOriginalGetValueClassAndArgument(MatrixBox, 'default')