############################################################################## # # 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 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 # Patch the fiels methods to provide improved namespace handling from Products.Formulator.Field import Field from zLOG import LOG, PROBLEM def get_value(self, id, **kw): """Get value for id.""" # 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: REQUEST = get_request() form = self.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 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' % (self.id, id), self) kw['field'] = field else: kw['field'] = self 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 = tales_expr.__of__(self)(**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(), self.getId(), id), error=sys.exc_info()) value = self.get_orig_value(id) else: # FIXME: backwards compat hack to make sure overrides dict exists if not hasattr(self, 'overrides'): self.overrides = {} override = self.overrides.get(id, "") if override: # call wrapped method to get answer value = override.__of__(self)() else: # Get a normal value. value = self.get_orig_value(id) # For the 'default' value, we try to get a property value # stored in the context, only if the field is prefixed with my_. REQUEST = get_request() if REQUEST is not None: field_id = REQUEST.get('field__proxyfield_%s_%s' % (self.id, id), self).id else: field_id = self.id if id == 'default' and field_id.startswith('my_'): try: form = self.aq_parent ob = getattr(form, 'aq_parent', None) key = field_id[3:] if value not in (None, ''): # If a default value is defined on the field, it has precedence value = ob.getProperty(key, d=value) else: # else we should give a chance to the accessor to provide # a default value (including None) value = ob.getProperty(key) except (KeyError, AttributeError): value = None # For the 'editable' value, we try to get a default value elif id == 'editable': # 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): value = 0 # if normal value is a callable itself, wrap it if callable(value): value = value.__of__(self) #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 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="") 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, 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', 'getAllFormFieldList') def getAllFormFieldList(self): """""" form_list = [] def iterate(obj): for i in obj.objectValues(): if i.meta_type=='ERP5 Form': 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(): if field.meta_type=='ProxyField': template_field = field.getRecursiveTemplateField() template_meta_type = getattr(template_field, 'meta_type', None) field_type = '%s(Proxy)' % template_meta_type proxy_flag = True else: field_type = field.meta_type proxy_flag = False 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): """""" meta_type = field.meta_type matched = {} form_order = [] if form_field_list is None: form_field_list = self.getAllFormFieldList() for i in form_field_list: for data in i['field_list']: if data['field_type'].startswith(meta_type): form_path = i['form_path'] form_id = i['form_id'] field_type = data['field_type'] field_object = data['field_object'] if field.aq_base is field_object.aq_base: continue proxy_flag = data['proxy_flag'] if not form_path in form_order: form_order.append(form_path) matched[form_path] = [] matched[form_path].append({'form_id':form_id, 'field_type':field_type, 'field_object':field_object, 'proxy_flag':proxy_flag}) form_order.sort() return form_order, matched security.declareProtected('Change Formulator Forms', 'proxifyField') def proxifyField(self, field_dict=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 not isinstance(value, (str, unicode, int, long, bool, list, tuple, dict)): raise ValueError, str(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.getRecursiveTemplateField() # 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) 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) return self.formProxify(manage_tabs_message='Changed') psyco.bind(__call__) psyco.bind(_exec) # 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)