############################################################################## # # Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved. # Jean-Paul Smets-Solanes <jp@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 Products.Formulator.Form import Form, BasicForm, ZMIForm from Products.Formulator.Form import manage_addForm, manage_add, initializeForm from Products.Formulator.Errors import FormValidationError, ValidationError from Products.Formulator.DummyField import fields from Products.Formulator.XMLToForm import XMLToForm from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate from Products.CMFCore.utils import _checkPermission, getToolByName from Products.CMFCore.exceptions import AccessControl_Unauthorized from Products.ERP5Type import PropertySheet, Permissions from Products.ERP5Type.Cache import CachingMethod from urllib import quote from Globals import InitializeClass, PersistentMapping, DTMLFile, get_request from AccessControl import Unauthorized, getSecurityManager, ClassSecurityInfo from ZODB.POSException import ConflictError from Products.PageTemplates.Expressions import SecureModuleImporter from Products.ERP5Type.Utils import UpperCase from Products.ERP5Type.PsycoWrapper import psyco import sys _field_value_cache = {} def purgeFieldValueCache(): _field_value_cache.clear() # Patch the fiels methods to provide improved namespace handling from Products.Formulator.Field import Field from Products.Formulator.TALESField import TALESMethod from zLOG import LOG, PROBLEM class StaticValue: """ Encapsulated a static value in a class (quite heavy, would be faster to store the value as is) """ def __init__(self, value): self.value = value def __call__(self, field, id, **kw): return self.returnValue(field, id, self.value) def returnValue(self, field, id, value): # if normal value is a callable itself, wrap it if callable(value): value = value.__of__(field) #value=value() # Mising call ??? XXX Make sure compatible with listbox methods if id == 'default': # We make sure we convert values to empty strings # for most fields (so that we do not get a 'value' # message on screen) # This can be overriden by using TALES in the field if value is None: value = '' return value class TALESValue(StaticValue): def __init__(self, tales_expr): self.tales_expr = tales_expr def __call__(self, field, id, **kw): REQUEST = get_request() if REQUEST is not None: # Proxyfield stores the "real" field in the request. Look if the # corresponding field exists in request, and use it as field in the # TALES context field = REQUEST.get('field__proxyfield_%s_%s' % (field.id, id), field) kw['field'] = field form = field.aq_parent # XXX (JPS) form for default is wrong apparently in listbox - double check obj = getattr(form, 'aq_parent', None) if obj is not None: container = obj.aq_inner.aq_parent else: container = None kw['form'] = form kw['request'] = REQUEST kw['here'] = obj kw['context'] = obj kw['modules'] = SecureModuleImporter kw['container'] = container try : kw['preferences'] = obj.getPortalObject().portal_preferences except AttributeError : LOG('ERP5Form', PROBLEM, 'portal_preferences not put in TALES context (not installed?)') # This allows to pass some pointer to the local object # through the REQUEST parameter. Not very clean. # Used by ListBox to render different items in a list if kw.has_key('REQUEST') and kw.get('cell', None) is None: if getattr(kw['REQUEST'],'cell', None) is not None: kw['cell'] = getattr(kw['REQUEST'],'cell') else: kw['cell'] = kw['REQUEST'] elif kw.get('cell', None) is None: if getattr(REQUEST, 'cell', None) is not None: kw['cell'] = getattr(REQUEST, 'cell') try: value = self.tales_expr.__of__(field)(**kw) except (ConflictError, RuntimeError): raise except: # We add this safety exception to make sure we always get # something reasonable rather than generate plenty of errors LOG('ERP5Form', PROBLEM, 'Field.get_value ( %s/%s [%s]), exception on tales_expr: ' % ( form.getId(), field.getId(), id), error=sys.exc_info()) value = field.get_orig_value(id) return self.returnValue(field, id, value) class OverrideValue(StaticValue): def __init__(self, override): self.override = override def __call__(self, field, id, **kw): return self.returnValue(field, id, self.override.__of__(field)()) class DefaultValue(StaticValue): def __init__(self, field_id, value): self.key = field_id[3:] self.value = value def __call__(self, field, id, **kw): try: form = field.aq_parent ob = getattr(form, 'aq_parent', None) value = self.value if value not in (None, ''): # If a default value is defined on the field, it has precedence value = ob.getProperty(self.key, d=value) else: # else we should give a chance to the accessor to provide # a default value (including None) value = ob.getProperty(self.key) except (KeyError, AttributeError): value = None return self.returnValue(field, id, value) class EditableValue(StaticValue): def __call__(self, field, id, **kw): # By default, pages are editable and # fields are editable if they are set to editable mode # However, if the REQUEST defines editable_mode to 0 # then all fields become read only. # This is useful to render ERP5 content as in a web site (ECommerce) # editable_mode should be set for example by the page template # which defines the current layout if kw.has_key('REQUEST'): if not getattr(kw['REQUEST'], 'editable_mode', 1): self.value = 0 return self.value def getFieldValue(self, field, id, **kw): """ Return a callable expression """ # FIXME: backwards compat hack to make sure tales dict exists if not hasattr(self, 'tales'): self.tales = {} tales_expr = self.tales.get(id, "") if tales_expr: # TALESMethod is persistent object, so that we cannot cache original one. # Becase if connection which original talesmethod uses is closed, # RuntimeError must occurs in __setstate__. clone = TALESMethod(tales_expr._text) return TALESValue(clone) # FIXME: backwards compat hack to make sure overrides dict exists if not hasattr(self, 'overrides'): self.overrides = {} override = self.overrides.get(id, "") if override: return OverrideValue(override) # Get a normal value. value = self.get_orig_value(id) field_id = field.id if id == 'default' and field_id.startswith('my_'): return DefaultValue(field_id, value) # For the 'editable' value, we try to get a default value if id == 'editable': return EditableValue(value) # Return default value in non callable mode if callable(value): return StaticValue(value) # Return default value in non callable mode return StaticValue(value)(field, id, **kw) def get_value(self, id, **kw): REQUEST = get_request() if REQUEST is not None: field = REQUEST.get('field__proxyfield_%s_%s' % (self.id, id), self) else: field = self cache_id = ('Form.get_value', self._p_oid or repr(self), field._p_oid or repr(field), id) try: value = _field_value_cache[cache_id] except KeyError: # either returns non callable value (ex. "Title") # or a FieldValue instance of appropriate class value = _field_value_cache[cache_id] = getFieldValue(self, field, id, **kw) if callable(value): return value(field, id, **kw) return value psyco.bind(get_value) def om_icons(self): """Return a list of icon URLs to be displayed by an ObjectManager""" icons = ({'path': self.icon, 'alt': self.meta_type, 'title': self.meta_type},) return icons def _get_default(self, key, value, REQUEST): if value is not None: return value try: value = self._get_user_input_value(key, REQUEST) except (KeyError, AttributeError): # fall back on default return self.get_value('default', REQUEST=REQUEST) # It was missing on Formulator # if we enter a string value while the field expects unicode, # convert to unicode first # this solves a problem when re-rendering a sticky form with # values from request if (self.has_value('unicode') and self.get_value('unicode') and type(value) == type('')): return unicode(value, self.get_form_encoding()) else: return value # Dynamic Patch Field.get_value = get_value Field._get_default = _get_default Field.om_icons = om_icons # Constructors manage_addForm = DTMLFile("dtml/form_add", globals()) def addERP5Form(self, id, title="", REQUEST=None): """Add form to folder. id -- the id of the new form to add title -- the title of the form to add Result -- empty string """ # add actual object id = self._setObject(id, ERP5Form(id, title)) # respond to the add_and_edit button if necessary add_and_edit(self, id, REQUEST) return '' def add_and_edit(self, id, REQUEST): """Helper method to point to the object's management screen if 'Add and Edit' button is pressed. id -- id of the object we just added """ if REQUEST is None: return try: u = self.DestinationURL() except AttributeError: u = REQUEST['URL1'] if REQUEST['submit'] == " Add and Edit ": u = "%s/%s" % (u, quote(id)) REQUEST.RESPONSE.redirect(u+'/manage_main') def initializeForm(field_registry, form_class=None): """Sets up ZMIForm with fields from field_registry. """ if form_class is None: form_class = ERP5Form meta_types = [] for meta_type, field in field_registry.get_field_classes().items(): # don't set up in form if this is a field for internal use only if field.internal_field: continue # set up individual add dictionaries for meta_types dict = { 'name': field.meta_type, 'action': 'manage_addProduct/Formulator/manage_add%sForm' % meta_type } meta_types.append(dict) # set up add method setattr(form_class, 'manage_add%sForm' % meta_type, DTMLFile('dtml/fieldAdd', globals(), fieldname=meta_type)) # set up meta_types that can be added to form form_class._meta_types = tuple(meta_types) # set up settings form form_class.settings_form._realize_fields() # Special Settings def create_settings_form(): """Create settings form for ZMIForm. """ form = BasicForm('manage_settings') title = fields.StringField('title', title="Title", required=0, default="") description = fields.TextAreaField('description', title="Description", required=0, default="") row_length = fields.IntegerField('row_length', title='Number of groups in row (in order tab)', required=1, default=4) name = fields.StringField('name', title="Form name", required=0, default="") pt = fields.StringField('pt', title="Page Template", required=0, default="") action = fields.StringField('action', title='Form action', required=0, default="") update_action = fields.StringField('update_action', title='Form update action', required=0, default="") method = fields.ListField('method', title='Form method', items=[('POST', 'POST'), ('GET', 'GET')], required=1, size=1, default='POST') enctype = fields.ListField('enctype', title='Form enctype', items=[('No enctype', ""), ('application/x-www-form-urlencoded', 'application/x-www-form-urlencoded'), ('multipart/form-data', 'multipart/form-data')], required=0, size=1, default=None) encoding = fields.StringField('encoding', title='Encoding of pages the form is in', default="UTF-8", required=1) stored_encoding = fields.StringField('stored_encoding', title='Encoding of form properties', default='UTF-8', required=1) unicode_mode = fields.CheckBoxField('unicode_mode', title='Form properties are unicode', default=0, required=1) form.add_fields([title, description, row_length, name, pt, action, update_action, method, enctype, encoding, stored_encoding, unicode_mode]) return form class ERP5Form(ZMIForm, ZopePageTemplate): """ A Formulator form with a built-in rendering parameter based on page templates or DTML. """ meta_type = "ERP5 Form" icon = "www/Form.png" # Declarative Security security = ClassSecurityInfo() # Tabs in ZMI manage_options = (ZMIForm.manage_options[:5] + ({'label':'Proxify', 'action':'formProxify'},)+ ZMIForm.manage_options[5:]) # Declarative properties property_sheets = ( PropertySheet.Base , PropertySheet.SimpleItem) # Constructors constructors = (manage_addForm, addERP5Form) # This is a patched dtml formOrder security.declareProtected('View management screens', 'formOrder') formOrder = DTMLFile('dtml/formOrder', globals()) # Proxify form security.declareProtected('View management screens', 'formProxify') formProxify = DTMLFile('dtml/formProxify', globals()) # Default Attributes pt = 'form_view' update_action = '' # Special Settings settings_form = create_settings_form() def __init__(self, id, title, unicode_mode=0, encoding='UTF-8', stored_encoding='UTF-8'): """Initialize form. id -- id of form title -- the title of the form """ ZMIForm.inheritedAttribute('__init__')(self, "", "POST", "", id, encoding, stored_encoding, unicode_mode) self.id = id self.title = title self.row_length = 4 self.group_list = ["left", "right", "center", "bottom", "hidden"] groups = {} for group in self.group_list: groups[group] = [] self.groups = groups # Proxy method to PageTemplate def __call__(self, *args, **kwargs): # Security # # The minimal action consists in checking that # we have View permission on the current object # before rendering a form. Otherwise, object with # AccessContentInformation can be viewed by invoking # a form directly. # # What would be better is to prevent calling certain # forms to render objects. This can not be done # through actions since we are using sometimes forms # to render the results of a report dialog form. # An a appropriate solutions could consist in adding # a permission field to the form. Another solutions # is the use of REFERER in the rendering process. # # Both solutions are not perfect if the goal is, for # example, to prevent displaying private information of # staff. The only real solution is to use a special # permission (ex. AccessPrivateInformation) for those # properties which are sensitive. if not kwargs.has_key('args'): kwargs['args'] = args form = self obj = getattr(form, 'aq_parent', None) if obj is not None: container = obj.aq_inner.aq_parent if not _checkPermission(Permissions.View, obj): raise AccessControl_Unauthorized('This document is not authorized for view.') else: container = None pt = getattr(self,self.pt) extra_context = dict( container=container, template=self, form=self, options=kwargs, here=obj ) return pt.pt_render(extra_context=extra_context) def _exec(self, bound_names, args, kw): pt = getattr(self,self.pt) return pt._exec(self, bound_names, args, kw) # Utilities def ErrorFields(self, validation_errors): """ Create a dictionnary of validation_errors with field id as key """ ef = {} for e in validation_errors.errors: ef[e.field_id] = e return ef def om_icons(self): """Return a list of icon URLs to be displayed by an ObjectManager""" icons = ({'path': 'misc_/ERP5Form/Form.png', 'alt': self.meta_type, 'title': self.meta_type},) return icons # Pached validate_all to support ListBox validation security.declareProtected('View', 'validate_all') def validate_all(self, REQUEST): """Validate all enabled fields in this form, catch any ValidationErrors if they occur and raise a FormValidationError in the end if any Validation Errors occured. """ result = {} errors = [] for group in self.get_groups(): if group.lower() == 'hidden': continue for field in self.get_fields_in_group(group): # skip any field we don't need to validate if not field.need_validate(REQUEST): continue if not (field.get_value('editable',REQUEST=REQUEST)): continue try: value = field.validate(REQUEST) # store under id result[field.id] = value # store as alternate name as well if necessary alternate_name = field.get_value('alternate_name') if alternate_name: result[alternate_name] = value except FormValidationError, e: # XXX JPS Patch for listbox #LOG('validate_all', 0, 'FormValidationError: field = %s, errors=%s' % (repr(field), repr(errors))) errors.extend(e.errors) result.update(e.result) except ValidationError, err: #LOG('validate_all', 0, 'ValidationError: field.id = %s, err=%s' % (repr(field.id), repr(err))) errors.append(err) except KeyError, err: LOG('ERP5Form/Form.py:validate_all', 0, 'KeyError : %s' % (err, )) if len(errors) > 0: raise FormValidationError(errors, result) return result # FTP/DAV Access manage_FTPget = ZMIForm.get_xml def PUT(self, REQUEST, RESPONSE): """Handle HTTP PUT requests.""" self.dav__init(REQUEST, RESPONSE) self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1) body=REQUEST.get('BODY', '') # Empty the form (XMLToForm is unable to empty things before reopening) for k in self.get_field_ids(): try: self._delObject(k) except AttributeError: pass self.groups = {} self.group_list = [] # And reimport XMLToForm(body, self) self.ZCacheable_invalidate() RESPONSE.setStatus(204) return RESPONSE manage_FTPput = PUT #Methods for Proxify tab. security.declareProtected('View management screens', 'getFormFieldList') def getFormFieldList(self): """ find fields and forms which name ends with 'FieldLibrary' in same skin folder. """ form_list = [] def iterate(obj): for i in obj.objectValues(): if (i.meta_type=='ERP5 Form' and i.getId().endswith('FieldLibrary')): form_id = i.getId() form_path = '%s.%s' % (obj.getId(), form_id) field_list = [] form_list.append({'form_path':form_path, 'form_id':form_id, 'field_list':field_list}) for field in i.objectValues(): field_type, proxy_flag = get_field_meta_type_and_proxy_flag(field) if proxy_flag: field_type = '%s(Proxy)' % field_type field_list.append({'field_object':field, 'field_type':field_type, 'proxy_flag':proxy_flag}) if i.meta_type=='Folder': iterate(i) iterate(getToolByName(self, 'portal_skins')) return form_list security.declareProtected('View management screens', 'getProxyableFieldList') def getProxyableFieldList(self, field, form_field_list=None): """""" def extract_keyword(name): return [i for i in name.split('_') if not i in ('my', 'default')] def check_keyword_list(name, keyword_list): count = 0 for i in keyword_list: if i in name: count += 1 return count/float(len(keyword_list)) def match(field_data): if not field_data['field_type'].startswith(field.meta_type): return 0 field_object = field_data['field_object'] if field_object.aq_base is field.aq_base: return 0 field_id = field_object.getId() if id_.startswith('my_') and not field_id.startswith('my_'): return 0 return check_keyword_list(field_id, extract_keyword(id_)) def make_dict_list_append_function(dic, order_list): def append(key, item): if not key in order_list: order_list.append(key) dic[key] = [] dic[key].append(item) return append def add_default_field_library(): portal_url = getToolByName(self, 'portal_url') portal = portal_url.getPortalObject() portal_skins = getToolByName(self, 'portal_skins') default_field_library_path = portal.getProperty('erp5_default_field_library_path', None) if (not default_field_library_path or len(default_field_library_path.split('.'))!=2): return skinfolder_id, form_id = default_field_library_path.split('.') skinfolder = getattr(portal_skins, skinfolder_id, None) default_field_library = getattr(skinfolder, form_id, None) if default_field_library is None: return if not default_field_library_path in form_order: for i in default_field_library.objectValues(): field_meta_type, proxy_flag = get_field_meta_type_and_proxy_flag(i) if meta_type==field_meta_type: if proxy_flag: field_meta_type = '%s(Proxy' % field_meta_type matched_item = {'form_id':form_id, 'field_type':field_meta_type, 'field_object':i, 'proxy_flag':proxy_flag, 'matched_rate':0 } matched_append(default_field_library_path, matched_item) id_ = field.getId() meta_type = field.meta_type matched = {} form_order = [] matched_append = make_dict_list_append_function(matched, form_order) perfect_matched = {} perfect_matched_form_order = [] perfect_matched_append = make_dict_list_append_function(perfect_matched, perfect_matched_form_order) if form_field_list is None: form_field_list = self.getFormFieldList() for i in form_field_list: for data in i['field_list']: tmp = [] matched_rate = match(data) if matched_rate>=0.5: form_path = i['form_path'] form_id = i['form_id'] field_type = data['field_type'] field_object = data['field_object'] proxy_flag = data['proxy_flag'] matched_item = {'form_id':form_id, 'field_type':field_type, 'field_object':field_object, 'proxy_flag':proxy_flag, 'matched_rate':matched_rate } if matched_rate==1: perfect_matched_append(form_path, matched_item) elif not perfect_matched: matched_append(form_path, matched_item) if perfect_matched: perfect_matched_form_order.sort() return perfect_matched_form_order, perfect_matched form_order.sort() add_default_field_library() return form_order, matched security.declareProtected('Change Formulator Forms', 'proxifyField') def proxifyField(self, field_dict=None, REQUEST=None): """Convert fields to proxy fields""" from Products.ERP5Form.ProxyField import ProxyWidget from Products.Formulator.MethodField import Method from Products.Formulator.TALESField import TALESMethod def copy(_dict): new_dict = {} for key, value in _dict.items(): if value=='': continue if type(value) is Method: value = Method(value.method_name) elif type(value) is TALESMethod: value = TALESMethod(value._text) elif value is not None and not isinstance(value, (str, unicode, int, long, bool, list, tuple, dict)): raise ValueError, repr(value) new_dict[key] = value return new_dict def is_equal(a, b): type_a = type(a) type_b = type(b) if type_a is not type_b: return False elif type_a is Method: return a.method_name==b.method_name elif type_a is TALESMethod: return a._text==b._text else: return a==b def remove_same_value(new_dict, target_dict): for key, value in new_dict.items(): target_value = target_dict.get(key) if is_equal(value, target_value): del new_dict[key] return new_dict def get_group_and_position(field_id): for i in self.groups.keys(): if field_id in self.groups[i]: return i, self.groups[i].index(field_id) def set_group_and_position(group, position, field_id): self.field_removed(field_id) self.groups[group].insert(position, field_id) # Notify changes explicitly. self.groups = self.groups if field_dict is None: return for field_id in field_dict.keys(): target = field_dict[field_id] target_form_id, target_field_id = target.split('.') # keep current group and position. group, position = get_group_and_position(field_id) # create proxy field old_field = getattr(self, field_id) self.manage_delObjects(field_id) self.manage_addField(id=field_id, title='', fieldname='ProxyField') proxy_field = getattr(self, field_id) proxy_field.values['form_id'] = target_form_id proxy_field.values['field_id'] = target_field_id target_field = proxy_field.getTemplateField() # copy data new_values = remove_same_value(copy(old_field.values), target_field.values) new_tales = remove_same_value(copy(old_field.tales), target_field.tales) if target_field.meta_type=='ProxyField': for i in new_values.keys(): if not i in target_field.delegated_list: # obsolete variable check try: target_field.get_recursive_orig_value(i) except KeyError: # then `i` is obsolete! del new_values[i] else: if is_equal(target_field.get_recursive_orig_value(i), new_values[i]): del new_values[i] for i in new_tales.keys(): if not i in target_field.delegated_list: # obsolete variable check try: target_field.get_recursive_tales(i) except KeyError: # then `i` is obsolete! del new_tales[i] else: if is_equal(target_field.get_recursive_tales(i), new_tales[i]): del new_tales[i] delegated_list = [] for i in (new_values.keys()+new_tales.keys()): if not i in delegated_list: delegated_list.append(i) proxy_field.values.update(new_values) proxy_field.tales.update(new_tales) proxy_field.delegated_list = delegated_list # move back to the original group and position. set_group_and_position(group, position, field_id) if REQUEST is not None: return self.formProxify(manage_tabs_message='Changed') psyco.bind(__call__) psyco.bind(_exec) # Overload of the Form method # Use the include_disabled parameter since # we should consider all fields to render the group tab # moreoever, listbox rendering fails whenever enabled # is based on the cell parameter. security.declareProtected('View', 'get_largest_group_length') def get_largest_group_length(self): """Get the largest group length available; necessary for 'order' screen user interface. XXX - Copyright issue """ max = 0 for group in self.get_groups(include_empty=1): fields = self.get_fields_in_group(group, include_disabled=1) if len(fields) > max: max = len(fields) return max security.declareProtected('View', 'get_groups') def get_groups(self, include_empty=0): """Get a list of all groups, in display order. If include_empty is false, suppress groups that do not have enabled fields. XXX - Copyright issue """ if include_empty: return self.group_list return [group for group in self.group_list if self.get_fields_in_group(group, include_disabled=1)] # utility function def get_field_meta_type_and_proxy_flag(field): if field.meta_type=='ProxyField': try: return field.getRecursiveTemplateField().meta_type, True except AttributeError: raise AttributeError, 'The proxy target of %s field does not exists. Please check the field setting.' % field.getId() else: return field.meta_type, False # More optimizations #psyco.bind(ERP5Field) # XXX Not useful, as we patch those methods in FormulatorPatch psyco.bind(Field.render) psyco.bind(Field._render_helper) psyco.bind(Field.get_value) #from Products.PageTemplates.PageTemplate import PageTemplate #from TAL import TALInterpreter #psyco.bind(TALInterpreter.TALInterpreter) #psyco.bind(TALInterpreter.TALInterpreter.interpret) #psyco.bind(PageTemplate.pt_render) #psyco.bind(PageTemplate.pt_macros) #from Products.CMFCore.ActionsTool import ActionsTool #psyco.bind(ActionsTool.listFilteredActionsFor) # install interactor from Products.ERP5Type.Interactor import fielf_value_interactor fielf_value_interactor.install()