RelationField.py 19.1 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 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 29 30 31 32
#
# 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 Products.Formulator import Widget, Validator
from Products.Formulator.Field import ZMIField
from Products.Formulator.DummyField import fields
from Products.ERP5Type.Utils import convertToUpperCase
33
from Products.CMFCore.utils import getToolByName
Alexandre Boeglin's avatar
Alexandre Boeglin committed
34
from Globals import get_request
35
from Products.PythonScripts.Utility import allow_class
Jean-Paul Smets's avatar
Jean-Paul Smets committed
36

Romain Courteaud's avatar
Romain Courteaud committed
37 38
import string

Sebastien Robin's avatar
Sebastien Robin committed
39
from zLOG import LOG
Romain Courteaud's avatar
Romain Courteaud committed
40
MAX_SELECT = 30 # Max. number of catalog result
41
new_content_prefix = '_newContent_'
Sebastien Robin's avatar
Sebastien Robin committed
42

43 44

class RelationStringFieldWidget(Widget.TextWidget, Widget.ListWidget):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
45 46 47 48 49 50 51 52 53 54 55
    """
        RelationStringField widget

        Works like a string field but includes one buttons

        - one search button which updates the field and sets a relation

        - creates object if not there

    """
    property_names = Widget.TextWidget.property_names + \
56
      ['update_method', 'jump_method', 'allow_jump', 'base_category', 'portal_type', 'allow_creation', 'catalog_index',
57
       'default_module', 'relation_setter_id', 'columns','sort','parameter_list','list_method',
58
       'first_item', 'items', 'size', 'extra_item']
Jean-Paul Smets's avatar
Jean-Paul Smets committed
59

60
    # XXX Field to remove...
Jean-Paul Smets's avatar
Jean-Paul Smets committed
61 62 63 64
    update_method = fields.StringField('update_method',
                               title='Update Method',
                               description=(
        "The method to call to set the relation. Required."),
65
                               default="Base_validateRelation",
Jean-Paul Smets's avatar
Jean-Paul Smets committed
66 67 68 69 70 71
                               required=1)

    jump_method = fields.StringField('jump_method',
                               title='Jump Method',
                               description=(
        "The method to call to jump to the relation. Required."),
Yoshinori Okuji's avatar
Yoshinori Okuji committed
72
                               default="Base_jumpToRelatedDocument",
Jean-Paul Smets's avatar
Jean-Paul Smets committed
73 74
                               required=1)

75 76
    allow_jump = fields.CheckBoxField('allow_jump',
                               title='Allow Jump',
77 78 79 80 81
                               description=(
        "Do we allow to jump to the relation ?"),
                               default=1,
                               required=0)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
82 83 84 85 86 87 88 89 90 91 92 93 94 95
    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)

96 97 98 99 100 101 102
    allow_creation = fields.CheckBoxField('allow_creation',
                               title='Allow Creation',
                               description=(
        "Do we allow to create new objects ?"),
                               default=1,
                               required=0)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
103 104 105 106 107 108 109 110 111 112 113 114 115 116
    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)

117 118
    # XXX Is it a good idea to keep such a field ??
    # User can redefine setter method with a script (and so, don't use the API)
119 120 121 122 123 124 125
    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)

126 127 128 129 130 131 132
    size = fields.IntegerField('size',
                               title='Size',
                               description=(
        "The display size in rows of the field. If set to 1, the "
        "widget will be displayed as a drop down box by many browsers, "
        "if set to something higher, a list will be shown. Required."),
                               default=1,
133 134
                               required=1)

Romain Courteaud's avatar
Romain Courteaud committed
135 136 137 138 139 140 141
    columns = fields.ListTextAreaField('columns',
                                 title="Columns",
                                 description=(
        "A list of attributes names to display."),
                                 default=[],
                                 required=0)

Sebastien Robin's avatar
Sebastien Robin committed
142 143 144 145 146 147
    sort = fields.ListTextAreaField('sort',
                                 title='Default Sort',
                                 description=('The default sort keys and order'),
                                 default=[],
                                 required=0)

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
    parameter_list = fields.ListTextAreaField('parameter_list',
                                 title="Parameter List",
                                 description=(
        "A list of paramters used for the portal_catalog."),
                                 default=[],
                                 required=0)

    list_method = fields.MethodField('list_method',
                                 title='List Method',
                                 description=('The method to use to list'
                                              'objects'),
                                 default='',
                                 required=0)


Jean-Paul Smets's avatar
Jean-Paul Smets committed
163 164 165
    def render(self, field, key, value, REQUEST):
        """Render text input field.
        """
166 167
        relation_field_id = 'relation_%s' % key
        relation_item_id = 'item_%s' % key
Sebastien Robin's avatar
Sebastien Robin committed
168
        here = REQUEST['here']
169 170 171
        portal_url = getToolByName(here, 'portal_url')
        portal_url_string = portal_url()
        portal_object = portal_url.getPortalObject()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
172
        html_string = Widget.TextWidget.render(self, field, key, value, REQUEST)
Romain Courteaud's avatar
Romain Courteaud committed
173

174 175 176 177 178 179 180
        if REQUEST.has_key(relation_item_id):
          # Define default tales on the fly
          tales_expr = field.tales.get('items', None)
          if not tales_expr:
            from Products.Formulator.TALESField import TALESMethod
            field.tales['items'] = TALESMethod('REQUEST/relation_item_list')
          REQUEST['relation_item_list'] = REQUEST.get(relation_item_id)
181 182 183
          html_string += '&nbsp;%s&nbsp;' % Widget.ListWidget.render(self,
                                field, relation_field_id, None, REQUEST)
          REQUEST['relation_item_list'] = None
Romain Courteaud's avatar
Romain Courteaud committed
184

Jean-Paul Smets's avatar
Jean-Paul Smets committed
185 186 187 188
        # We used to add a button which has a path reference to a base category...
        # but it really created too many problems
        # now we do it in another way
        # we compare what has been changed in the relation update script
Romain Courteaud's avatar
Romain Courteaud committed
189

Romain Courteaud's avatar
Romain Courteaud committed
190
        #elif value != field.get_value('default'):
191
        else:
192
            html_string += '&nbsp;<input type="image" src="%s/images/exec16.png" value="update..." name="%s/portal_selections/viewSearchRelatedDocumentDialog%s:method"/>' \
193
              %  (portal_url_string, portal_object.getPath(),
Jean-Paul Smets's avatar
Jean-Paul Smets committed
194
                  getattr(field.aq_parent, '_v_relation_field_index', 0))
Romain Courteaud's avatar
Romain Courteaud committed
195

Jean-Paul Smets's avatar
Jean-Paul Smets committed
196
        relation_field_index = getattr(field.aq_parent, '_v_relation_field_index', 0)
197
        field.aq_parent._v_relation_field_index = relation_field_index + 1 # Increase index
Romain Courteaud's avatar
Romain Courteaud committed
198

199
        if value not in ( None, '' ) and not REQUEST.has_key(relation_item_id) and value == field.get_value('default') and field.get_value('allow_jump') == 1 :
Jean-Paul Smets's avatar
Jean-Paul Smets committed
200
          if REQUEST.get('selection_name') is not None:
201
            html_string += '&nbsp;&nbsp;<a href="%s/%s?field_id=%s&form_id=%s&selection_name=%s&selection_index=%s"><img src="%s/images/jump.png"/></a>' \
202
              % (here.absolute_url(), field.get_value('jump_method'), field.id, field.aq_parent.id, REQUEST.get('selection_name'), REQUEST.get('selection_index'),portal_url_string)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
203
          else:
204
            html_string += '&nbsp;&nbsp;<a href="%s/%s?field_id=%s&form_id=%s"><img src="%s/images/jump.png"/></a>' \
205
              % (here.absolute_url(), field.get_value('jump_method'), field.id, field.aq_parent.id,portal_url_string)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
206 207
        return html_string

208
    def render_view(self, field, value):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
209 210
        """Render text input field.
        """
211
        if field.get_value('allow_jump') == 0 :
212
          return Widget.TextWidget.render_view(self, field, value)
Alexandre Boeglin's avatar
Alexandre Boeglin committed
213 214
        REQUEST = get_request()
        here = REQUEST['here']
215
        html_string = Widget.TextWidget.render_view(self, field, value)
Alexandre Boeglin's avatar
Alexandre Boeglin committed
216
        portal_url_string = getToolByName(here, 'portal_url')()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
217
        if value not in ('', None):
218 219
          html_string = '<a href="%s/%s?field_id=%s&form_id=%s">%s</a>' \
            % (here.absolute_url(), field.get_value('jump_method'), field.id, field.aq_parent.id, html_string)
220
          html_string += '&nbsp;&nbsp;<a href="%s/%s?field_id=%s&form_id=%s"><img src="%s/images/jump.png"/></a>' \
221
            % (here.absolute_url(), field.get_value('jump_method'), field.id, field.aq_parent.id, portal_url_string)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
222 223
        return html_string

224 225 226 227 228
class RelationEditor:
    """
      A class holding all values required to update a relation
    """

229
    def __init__(self, field_id, base_category, portal_type, uid, portal_type_item,
230 231 232 233 234 235 236 237 238 239
                       key, value, relation_setter_id, display_text):
      self.field_id = field_id
      self.uid = uid
      self.base_category = base_category
      self.portal_type = portal_type
      self.portal_type_item = portal_type_item
      self.key = key
      self.value = value
      self.relation_setter_id = relation_setter_id
      self.display_text = display_text
240

241
    def __call__(self, REQUEST):
242
      if self.uid is not None:
243 244
        # Decorate the request so that we can display
        # the select item in a popup
245
        relation_field_id = 'relation_%s' % self.field_id
246 247 248 249 250 251
        relation_item_id = 'item_%s' % self.field_id
        REQUEST.set(relation_item_id, ((self.display_text, self.uid),))
        REQUEST.set(relation_field_id, self.uid)
        REQUEST.set(self.field_id[len('field_'):], self.value) # XXX Dirty
      else:
        # Make sure no default value appears
252 253
        REQUEST.set(self.field_id[len('field_'):], None)

254
    def view(self):
255 256 257
      return self.__dict__

    def edit(self, o):
258 259 260 261 262 263 264
      if self.uid is not None:
        if type(self.uid) is type('a') and self.uid.startswith(new_content_prefix):
          # Create a new content
          portal_type = self.uid[len(new_content_prefix):]
          portal_module = None
          for p_item in self.portal_type_item:
            if p_item[0] == portal_type:
265 266
              #portal_module = p_item[1]
              portal_module = o.getPortalObject().getDefaultModuleId( p_item[0] )
267
          if portal_module is not None:
268 269
            portal_module_object = getattr(o.getPortalObject(), portal_module)
            kw ={}
Romain Courteaud's avatar
Romain Courteaud committed
270
            kw[self.key] = string.join( string.split(self.value,'%'), '' )
271
            kw['portal_type'] = portal_type
Romain Courteaud's avatar
Romain Courteaud committed
272
            kw['immediate_reindex'] = 1
273 274 275
            new_object = portal_module_object.newContent(**kw)
            self.uid = new_object.getUid()
          else:
276
            raise
277

278
        # Edit relation
279 280 281
        if self.relation_setter_id:
          relation_setter = getattr(o, self.relation_setter_id)
          relation_setter((), portal_type=self.portal_type)
282
          relation_setter((int(self.uid),), portal_type=self.portal_type)
283
        else:
284 285
          # we could call a generic method which create the setter method name
          set_method_name = '_set'+convertToUpperCase(self.base_category)+'Value'
286
          object = o.portal_catalog.getObject( self.uid )
287
          getattr(o, set_method_name)( object,portal_type=self.portal_type )
288

Romain Courteaud's avatar
Romain Courteaud committed
289 290
      else:
        if self.value == '':
291
          # Delete relation
Romain Courteaud's avatar
Romain Courteaud committed
292 293 294 295
          if self.relation_setter_id:
            relation_setter = getattr(o, self.relation_setter_id)
            relation_setter((), portal_type=self.portal_type)
          else:
296 297 298
            # we could call a generic method which create the setter method name
            set_method_name = '_set'+convertToUpperCase(self.base_category)
            getattr(o, set_method_name)( None ,portal_type=self.portal_type)
Romain Courteaud's avatar
Romain Courteaud committed
299 300


301 302
allow_class(RelationEditor)

303
class RelationStringFieldValidator(Validator.StringValidator):
304 305
    """
        Validation includes lookup of relared instances
306 307
    """

308 309 310 311 312 313
    message_names = Validator.StringValidator.message_names +\
                    ['relation_result_too_long', 'relation_result_ambiguous', 'relation_result_empty',]

    relation_result_too_long = "Too many documents were found."
    relation_result_ambiguous = "Select appropriate document in the list."
    relation_result_empty = "No such document was found."
314

315
    def validate(self, field, key, REQUEST):
316
      relation_field_id = 'relation_%s' % key
317 318 319 320 321 322
      relation_item_id = 'item_%s' % key
      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')
323
      portal_catalog = getToolByName(field, 'portal_catalog')
324 325 326 327
      translation_service = getToolByName(field, 'translation_service', None)
      if translation_service is not None:
        N_ = translation_service.translate
      else :
328
        N_ = lambda catalog, msg, **kw:msg
329 330 331 332 333 334 335
      # Get the current value
      value = Validator.StringValidator.validate(self, field, key, REQUEST)
      # If the value is the same as the current field value, do nothing
      current_value = field.get_value('default')
      # If a relation has been defined in a popup menu, use it
      relation_uid = REQUEST.get(relation_field_id, None)
      catalog_index = field.get_value('catalog_index')
336
      parameter_list = field.get_value('parameter_list')
337
      relation_setter_id = field.get_value('relation_setter_id')
338

339
      if (value == current_value) and (relation_uid is None):
Romain Courteaud's avatar
Romain Courteaud committed
340
        return None
341
# XXX        return RelationEditor(key, base_category, portal_type, None,
Romain Courteaud's avatar
Romain Courteaud committed
342
#                              portal_type_item, catalog_index, value, relation_setter_id, None)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
343
                              # Will be interpreted by Base_edit as "do nothing"
344 345
      if relation_uid not in (None, ''):
        # A value has been defined by the user
346
        if type(relation_uid) in (type([]), type(())):
Romain Courteaud's avatar
Romain Courteaud committed
347 348
          if len( relation_uid ) == 0:
            # No object was selected...
349
            return None
Romain Courteaud's avatar
Romain Courteaud committed
350 351 352
          else:
            relation_uid = relation_uid[0]

353 354 355 356 357
        try:
          related_object = portal_catalog.getObject(relation_uid)
        except ValueError:
          # Catch the error raised when the uid is a string
          related_object = None
358 359 360
        if related_object is not None:
          display_text = str(related_object.getProperty(catalog_index))
        else:
361 362
          display_text = 'Object has been deleted'
        return RelationEditor(key, base_category, portal_type, relation_uid,
363
                              portal_type_item, catalog_index, value, relation_setter_id, display_text)
Romain Courteaud's avatar
Romain Courteaud committed
364 365 366 367

      # We must be able to erase the relation
      if value == '':
        display_text = 'Delete the relation'
368
        return RelationEditor(key, base_category, portal_type, None,
Romain Courteaud's avatar
Romain Courteaud committed
369 370 371
                              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 = '')

372

373 374 375
      kw ={}
      kw[catalog_index] = value
      kw['portal_type'] = portal_type
376
      kw['sort_on'] = catalog_index
377 378 379
      if len(parameter_list) > 0:
        for k,v in parameter_list:
          kw[k] = v
380 381 382 383 384
      # 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 = [('', '')]
Romain Courteaud's avatar
Romain Courteaud committed
385
      new_object_menu_item_list = []
386
      for p in portal_type:
387 388 389
        new_object_menu_item_list += [(N_('ui', 'New ${portal_type}',
             mapping={'portal_type':N_('ui', p)}),
             '%s%s' % (new_content_prefix,p))]
390 391 392 393 394 395 396
      # If the length is 1, return uid
      if len(relation_list) == 1:
        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:
397 398 399
          display_text = 'Object has been deleted'

        return RelationEditor(key, base_category, portal_type, relation_uid,
400 401
                              portal_type_item, catalog_index, value, relation_setter_id, display_text)
      # If the length is 0, raise an error
Romain Courteaud's avatar
Romain Courteaud committed
402
      elif len(relation_list) == 0:
403 404
        if field.get_value('allow_creation') == 1 :
          menu_item_list += new_object_menu_item_list
405 406 407
        REQUEST.set(relation_item_id, menu_item_list)
        self.raise_error('relation_result_empty', field)
      # If the length is short, raise an error
408 409 410
      elif len(relation_list) < MAX_SELECT:
        #menu_item_list += [('-', '')]
        menu_item_list += map(lambda x: (x.getObject().getProperty(catalog_index), x.uid),
411 412 413
                                                                        relation_list)
        REQUEST.set(relation_item_id, menu_item_list)
        self.raise_error('relation_result_ambiguous', field)
Romain Courteaud's avatar
Romain Courteaud committed
414 415
      else:
        # If the length is long, raise an error
416

Romain Courteaud's avatar
Romain Courteaud committed
417 418
        # If this error is raise, we don t want to create a new object...
        #REQUEST.set(relation_item_id, menu_item_list)
419 420
        self.raise_error('relation_result_too_long', field)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
421
RelationStringFieldWidgetInstance = RelationStringFieldWidget()
422
RelationStringFieldValidatorInstance = RelationStringFieldValidator()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
423 424 425

class RelationStringField(ZMIField):
    meta_type = "RelationStringField"
426
    is_relation_field = 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
427 428 429 430 431 432

    widget = RelationStringFieldWidgetInstance
    validator = RelationStringFieldValidatorInstance