MatrixBox.py 18.7 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'):
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 239 240 241
          header = """\
  <!-- Matrix Content -->
  %s<br>
  <div class="ListContent">
   <table cellpadding="0" cellspacing="0" border="0">
242
  """ % first_tab
Jean-Paul Smets's avatar
Jean-Paul Smets committed
243 244 245 246 247 248 249 250

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

          list_header = """\
261
  <tr><td class=\"Data\"></td>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
262 263 264
  """

          for cname in columns:
265 266 267 268 269
            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])
270

Jean-Paul Smets's avatar
Jean-Paul Smets committed
271 272 273 274 275 276 277
          list_header = list_header + "</tr>"

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

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

Jean-Paul Smets's avatar
Jean-Paul Smets committed
289
            for c in columns:
290
              has_error = 0
291
              column_id = c[0]
Romain Courteaud's avatar
Romain Courteaud committed
292 293
              if (column_id is not None) and \
                 (not isinstance(column_id, (list, tuple))):
294 295 296 297
                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
298 299 300 301 302
                kw = [l[0], c[0]]
              else:
                kw = [l[0], c[0]] + tab_id
              kwd = {}
              kwd['base_id'] = cell_base_id
303
              cell = cell_getter_method(*kw, **kwd)
304

Jean-Paul Smets's avatar
Jean-Paul Smets committed
305
              cell_body = ''
306

Jean-Paul Smets's avatar
Jean-Paul Smets committed
307 308 309 310
              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)
311
                  key = my_field.id + '_cell_%s_%s_%s' % (i,j,k)
312
                  if cell != None:
313 314
                    attribute_value = my_field.get_value('default',
                           cell=cell, cell_index=kw, cell_position = (i,j,k))
315
                  
316 317
                    if render_format=='html':
                      REQUEST['cell'] = cell
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
                      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
                        cell_body += "%s<br/>%s" % (
                            my_field.render(value=display_value,
                                            REQUEST=REQUEST,
                                            key=key),
                            N_(field_errors[key].error_text))
                      else:
                        cell_body += str(my_field.render(
                                            value=attribute_value,
                                            REQUEST=REQUEST,
                                            key=key))

                    elif render_format == 'list':
337 338 339 340
                      if not my_field.get_value('hidden'):
                        list_result_lines.append(attribute_value)

                  else:
341
                    if my_field.get_value('hidden'):
342 343 344 345 346 347 348 349 350 351 352 353 354 355
                      attribute_value = my_field.get_value('default',
                            cell_index=kw, cell_position=(i,j,k))
                    else :
                      attribute_value = my_field.get_orig_value('default')
                    if render_format == 'html':
                      REQUEST['cell'] = None
                      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 :
                css = td_css + 'Error'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
356
              list_body = list_body + \
357
                    ('<td class=\"%s\">%s</td>' % (css, cell_body))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
358
              j += 1
359

Jean-Paul Smets's avatar
Jean-Paul Smets committed
360 361
            list_body = list_body + '</tr>'
            i += 1
362 363 364
            
            if render_format == 'list':
              list_result_tab.append(list_result_lines)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
365 366 367 368 369

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

370 371 372 373 374 375
          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
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
        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')
393
        getter_method_id = field.get_value('getter_method')
394
        error_list = []
395 396 397
        context = here
        if getter_method_id not in (None,''):
          context = getattr(here,getter_method_id)()
398
          if context is None: return {}
399
        cell_getter_method = context.getCell
Jean-Paul Smets's avatar
Jean-Paul Smets committed
400 401 402

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

Romain Courteaud's avatar
Romain Courteaud committed
406 407 408 409 410
        # 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
411 412 413 414 415

        k = 0
        result = {}
        # Create one table per tab
        for tab_id in tab_ids:
Romain Courteaud's avatar
Romain Courteaud committed
416 417
          if (tab_id is not None) and \
             (not isinstance(tab_id, (list, tuple))):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
418 419 420 421 422 423 424
            tab_id = [tab_id]

          i = 0
          j = 0
          for l in line_ids:
            j = 0
            for c in column_ids:
425 426 427
              if c is None:
                kw = [l]
              elif tab_id is None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
428 429 430 431 432 433
                kw = [l, c]
              else:
                kw = [l, c] + tab_id
              kw = tuple(kw)
              kwd = {}
              kwd['base_id'] = cell_base_id
434
              cell = cell_getter_method(*kw, **kwd)
435

Jean-Paul Smets's avatar
Jean-Paul Smets committed
436
              for attribute_id in editable_attribute_ids:
437

Jean-Paul Smets's avatar
Jean-Paul Smets committed
438 439 440
                my_field_id = '%s_%s' % (field.id, attribute_id)
                if form.has_field(my_field_id):
                  my_field = form.get_field(my_field_id)
441
                  if my_field.get_value('editable'):
442 443 444 445 446 447 448 449 450 451 452 453 454
                    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))
                    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'):
455
                      # Only validate modified values from visible fields
456
                      result.setdefault(kw, {})
Jean-Paul Smets's avatar
Jean-Paul Smets committed
457
                      result[kw][attribute_id] = value
458 459 460
                    else:
                      if result.has_key(kw):
                        result[kw][attribute_id] = value
Jean-Paul Smets's avatar
Jean-Paul Smets committed
461 462 463
              j += 1
            i += 1
          k += 1
464 465
        if len(error_list):
          raise FormValidationError(error_list, {})
Jean-Paul Smets's avatar
Jean-Paul Smets committed
466 467 468 469 470 471 472 473 474 475
        return result

MatrixBoxValidatorInstance = MatrixBoxValidator()

class MatrixBox(ZMIField):
    meta_type = "MatrixBox"

    widget = MatrixBoxWidgetInstance
    validator = MatrixBoxValidatorInstance

476 477 478 479
    security = ClassSecurityInfo()

    security.declareProtected('Access contents information', 'get_value')
    def get_value(self, id, **kw):
Romain Courteaud's avatar
Romain Courteaud committed
480 481 482 483
      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'))
484 485
      else:
        return ZMIField.get_value(self, id, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
486 487

# Psyco
488
from Products.ERP5Type.PsycoWrapper import psyco
Jean-Paul Smets's avatar
Jean-Paul Smets committed
489 490
psyco.bind(MatrixBoxWidget.render)
psyco.bind(MatrixBoxValidator.validate)
491 492 493 494

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