############################################################################## # # Copyright (c) 2002, 2006 Nexedi SARL and Contributors. All Rights Reserved. # Jean-Paul Smets-Solanes <jp@nexedi.com> # Romain Courteaud <romain@nexedi.com> # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## from AccessControl import ClassSecurityInfo from AccessControl.ZopeGuards import guarded_getattr from Products.Formulator.DummyField import fields from Products.Formulator import Widget, Validator from Products.Formulator.Errors import FormValidationError, ValidationError from Products.Formulator.Field import ZMIField from Products.ERP5Type.Message import translateString class MatrixBoxWidget(Widget.Widget): """ An UI widget which displays a matrix Don't forget that you can use tales expressions for every field, so this is really usefull if you want to use fonctions instead of predefined variables. A function is provided to - access a cell - modify a cell """ property_names = Widget.Widget.property_names +\ ['cell_base_id', 'cell_portal_type', 'lines', 'columns', 'tabs', 'as_cell_range_script_id', 'getter_method', 'editable_attributes', 'global_attributes', 'cell_getter_method', '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) as_cell_range_script_id = fields.StringField('as_cell_range_script_id', title='Cell range method', description=( "Method returning columns, lines and tabs. The method is passed" " matrixbox=True, base_id=base_id as arguments."), default='', required=0) columns = fields.ListTextAreaField('columns', title="Columns", description=( "This defines columns 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.\n" "For example (('color/blue', 'Bleu'), ('color/red','Red')).\n" " Deprecated, use cell range method instead"), default=[], required=0) lines = fields.ListTextAreaField('lines', title="Lines", description=( "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.\n" "For example (('size/baby/02','baby/02'),('size/baby/03','baby/03')).\n" "Deprecated, use cell range method instead"), default=[], required=0) tabs = fields.ListTextAreaField('tabs', title="Tabs", description=( "This defines tabs. You can use it with the same way as Lines " "and Columns.\n" "This is used only if you have more than 2 kinds of variations.\n" "Deprecated, use cell range method instead"), default=[], required=0) # XXX ListTextAreaField ? cell_range = fields.ListTextAreaField('cell_range', title="Cell Range", description=( "This defines the range of the matrix."), default=[], required=0) getter_method = fields.StringField('getter_method', title='Getter method', description=( "You can specify a specific method in order to retrieve the context. " "This field can be empty, if so the MatrixBox will use the default " "context."), default='', required=0) cell_getter_method = fields.StringField('cell_getter_method', title='Cell Getter method', description=( "You can specify a method in order to retrieve cells. This field can " "be empty, if so the MatrixBox will use the default method : getCell." ), 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. " "This field can be empty, if so the MatrixBox will use the default " "method : newCell."), default='', required=0) editable_attributes = fields.ListTextAreaField('editable_attributes', title="Editable Properties", description=( "A list of attributes which are set by hidden fields called " "matrixbox_attribute_name. This is used " "when we want to specify a computed value for each cell"), default=[], required=0) global_attributes = fields.ListTextAreaField('global_attributes', title="Global Properties", description=( "An optional list of globals attributes which are set by hidden " "fields and which are applied to each cell. " "This is used if we want to set the same value for every cell"), default=[], required=0) cell_base_id = fields.StringField('cell_base_id', title='Base id for cells', description=( "The Base id for cells : this is the name used to store cells, " "we usually use names like : 'movement', 'path', ... "), default='cell', required=0) cell_portal_type = fields.StringField('cell_portal_type', title='Portal Type for cells', description=( "The Portal Type for cells : This is the portal type used to " "create a new cell."), default='Mapped Value', required=0) update_cell_range = fields.CheckBoxField('update_cell_range', title="Update Cell Range", description=( "The cell range should be updated upon edit."), default=0) def render(self, field, key, value, REQUEST, render_format='html', render_prefix=None): """ 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') 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 '' as_cell_range_script_id = field.get_value('as_cell_range_script_id') extra_dimension_category_list_list = [None] if as_cell_range_script_id: lines = [] columns = [] tabs = [] dimension_list = guarded_getattr(context, as_cell_range_script_id)(matrixbox=True, base_id=cell_base_id) len_dimension_list = len(dimension_list) if len_dimension_list: if len_dimension_list == 1: lines, = dimension_list elif len_dimension_list == 2: lines, columns = dimension_list elif len_dimension_list >= 3: lines, columns, tabs = dimension_list[:3] if len_dimension_list > 3: extra_dimension_list = dimension_list[3:] extra_dimension_category_label_dict = {} extra_dimension_category_index_dict = {} for extra_dimension in extra_dimension_list: for index, (category, label) in enumerate(extra_dimension): extra_dimension_category_label_dict[category] = label extra_dimension_category_index_dict[category] = index from Products.ERP5Type.Utils import cartesianProduct extra_dimension_category_list_list = cartesianProduct( [[category for category, label in extra_dimension] for extra_dimension in extra_dimension_list]) else: lines = field.get_value('lines') columns = field.get_value('columns') tabs = field.get_value('tabs') field_errors = REQUEST.get('field_errors', {}) cell_getter_method_id = field.get_value('cell_getter_method') if cell_getter_method_id not in (None, ''): cell_getter_method = getattr(here, cell_getter_method_id) else: cell_getter_method = context.getCell editable_attributes = field.get_value('editable_attributes') # This is required when we have no tabs if len(tabs) == 0: tabs = [(None,None)] # This is required when we have no columns if len(columns) == 0: columns = [(None,None)] 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] # 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) # result for the list render list_result = [] url = REQUEST.URL list_html = '' for extra_dimension_category_list in extra_dimension_category_list_list: if extra_dimension_category_list is None: extra_dimension_label = '' extra_dimension_position = () extra_dimension_index = '' extra_dimension_category_list = [] else: extra_dimension_label = ','+','.join([extra_dimension_category_label_dict[category] for category in extra_dimension_category_list]) extra_dimension_position = tuple([extra_dimension_category_index_dict[category] for category in extra_dimension_category_list]) extra_dimension_index = '_'+'_'.join(map(str, extra_dimension_position)) # Create one table per tab k = 0 for tab in tabs: tab_id = tab[0] if (tab_id is not None) and \ (not isinstance(tab_id, (list, tuple))): tab_id = [tab_id] if render_format == 'list': list_result_tab = [[tab[1]]] # Create the header of the table - this should probably become DTML first_tab = tab[1] or '' header = """\ <!-- Matrix Content --> <div class="matrixbox_label_tab">%s</div> <div class="MatrixContent"> <table cellpadding="0" cellspacing="0" border="0"> """ % (first_tab+extra_dimension_label) # Create the footer. This should be replaced by DTML # And work as some kind of parameter footer = """\ <tr> <td colspan="%s" align="center" valign="middle" class="Data footer"> </td> </tr> </table> </div> """ % len(columns) list_header = """\ <tr class="matrixbox_label_line"><td class=\"Data\"></td> """ for cname in columns: 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]) list_header = list_header + "</tr>" # Build Lines i = 0 j = 0 list_body = '' for l in lines: if not i % 2: td_css = 'DataA' else: td_css = 'DataB' list_body = list_body + '\n<tr class=\"%s\"><td class=\"matrixbox_label_column\">%s</td>' % (td_css, str(l[1])) j = 0 if render_format == 'list': list_result_lines = [ str(l[1]) ] for c in columns: has_error = 0 column_id = c[0] if (column_id is not None) and \ (not isinstance(column_id, (list, tuple))): column_id = [column_id] if column_id is None: kw = [l[0]] elif tab_id is None: kw = [l[0], c[0]] else: kw = [l[0], c[0]] + tab_id + extra_dimension_category_list kwd = {} kwd['base_id'] = cell_base_id cell = cell_getter_method(*kw, **kwd) REQUEST['cell'] = cell cell_body = '' 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) key = my_field.id + '_cell_%s_%s_%s%s' % (i,j,k,extra_dimension_index) if cell is not None: attribute_value = my_field.get_value('default', cell=cell, cell_index=kw, cell_position = ((i,j,k)+extra_dimension_position)) if render_format=='html': display_value = attribute_value if field_errors: # Display previous value in case of any error # in this form because we have no cell to get # value from display_value = REQUEST.get('field_%s' % key, attribute_value) else: display_value = attribute_value if field_errors.has_key(key): # Display error message if this cell has an error has_error = 1 cell_body += '<span class="input">%s</span>%s' % ( my_field.render(value=display_value, REQUEST=REQUEST, key=key), translateString(field_errors[key].error_text)) else: cell_body += '<span class="input">%s</span>' %\ my_field.render( value=display_value, REQUEST=REQUEST, key=key) elif render_format == 'list': if not my_field.get_value('hidden'): list_result_lines.append(attribute_value) else: attribute_value = my_field.get_value('default', cell=None, cell_index=kw, cell_position=((i,j,k)+extra_dimension_position)) if render_format == 'html': if field_errors: # Display previous value in case of any error # in this form because we have no cell to get # value from display_value = REQUEST.get('field_%s' % key, attribute_value) else: display_value = attribute_value if field_errors.has_key(key): # Display error message if this cell has an error has_error = 1 cell_body += '<span class="input">%s</span>%s' % ( my_field.render(value=display_value, REQUEST=REQUEST, key=key), translateString(field_errors[key].error_text)) else: cell_body += '<span class="input">%s</span>' %\ my_field.render( value=display_value, REQUEST=REQUEST, key=key) elif render_format == 'list': list_result_lines.append(None) css = td_css if has_error : css = 'error' list_body = list_body + \ ('<td class=\"%s\">%s</td>' % (css, cell_body)) j += 1 list_body = list_body + '</tr>' i += 1 if render_format == 'list': list_result_tab.append(list_result_lines) list_html += header + list_header + \ list_body + footer k += 1 if render_format == 'list': list_result.append(list_result_tab) if render_format == 'list': return list_result 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') as_cell_range_script_id = field.get_value('as_cell_range_script_id') 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 {} extra_dimension_category_list_list = [None] if as_cell_range_script_id: lines = [] columns = [] tabs = [] dimension_list = guarded_getattr(context, as_cell_range_script_id)(matrixbox=True, base_id=cell_base_id) len_dimension_list = len(dimension_list) if len_dimension_list: if len_dimension_list == 1: lines, = dimension_list elif len_dimension_list == 2: lines, columns = dimension_list elif len_dimension_list >= 3: lines, columns, tabs = dimension_list[:3] if len_dimension_list > 3: extra_dimension_list = dimension_list[3:] extra_dimension_category_label_dict = {} extra_dimension_category_index_dict = {} for extra_dimension in extra_dimension_list: for index, (category, label) in enumerate(extra_dimension): extra_dimension_category_label_dict[category] = label extra_dimension_category_index_dict[category] = index from Products.ERP5Type.Utils import cartesianProduct extra_dimension_category_list_list = cartesianProduct( [[category for category, label in extra_dimension] for extra_dimension in extra_dimension_list]) else: lines = field.get_value('lines') columns = field.get_value('columns') tabs = field.get_value('tabs') editable_attributes = field.get_value('editable_attributes') error_list = [] cell_getter_method_id = field.get_value('cell_getter_method') if cell_getter_method_id not in (None, ''): cell_getter_method = getattr(here, cell_getter_method_id) else: cell_getter_method = context.getCell # This is required when we have no tabs if len(tabs) == 0: tabs = [(None,None)] # This is required when we have no columns if len(columns) == 0: columns = [(None,None)] # 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] result = {} for extra_dimension_category_list in extra_dimension_category_list_list: if extra_dimension_category_list is None: extra_dimension_label = '' extra_dimension_position = () extra_dimension_index = '' extra_dimension_category_list = [] else: extra_dimension_label = ','+','.join([extra_dimension_category_label_dict[category] for category in extra_dimension_category_list]) extra_dimension_position = tuple([extra_dimension_category_index_dict[category] for category in extra_dimension_category_list]) extra_dimension_index = '_'+'_'.join(map(str, extra_dimension_position)) k = 0 # Create one table per tab for tab_id in tab_ids: if (tab_id is not None) and \ (not isinstance(tab_id, (list, tuple))): tab_id = [tab_id] i = 0 j = 0 for l in line_ids: j = 0 for c in column_ids: if c is None: kw = [l] elif tab_id is None: kw = [l, c] else: kw = [l, c] + tab_id + extra_dimension_category_list kw = tuple(kw) kwd = {} kwd['base_id'] = cell_base_id cell = cell_getter_method(*kw, **kwd) 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) if my_field.get_value('editable'): key = 'field_' + my_field.id + '_cell_%s_%s_%s%s' % (i,j,k,extra_dimension_index) attribute_value = my_field.get_value('default', cell=cell, cell_index=kw, cell_position = ((i,j,k)+extra_dimension_position)) value = None try : value = my_field.validator.validate( my_field, key, REQUEST) except ValidationError, err : err.field_id = my_field.id + '_cell_%s_%s_%s%s' % (i,j,k,extra_dimension_index) error_list.append(err) if (attribute_value != value or \ attribute_value not in ('',None,(),[])) \ and not my_field.get_value('hidden'): # Only validate modified values from visible fields result.setdefault(kw, {})[attribute_id] = value else: if result.has_key(kw): result[kw][attribute_id] = value j += 1 i += 1 k += 1 if len(error_list): raise FormValidationError(error_list, {}) return result MatrixBoxValidatorInstance = MatrixBoxValidator() class MatrixBox(ZMIField): meta_type = "MatrixBox" widget = MatrixBoxWidgetInstance validator = MatrixBoxValidatorInstance security = ClassSecurityInfo() security.declareProtected('Access contents information', 'get_value') def get_value(self, id, **kw): 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')) else: return ZMIField.get_value(self, id, **kw) # Psyco from Products.ERP5Type.PsycoWrapper import psyco psyco.bind(MatrixBoxWidget.render) psyco.bind(MatrixBoxValidator.validate) # Register get_value from Products.ERP5Form.ProxyField import registerOriginalGetValueClassAndArgument registerOriginalGetValueClassAndArgument(MatrixBox, 'default')