diff --git a/product/ERP5Form/MultiRelationField.py b/product/ERP5Form/MultiRelationField.py index 96bef60c1216ac1bd2659e6d0a44ab789079b9c6..a3dac14f1d5f2ba72cff3d445e97e388e99f5792 100755 --- a/product/ERP5Form/MultiRelationField.py +++ b/product/ERP5Form/MultiRelationField.py @@ -1,7 +1,8 @@ ############################################################################## # -# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved. +# Copyright (c) 2002, 2004 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 @@ -31,8 +32,32 @@ from Products.Formulator.Field import ZMIField from Products.Formulator.DummyField import fields from Products.ERP5Type.Utils import convertToUpperCase from Products.CMFCore.utils import getToolByName +from Products.ERP5Form import RelationField +from Products.ERP5Form.RelationField import MAX_SELECT, new_content_prefix +from Globals import get_request +from Products.PythonScripts.Utility import allow_class -class MultiRelationStringFieldWidget(Widget.LinesTextAreaWidget): +import string +from zLOG import LOG +#MAX_SELECT = 50 # Max. number of catalog result +#new_content_prefix = '_newContent_' + +def checkSameKeys(a , b): + """ + Checks if the two lists contain + the same values + """ + same = 1 + for ka in a: + if (not ka in b) and (ka != ''): + same = 0 + for kb in b: + if (not kb in a) and (kb != ''): + same = 0 + return same + + +class MultiRelationStringFieldWidget(Widget.LinesTextAreaWidget, RelationField.RelationStringFieldWidget): """ RelationStringField widget @@ -44,74 +69,118 @@ class MultiRelationStringFieldWidget(Widget.LinesTextAreaWidget): """ property_names = Widget.LinesTextAreaWidget.property_names + \ - ['update_method', 'jump_method', 'base_category', 'portal_type', 'catalog_index', - 'default_module', 'relation_setter_id','columns'] - - update_method = fields.StringField('update_method', - title='Update Method', - description=( - "The method to call to set the relation. Required."), - default="Base_validateRelation", - required=1) - - jump_method = fields.StringField('jump_method', - title='Jump Method', - description=( - "The method to call to jump to the relation. Required."), - default="Base_jumpToRelatedDocument", - required=1) - - base_category = fields.StringField('base_category', - title='Base Category', - description=( - "The method to call to set the relation. Required."), - default="", - required=1) - - portal_type = fields.ListTextAreaField('portal_type', - title='Portal Type', - description=( - "The method to call to set the relation. Required."), - default="", - required=1) - - catalog_index = fields.StringField('catalog_index', - title='Catalog Index', - description=( - "The method to call to set the relation. Required."), - default="", - required=1) - - default_module = fields.StringField('default_module', - title='Default Module', - description=( - "The module which should be invoked to create new objects."), - default="", - required=1) - - relation_setter_id = fields.StringField('relation_setter_id', - title='Relation Update Method', - description=( - "The method to invoke in order to update the relation"), - default="", - required=0) - - columns = fields.ListTextAreaField('columns', - title="Columns", - description=( - "A list of attributes names to display."), - default=[], - required=0) + RelationField.RelationStringFieldWidget.property_names + + # delete double in order to keep a usable ZMI... + #property_names = dict([(i,0) for i in property_names]).keys() # XXX need to keep order ! + _v_dict = {} + _v_property_name_list = [] + for property_name in property_names: + if not _v_dict.has_key(property_name): + _v_property_name_list.append(property_name) + _v_dict[property_name] = 1 + property_names = _v_property_name_list + def render(self, field, key, value, REQUEST): - """Render text input field. + """ + Render text input field. """ here = REQUEST['here'] - html_string = Widget.LinesTextAreaWidget.render(self, field, key, value, REQUEST) - portal_url_string = getToolByName(here, 'portal_url')() - # We add a button which has a path reference to a base category... - html_string += ' <input type="image" src="%s/images/exec16.png" value="update..." name="%s:method">' \ - % (portal_url_string,field.get_value('update_method')) + + relation_field_id = 'relation_%s' % key + relation_item_id = 'item_%s' % key + + portal_url = getToolByName(here, 'portal_url') + portal_url_string = portal_url() + portal_object = portal_url.getPortalObject() + + if type(value) == type(''): + # Value is a string, reformat it correctly + value_list = string.split(value, "\r\n") + else: + value_list = value + + need_validation = 0 + # Check all relation + for i in range( len(value_list) ): + relation_field_id = 'relation_%s_%s' % ( key, i ) + relation_item_id = 'item_%s_%s' % ( key, i ) + if REQUEST.has_key(relation_item_id) and value_list[i] != '': + need_validation = 1 + break + + html_string = '' + if need_validation: + # Check all relation + for i in range( len(value_list) ): + value = value_list[i] + relation_field_id = 'relation_%s_%s' % ( key, i ) + relation_item_id = 'item_%s_%s' % ( key, i ) + + + # If we get a empty string, display nothing ! + if value == '': + pass + + else: + + html_string += Widget.TextWidget.render(self, field, key, value, REQUEST) + + if REQUEST.has_key(relation_item_id): + relation_item_list = REQUEST.get(relation_item_id) + + if relation_item_list != []: + # Define default tales on the fly + tales_expr = field.tales.get('items', None) + defined_tales = 0 + if not tales_expr: + defined_tales = 1 + from Products.Formulator.TALESField import TALESMethod + field.tales['items'] = TALESMethod('REQUEST/relation_item_list') + + + REQUEST['relation_item_list'] = relation_item_list + html_string += ' %s ' % Widget.ListWidget.render(self, + field, relation_field_id, None, REQUEST) + REQUEST['relation_item_list'] = None + + if defined_tales: + # Delete default tales on the fly + field.tales['items'] = None + + else: + html_string += ' <input type="image" src="%s/images/exec16.png" value="update..." name="%s/portal_selections/viewSearchRelatedDocumentDialog%s_%s:method">' \ + % (portal_url_string, portal_object.getPath(), field.aq_parent._v_relation_field_index, i) + + html_string += '<br/>' + + else: + # no modification made, we can display only a lines text area widget + html_string += Widget.LinesTextAreaWidget.render(self, field, key, value_list, REQUEST) + if value_list not in ((), [], None) and value_list == field.get_value('default'): + if REQUEST.get('selection_name') is not None: + html_string += ' <a href="%s?field_id=%s&form_id=%s&selection_name=%s&selection_index=%s"><img src="%s/images/jump.png"></a>' \ + % (field.get_value('jump_method'), field.id, field.aq_parent.id, REQUEST.get('selection_name'), REQUEST.get('selection_index'),portal_url_string) + else: + html_string += ' <a href="%s?field_id=%s&form_id=%s"><img src="%s/images/jump.png"></a>' \ + % (field.get_value('jump_method'), field.id, field.aq_parent.id,portal_url_string) + + field.aq_parent._v_relation_field_index += 1 # Increase index + return html_string + + def render_view(self, field, value): + """ + Render text field. + """ + REQUEST = get_request() + here = REQUEST['here'] + + portal_url = getToolByName(here, 'portal_url') + portal_url_string = portal_url() + + # no modification made, we can display only a lines text area widget + html_string = Widget.LinesTextAreaWidget.render_view(self, field, value) if value not in ((), [], None, ''): if REQUEST.get('selection_name') is not None: html_string += ' <a href="%s?field_id=%s&form_id=%s&selection_name=%s&selection_index=%s"><img src="%s/images/jump.png"></a>' \ @@ -119,10 +188,261 @@ class MultiRelationStringFieldWidget(Widget.LinesTextAreaWidget): else: html_string += ' <a href="%s?field_id=%s&form_id=%s"><img src="%s/images/jump.png"></a>' \ % (field.get_value('jump_method'), field.id, field.aq_parent.id,portal_url_string) + return html_string +class MultiRelationEditor: + """ + A class holding all values required to update a relation + """ + def __init__(self, field_id, base_category, portal_type, portal_type_item, key, relation_setter_id, relation_editor_list): + + + self.field_id = field_id + self.base_category = base_category + self.portal_type = portal_type + self.portal_type_item = portal_type_item + self.key = key + self.relation_setter_id = relation_setter_id + self.relation_editor_list = relation_editor_list + + + def __call__(self, REQUEST): + if self.relation_editor_list != None: + value_list = [] + + + for i, value, uid, display_text in self.relation_editor_list: + value_list.append(value) + if uid is not None: + # Decorate the request so that we can display + # the select item in a popup + #relation_field_id = 'relation_%s_%s' % ( self.key, i ) + #relation_item_id = 'item_%s_%s' % ( self.key, i ) + relation_field_id = 'relation_field_%s_%s' % ( self.field_id, i ) + relation_item_id = 'item_field_%s_%s' % ( self.field_id, i ) + + + REQUEST.set(relation_item_id, ((display_text, uid),)) + REQUEST.set(relation_field_id, uid) + + #REQUEST.set(self.field_id[len('field_'):], value_list) # XXX Dirty + REQUEST.set(self.field_id, value_list) # XXX Dirty + else: + # Make sure no default value appears + #REQUEST.set(self.field_id[len('field_'):], None) + REQUEST.set(self.field_id, None) # XXX Dirty + + def view(self): + return self.__dict__ + + def edit(self, o): + if self.relation_editor_list != None: + + relation_uid_list = [] + + for i, value, uid, display_text in self.relation_editor_list: + if uid is not None: + if type(uid) is type('a') and uid.startswith(new_content_prefix): + # Create a new content + portal_type = uid[len(new_content_prefix):] + portal_module = None + for p_item in self.portal_type_item: + if p_item[0] == portal_type: + portal_module = o.getPortalObject().getDefaultModuleId( p_item[0] ) + if portal_module is not None: + portal_module_object = getattr(o.getPortalObject(), portal_module) + kw ={} + #kw[self.key] = value + kw[self.key] = string.join( string.split(value,'%'), '' ) + kw['portal_type'] = portal_type + kw['immediate_reindex'] = 1 + new_object = portal_module_object.newContent(**kw) + uid = new_object.getUid() + else: + raise + relation_uid_list.append(int(uid)) + + #if relation_uid_list != []: + + # Edit relation + if self.relation_setter_id: + relation_setter = getattr(o, self.relation_setter_id) + relation_setter((), portal_type=self.portal_type) + relation_setter( relation_uid_list , portal_type=self.portal_type) + else: + o._setValueUids(self.base_category, (), portal_type=self.portal_type) + o._setValueUids(self.base_category, relation_uid_list, portal_type=self.portal_type) + + else: + # Nothing to do + pass +# # Delete relation +# if self.relation_setter_id: +# relation_setter = getattr(o, self.relation_setter_id) +# relation_setter((), portal_type=self.portal_type) +# else: +# o._setValueUids(self.base_category, (), portal_type=self.portal_type) + +allow_class(MultiRelationEditor) + + +class MultiRelationStringFieldValidator(Validator.LinesValidator, RelationField.RelationStringFieldValidator): + """ + Validation includes lookup of relared instances + """ + message_names = Validator.LinesValidator.message_names + \ + RelationField.RelationStringFieldValidator.message_names + + # delete double in order to keep a usable ZMI... + #message_names = dict([(i,0) for i in message_names]).keys() # XXX need to keep order ! + _v_dict = {} + _v_message_name_list = [] + for message_name in message_names: + if not _v_dict.has_key(message_name): + _v_message_name_list.append(message_name) + _v_dict[message_name] = 1 + message_names = _v_message_name_list + + def validate(self, field, key, REQUEST): + portal_type = map(lambda x:x[0],field.get_value('portal_type')) + portal_type_item = field.get_value('portal_type') + base_category = field.get_value( 'base_category') + + # If the value is different, build a query + portal_selections = getToolByName(field, 'portal_selections') + portal_catalog = getToolByName(field, 'portal_catalog') + + # Get the current value + value_list = Validator.LinesValidator.validate(self, field, key, REQUEST) + +# if type(value_list) == type(''): +# value_list = [value_list] + + # If the value is the same as the current field value, do nothing + current_value_list = field.get_value('default') + if type(current_value_list) == type(''): + current_value_list = [current_value_list] + + catalog_index = field.get_value('catalog_index') + relation_setter_id = field.get_value('relation_setter_id') + + if checkSameKeys( value_list, current_value_list ): + # XXX Will be interpreted by Base_edit as "do nothing" + #return MultiRelationEditor(field.id, base_category, portal_type, portal_type_item, catalog_index, relation_setter_id, None) + return None + + else: + + # We must be able to erase the relation + if value_list == ['']: + display_text = 'Delete the relation' + return MultiRelationEditor(field.id, base_category, portal_type, portal_type_item, catalog_index, relation_setter_id, []) +# return RelationEditor(key, base_category, portal_type, None, +# portal_type_item, catalog_index, value, relation_setter_id, display_text) + # Will be interpreted by Base_edit as "delete relation" (with no uid and value = '') + + else: + relation_editor_list = [] + raising_error_needed = 0 + raising_error_value = '' + + # Check all relation + for i in range( len(value_list) ): + relation_field_id = 'relation_%s_%s' % ( key, i ) + relation_item_id = 'item_%s_%s' % ( key, i ) + + relation_uid = REQUEST.get(relation_field_id, None) + + value = value_list[i] + + + # If we get a empty string, delete this line + if value == '': + # Clean request if necessary + if REQUEST.has_key( relation_field_id): + REQUEST.pop(relation_field_id) + + else: + # Got a true value + + if relation_uid not in (None, ''): + # A value has been defined by the user in popup menu + if type(relation_uid) in (type([]), type(())): relation_uid = relation_uid[0] + related_object = portal_catalog.getObject(relation_uid) + if related_object is not None: + display_text = str(related_object.getProperty(catalog_index)) + else: + display_text = 'Object has been deleted' + # Check + REQUEST.set(relation_item_id, ( (display_text, relation_uid), )) + relation_editor_list.append( (i, value, str(relation_uid), display_text) ) + + else: + + kw ={} + kw[catalog_index] = value + kw['portal_type'] = portal_type + # Get the query results + relation_list = portal_catalog(**kw) + relation_uid_list = map(lambda x: x.uid, relation_list) + + # Prepare a menu + menu_item_list = [('', '')] + new_object_menu_item_list = [] + for p in portal_type: + new_object_menu_item_list += [('New %s' % p, '%s%s' % (new_content_prefix,p))] + + if len(relation_list) >= MAX_SELECT: + # If the length is long, raise an error + # This parameter means we need listbox help + REQUEST.set(relation_item_id, []) + raising_error_needed = 1 + raising_error_value = 'relation_result_too_long' + + elif len(relation_list) == 1: + # If the length is 1, return uid + relation_uid = relation_uid_list[0] + related_object = portal_catalog.getObject(relation_uid) + if related_object is not None: + display_text = str(related_object.getProperty(catalog_index)) + else: + display_text = 'Object has been deleted' + + REQUEST.set(relation_item_id, ( (display_text, relation_uid), )) + relation_editor_list.append( (0, value, relation_uid, display_text) ) + + elif len(relation_list) == 0: + # If the length is 0, raise an error + menu_item_list += new_object_menu_item_list + REQUEST.set(relation_item_id, menu_item_list) + raising_error_needed = 1 + raising_error_value = 'relation_result_empty' + + else: + # If the length is short, raise an error + # len(relation_list) < MAX_SELECT: + + #menu_item_list += [('-', '')] + menu_item_list += map(lambda x: (x.getObject().getProperty(catalog_index), x.uid), + relation_list) + REQUEST.set(relation_item_id, menu_item_list) + raising_error_needed = 1 + raising_error_value = 'relation_result_ambiguous' + + # validate MultiRelation field + if raising_error_needed: + # Raise error + self.raise_error(raising_error_value, field) + return value_list + else: + # Can return editor + return MultiRelationEditor(field.id, base_category, portal_type, portal_type_item, catalog_index, relation_setter_id, relation_editor_list) + + + MultiRelationStringFieldWidgetInstance = MultiRelationStringFieldWidget() -MultiRelationStringFieldValidatorInstance = Validator.LinesValidator() +MultiRelationStringFieldValidatorInstance = MultiRelationStringFieldValidator() class MultiRelationStringField(ZMIField): meta_type = "MultiRelationStringField"