'''This package contains mixin classes that are mixed in with generated classes: - mixins/BaseMixin is mixed in with standard Zope classes; - mixins/ToolMixin is mixed in with the generated application Tool class.''' # ------------------------------------------------------------------------------ import os, os.path, sys, types, urllib, cgi from appy import Object import appy.gen as gen from appy.gen.utils import * from appy.gen.layout import Table, defaultPageLayouts, ColumnLayout from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor from appy.shared.utils import sequenceTypes,normalizeText,Traceback,getMimeType from appy.shared.data import rtlLanguages from appy.shared.xml_parser import XmlMarshaller from appy.shared.diff import HtmlDiff # ------------------------------------------------------------------------------ class BaseMixin: '''Every Zope class generated by appy.gen inherits from this class or a subclass of it.''' _appy_meta_type = 'Class' def get_o(self): '''In some cases, we want the Zope object, we don't know if the current object is a Zope or Appy object. By defining this property, "someObject.o" produces always the Zope object, be someObject an Appy or Zope object.''' return self o = property(get_o) def getInitiatorInfo(self): '''Gets information about a potential initiator object from the request. Returns a 3-tuple (initiator, pageName, field): * initiator is the initiator (Zope) object; * pageName is the page on the initiator where the origin of the Ref field lies; * field is the Ref instance. ''' rq = self.REQUEST if not rq.get('nav', '').startswith('ref.'): return None, None, None splitted = rq['nav'].split('.') initiator = self.getTool().getObject(splitted[1]) fieldName, page = splitted[2].split(':') return initiator, page, initiator.getAppyType(fieldName) def createOrUpdate(self, created, values, initiator=None, initiatorField=None): '''This method creates (if p_created is True) or updates an object. p_values are manipulated versions of those from the HTTP request. In the case of an object creation from the web (p_created is True and a REQUEST object is present), p_self is a temporary object created in /temp_folder, and this method moves it at its "final" place. In the case of an update, this method simply updates fields of p_self.''' rq = getattr(self, 'REQUEST', None) obj = self if created and rq: # Create the final object and put it at the right place. tool = self.getTool() id = tool.generateUid(obj.portal_type) if not initiator: folder = tool.getPath('/data') else: folder = initiator.getCreateFolder() # Check that the user can add objects through this Ref. initiatorField.checkAdd(initiator) obj = createObject(folder, id, obj.portal_type, tool.getAppName()) # Get the fields on the current page fields = None if rq: fields = self.getAppyTypes('edit', rq.get('page')) # Remember the previous values of fields, for potential historization previousData = None if not created and fields: previousData = obj.rememberPreviousData(fields) # Perform the change on the object if fields: # Store in the database the new value coming from the form for field in fields: value = getattr(values, field.name, None) field.store(obj, value) if previousData: # Keep in history potential changes on historized fields obj.historizeData(previousData) # Manage potential link with an initiator object if created and initiator: initiator.appy().link(initiatorField.name,obj) # Manage "add" permissions and reindex the object obj._appy_managePermissions() # Call the custom "onEdit" if available msg = None # The message to display to the user. It can be set by onEdit if obj.wrapperClass: appyObject = obj.appy() if hasattr(appyObject, 'onEdit'): msg = appyObject.onEdit(created) # Update last modification date if not created: from DateTime import DateTime obj.modified = DateTime() # Unlock the currently saved page on the object if rq: self.removeLock(rq['page']) obj.reindex() return obj, msg def delete(self): '''This method is self's suicide.''' # Call a custom "onDelete" if it exists appyObj = self.appy() if hasattr(appyObj, 'onDelete'): appyObj.onDelete() # Any people referencing me must forget me now for field in self.getAllAppyTypes(): if field.type != 'Ref': continue for obj in field.getValue(self): field.back.unlinkObject(obj, self, back=True) # Uncatalog the object self.reindex(unindex=True) # Delete the object self.getParentNode().manage_delObjects([self.id]) def onDelete(self): '''Called when an object deletion is triggered from the ui.''' rq = self.REQUEST self.delete() if self.getUrl(rq['HTTP_REFERER'],mode='raw') ==self.getUrl(mode='raw'): # We were consulting the object that has been deleted. Go back to # the main page. urlBack = self.getTool().getSiteUrl() else: urlBack = self.getUrl(rq['HTTP_REFERER']) self.say(self.translate('delete_done')) self.goto(urlBack) def onDeleteEvent(self): '''Called when an event (from object history) deletion is triggered from the ui.''' rq = self.REQUEST # Re-create object history, but without the event corresponding to # rq['eventTime'] history = [] from DateTime import DateTime eventToDelete = DateTime(rq['eventTime']) key = self.workflow_history.keys()[0] for event in self.workflow_history[key]: if (event['action'] != '_datachange_') or \ (event['time'] != eventToDelete): history.append(event) self.workflow_history[key] = tuple(history) appy = self.appy() self.log('Data change event deleted by %s for %s (UID=%s).' % \ (appy.user.getId(), appy.klass.__name__, appy.uid)) self.goto(self.getUrl(rq['HTTP_REFERER'])) def onUnlink(self): '''Called when an object unlinking is triggered from the ui.''' rq = self.REQUEST tool = self.getTool() sourceObject = tool.getObject(rq['sourceUid']) targetObject = tool.getObject(rq['targetUid']) field = sourceObject.getAppyType(rq['fieldName']) field.unlinkObject(sourceObject, targetObject) urlBack = self.getUrl(rq['HTTP_REFERER']) self.say(self.translate('unlink_done')) self.goto(urlBack) def onCreate(self): '''This method is called when a user wants to create a root object in the "data" folder or an object through a reference field. A temporary object is created in /temp_folder and the edit page to it is returned.''' rq = self.REQUEST className = rq.get('className') # Create the params to add to the URL we will redirect the user to # create the object. urlParams = {'mode':'edit', 'page':'main', 'nav':''} initiator, initiatorPage, initiatorField = self.getInitiatorInfo() if initiator: # The object to create will be linked to an initiator object through # a Ref field. We create here a new navigation string with one more # item, that will be the currently created item. splitted = rq.get('nav').split('.') splitted[-1] = splitted[-2] = str(int(splitted[-1])+1) urlParams['nav'] = '.'.join(splitted) # Check that the user can add objects through this Ref field initiatorField.checkAdd(initiator) # Create a temp object in /temp_folder tool = self.getTool() id = tool.generateUid(className) appName = tool.getAppName() obj = createObject(tool.getPath('/temp_folder'), id, className, appName) return self.goto(obj.getUrl(**urlParams)) def setLock(self, user, page): '''A p_user edits a given p_page on this object: we will set a lock, to prevent other users to edit this page at the same time.''' if not hasattr(self.aq_base, 'locks'): # Create the persistent mapping that will store the lock # ~{s_page: (s_userId, DateTime_lockDate)}~ from persistent.mapping import PersistentMapping self.locks = PersistentMapping() # Raise an error is the page is already locked by someone else. If the # page is already locked by the same user, we don't mind: he could have # used back/forward buttons of its browser... userId = user.getId() if (page in self.locks) and (userId != self.locks[page][0]): from AccessControl import Unauthorized raise Unauthorized('This page is locked.') # Set the lock from DateTime import DateTime self.locks[page] = (userId, DateTime()) def isLocked(self, user, page): '''Is this page locked? If the page is locked by the same user, we don't mind and consider the page as unlocked. If the page is locked, this method returns the tuple (userId, lockDate).''' if hasattr(self.aq_base, 'locks') and (page in self.locks): if (user.getId() != self.locks[page][0]): return self.locks[page] def removeLock(self, page): '''Removes the lock on the current page. This happens after the page has been saved: the lock must be released.''' if page not in self.locks: return # Raise an error if the user that saves changes is not the one that # has locked the page. userId = self.getUser().getId() if self.locks[page][0] != userId: from AccessControl import Unauthorized raise Unauthorized('This page was locked by someone else.') # Remove the lock del self.locks[page] def removeMyLock(self, user, page): '''If p_user has set a lock on p_page, this method removes it. This method is called when the user that locked a page consults view.pt for this page. In this case, we consider that the user has left the edit page in an unexpected way and we remove the lock.''' if hasattr(self.aq_base, 'locks') and (page in self.locks) and \ (user.getId() == self.locks[page][0]): del self.locks[page] def onCreateWithoutForm(self): '''This method is called when a user wants to create a object from a reference field, automatically (without displaying a form).''' rq = self.REQUEST self.appy().create(rq['fieldName']) def intraFieldValidation(self, errors, values): '''This method performs field-specific validation for every field from the page that is being created or edited. For every field whose validation generates an error, we add an entry in p_errors. For every field, we add in p_values an entry with the "ready-to-store" field value.''' rq = self.REQUEST for appyType in self.getAppyTypes('edit', rq.form.get('page')): if not appyType.validable: continue value = appyType.getRequestValue(rq) message = appyType.validate(self, value) if message: setattr(errors, appyType.name, message) else: setattr(values, appyType.name, appyType.getStorableValue(value)) # Validate sub-fields within Lists if appyType.type != 'List': continue i = -1 for row in value: i += 1 for name, field in appyType.fields: message = field.validate(self, getattr(row,name,None)) if message: setattr(errors, '%s*%d' % (field.name, i), message) def interFieldValidation(self, errors, values): '''This method is called when individual validation of all fields succeed (when editing or creating an object). Then, this method performs inter-field validation. This way, the user must first correct individual fields before being confronted to potential inter-field validation errors.''' obj = self.appy() if not hasattr(obj, 'validate'): return msg = obj.validate(values, errors) # Those custom validation methods may have added fields in the given # p_errors object. Within this object, for every error message that is # not a string, we replace it with the standard validation error for the # corresponding field. for key, value in errors.__dict__.iteritems(): resValue = value if not isinstance(resValue, basestring): resValue = self.translate('field_invalid') setattr(errors, key, resValue) return msg def onUpdate(self): '''This method is executed when a user wants to update an object. The object may be a temporary object created in /temp_folder. In this case, the update consists in moving it to its "final" place. If the object is not a temporary one, this method updates its fields in the database.''' rq = self.REQUEST tool = self.getTool() errorMessage = self.translate('validation_error') isNew = self.isTemporary() # If this object is created from an initiator, get info about him. initiator, initiatorPage, initiatorField = self.getInitiatorInfo() # If the user clicked on 'Cancel', go back to the previous page. buttonClicked = rq.get('button') if buttonClicked == 'cancel': if initiator: # Go back to the initiator page. urlBack = initiator.getUrl(page=initiatorPage, nav='') else: if isNew: # Go back to the root of the site. urlBack = tool.getSiteUrl() else: urlBack = self.getUrl() self.say(self.translate('object_canceled')) self.removeLock(rq['page']) return self.goto(urlBack) # Object for storing validation errors errors = Object() # Object for storing the (converted) values from the request values = Object() # Trigger field-specific validation self.intraFieldValidation(errors, values) if errors.__dict__: rq.set('errors', errors.__dict__) self.say(errorMessage) return self.gotoEdit() # Trigger inter-field validation msg = self.interFieldValidation(errors, values) if not msg: msg = errorMessage if errors.__dict__: rq.set('errors', errors.__dict__) self.say(msg) return self.gotoEdit() # Before saving data, must we ask a confirmation by the user ? appyObj = self.appy() saveConfirmed = rq.get('confirmed') == 'True' if hasattr(appyObj, 'confirm') and not saveConfirmed: msg = appyObj.confirm(values) if msg: rq.set('confirmMsg', msg.replace("'", "\\'")) return self.gotoEdit() # Create or update the object in the database obj, msg = self.createOrUpdate(isNew, values, initiator, initiatorField) # Redirect the user to the appropriate page if not msg: msg = self.translate('object_saved') # If the object has already been deleted (ie, it is a kind of transient # object like a one-shot form and has already been deleted in method # onEdit), redirect to the main site page. if not getattr(obj.getParentNode().aq_base, obj.id, None): return self.goto(tool.getSiteUrl(), msg) # If the user can't access the object anymore, redirect him to the # main site page. if not obj.allows('View'): return self.goto(tool.getSiteUrl(), msg) if (buttonClicked == 'save') or saveConfirmed: obj.say(msg) if isNew and initiator: return self.goto(initiator.getUrl(page=initiatorPage, nav='')) else: return self.goto(obj.getUrl()) if buttonClicked == 'previous': # Go to the previous page for this object. # We recompute the list of phases and pages because things # may have changed since the object has been updated (ie, # additional pages may be shown or hidden now, so the next and # previous pages may have changed). Moreover, previous and next # pages may not be available in "edit" mode, so we return the edit # or view pages depending on page.show. phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit') pageName, pageInfo = self.getPreviousPage(phaseInfo, rq['page']) if pageName: # Return to the edit or view page? if pageInfo['showOnEdit']: rq.set('page', pageName) # I do not use gotoEdit here because I really need to # redirect the user to the edit page. Indeed, the object # edit URL may have moved from temp_folder to another place. return self.goto(obj.getUrl(mode='edit', page=pageName)) else: return self.goto(obj.getUrl(page=pageName)) else: obj.say(msg) return self.goto(obj.getUrl()) if buttonClicked == 'next': # Go to the next page for this object. # We remember page name, because the next method may set a new # current page if the current one is not visible anymore. pageName = rq['page'] phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit') pageName, pageInfo = self.getNextPage(phaseInfo, pageName) if pageName: # Return to the edit or view page? if pageInfo['showOnEdit']: # Same remark as above (click on "previous"). return self.goto(obj.getUrl(mode='edit', page=pageName)) else: return self.goto(obj.getUrl(page=pageName)) else: obj.say(msg) return self.goto(obj.getUrl()) return obj.gotoEdit() def reindex(self, indexes=None, unindex=False): '''Reindexes this object the catalog. If names of indexes are specified in p_indexes, recataloging is limited to those indexes. If p_unindex is True, instead of cataloguing the object, it uncatalogs it.''' path = '/'.join(self.getPhysicalPath()) catalog = self.getPhysicalRoot().catalog if unindex: catalog.uncatalog_object(path) else: if indexes: catalog.catalog_object(self, path, idxs=indexes) else: # Get the list of indexes that apply on this object. Else, Zope # will reindex all indexes defined in the catalog, and through # acquisition, wrong methods can be called on wrong objects. iNames = self.wrapperClass.getIndexes().keys() catalog.catalog_object(self, path, idxs=iNames) def xml(self, action=None): '''If no p_action is defined, this method returns the XML version of this object. Else, it calls method named p_action on the corresponding Appy wrapper and returns, as XML, the its result.''' self.REQUEST.RESPONSE.setHeader('Content-Type','text/xml;charset=utf-8') # Check if the user is allowed to consult this object if not self.allows('View'): return XmlMarshaller().marshall('Unauthorized') if not action: marshaller = XmlMarshaller(rootTag=self.getClass().__name__, dumpUnicode=True) res = marshaller.marshall(self, objectType='appy') else: appyObj = self.appy() try: methodRes = getattr(appyObj, action)() res = XmlMarshaller().marshall(methodRes, objectType='appy') except Exception, e: tb = Traceback.get() res = XmlMarshaller().marshall(tb, objectType='appy') return res def say(self, msg, type='info'): '''Prints a p_msg in the user interface. p_logLevel may be "info", "warning" or "error".''' rq = self.REQUEST if 'messages' not in rq.SESSION.keys(): plist = self.getProductConfig().PersistentList messages = rq.SESSION['messages'] = plist() else: messages = rq.SESSION['messages'] messages.append( (type, msg) ) def log(self, msg, type='info'): '''Logs a p_msg in the log file. p_logLevel may be "info", "warning" or "error".''' logger = self.getProductConfig().logger if type == 'warning': logMethod = logger.warn elif type == 'error': logMethod = logger.error else: logMethod = logger.info logMethod(msg) def do(self): '''Performs some action from the user interface.''' rq = self.REQUEST action = rq['action'] if rq.get('objectUid', None): obj = self.getTool().getObject(rq['objectUid']) else: obj = self if rq.get('appy', None) == '1': obj = obj.appy() return getattr(obj, 'on'+action)() def rememberPreviousData(self, fields): '''This method is called before updating an object and remembers, for every historized field, the previous value. Result is a dict ~{s_fieldName: previousFieldValue}~''' res = {} for field in fields: if not field.historized: continue # appyType.historized can be a method or a boolean. if callable(field.historized): historized = field.callMethod(self, field.historized) else: historized = field.historized if historized: res[field.name] = field.getValue(self) return res def addHistoryEvent(self, action, **kw): '''Adds an event in the object history.''' userId = self.getUser().getId() from DateTime import DateTime event = {'action': action, 'actor': userId, 'time': DateTime(), 'comments': ''} event.update(kw) if 'review_state' not in event: event['review_state'] = self.State() # Add the event to the history histKey = self.workflow_history.keys()[0] self.workflow_history[histKey] += (event,) def addDataChange(self, changes, notForPreviouslyEmptyValues=False): '''This method allows to add "manually" a data change into the objet's history. Indeed, data changes are "automatically" recorded only when a HTTP form is uploaded, not if, in the code, a setter is called on a field. The method is also called by m_historizeData below, that performs "automatic" recording when a HTTP form is uploaded. Field changes for which the previous value was empty are not recorded into the history if p_notForPreviouslyEmptyValues is True.''' # Add to the p_changes dict the field labels for fieldName in changes.keys(): appyType = self.getAppyType(fieldName) if notForPreviouslyEmptyValues and \ appyType.isEmptyValue(changes[fieldName], self): del changes[fieldName] else: changes[fieldName] = (changes[fieldName], appyType.labelId) # Add an event in the history self.addHistoryEvent('_datachange_', changes=changes) def historizeData(self, previousData): '''Records in the object history potential changes on historized fields. p_previousData contains the values, before an update, of the historized fields, while p_self already contains the (potentially) modified values.''' # Remove from previousData all values that were not changed for field in previousData.keys(): prev = previousData[field] appyType = self.getAppyType(field) curr = appyType.getValue(self) try: if (prev == curr) or ((prev == None) and (curr == '')) or \ ((prev == '') and (curr == None)): del previousData[field] except UnicodeDecodeError, ude: # The string comparisons above may imply silent encoding-related # conversions that may produce this exception. pass if (appyType.type == 'Ref') and (field in previousData): previousData[field] = [r.title for r in previousData[field]] if previousData: self.addDataChange(previousData) def goto(self, url, msg=None): '''Brings the user to some p_url after an action has been executed.''' if msg: self.say(msg) return self.REQUEST.RESPONSE.redirect(url) def gotoEdit(self): '''Brings the user to the edit page for this object. This method takes care of not carrying any password value. Unlike m_goto above, there is no HTTP redirect here: we execute directly macro "edit" and we return the result.''' page = self.REQUEST.get('page', 'main') for field in self.getAppyTypes('edit', page): if (field.type == 'String') and (field.format in (3,4)): self.REQUEST.set(field.name, '') return self.ui.edit(self) def showField(self, name, layoutType='view'): '''Must I show field named p_name on this p_layoutType ?''' return self.getAppyType(name).isShowable(self, layoutType) def getMethod(self, methodName): '''Returns the method named p_methodName.''' # If I write "self.aq_base" instead of self, acquisition will be # broken on returned object. return getattr(self, methodName, None) def getCreateFolder(self): '''When an object must be created from this one through a Ref field, we must know where to put the newly create object: within this one if it is folderish, besides this one in its parent else. ''' if self.isPrincipiaFolderish: return self return self.getParentNode() def getFieldValue(self, name, onlyIfSync=False, layoutType=None, outerValue=None): '''Returns the database value of field named p_name for p_self. If p_onlyIfSync is True, it returns the value only if appyType can be retrieved in synchronous mode.''' appyType = self.getAppyType(name) if not onlyIfSync or (onlyIfSync and appyType.sync[layoutType]): # We must really get the field value. if '*' not in name: return appyType.getValue(self) # The field is an inner field from a List. listName, name, i = name.split('*') listType = self.getAppyType(listName) return listType.getInnerValue(outerValue, name, int(i)) def getFormattedFieldValue(self, name, value, showChanges=False): '''Gets a nice, string representation of p_value which is a value from field named p_name.''' return self.getAppyType(name).getFormattedValue(self,value,showChanges) def getRequestFieldValue(self, name): '''Gets the value of field p_name as may be present in the request.''' # Return the request value for standard fields. if '*' not in name: return self.getAppyType(name).getRequestValue(self.REQUEST) # For sub-fields within Lists, the corresponding request values have # already been computed in the request key corresponding to the whole # List. listName, name, rowIndex = name.split('*') rowIndex = int(rowIndex) if rowIndex == -1: return '' allValues = self.REQUEST.get(listName) if not allValues: return '' return getattr(allValues[rowIndex], name, '') def getFileInfo(self, fileObject): '''Returns filename and size of p_fileObject.''' if not fileObject: return {'filename': '', 'size': 0} return {'filename': fileObject.filename, 'size': fileObject.size} def getAppyRefs(self, name, startNumber=None): '''Gets the objects linked to me through Ref field named p_name. If p_startNumber is None, this method returns all referred objects. If p_startNumber is a number, this method will return appyType.maxPerPage objects, starting at p_startNumber.''' field = self.getAppyType(name) return field.getValue(self, type='zobjects', someObjects=True, startNumber=startNumber).__dict__ def getSelectableAppyRefs(self, name): '''p_name is the name of a Ref field. This method returns the list of all objects that can be selected to be linked as references to p_self through field p_name.''' appyType = self.getAppyType(name) if not appyType.select: # No select method has been defined: we must retrieve all objects # of the referred type that the user is allowed to access. return self.appy().search(appyType.klass) else: return appyType.select(self.appy()) xhtmlToText = re.compile('<.*?>', re.S) def getReferenceLabel(self, name, refObject, className=None): '''p_name is the name of a Ref field with link=True. I need to display, on an edit view, the p_refObject in the listbox that will allow the user to choose which object(s) to link through the Ref. The information to display may only be the object title or more if field.shownInfo is used.''' appyType = self.getAppyType(name, className=className) res = '' for fieldName in appyType.shownInfo: refType = refObject.o.getAppyType(fieldName) value = getattr(refObject, fieldName) value = refType.getFormattedValue(refObject.o, value) if refType.type == 'String': if refType.format == 2: value = self.xhtmlToText.sub(' ', value) elif type(value) in sequenceTypes: value = ', '.join(value) prefix = '' if res: prefix = ' | ' res += prefix + value maxWidth = appyType.width or 30 if len(res) > maxWidth: res = res[:maxWidth-2] + '...' return res def getReferenceUid(self, refObject): '''Returns the UID of referred object p_refObject.''' return refObject.o.UID() def getAppyRefIndex(self, fieldName, obj): '''Gets the position of p_obj within Ref field named p_fieldName.''' refs = getattr(self.aq_base, fieldName, None) if not refs: raise IndexError() return refs.index(obj.UID()) def mayAddReference(self, name): '''May the user add references via Ref field named p_name in p_folder?''' return self.getAppyType(name).mayAdd(self) def isDebug(self): '''Are we in debug mode ?''' for arg in sys.argv: if arg == 'debug-mode=on': return True def getClass(self, reloaded=False): '''Returns the Appy class that dictates self's behaviour.''' if not reloaded: return self.getTool().getAppyClass(self.__class__.__name__) else: klass = self.appy().klass moduleName = klass.__module__ exec 'import %s' % moduleName exec 'reload(%s)' % moduleName exec 'res = %s.%s' % (moduleName, klass.__name__) # More manipulations may have occurred in m_update if hasattr(res, 'update'): parentName= res.__bases__[-1].__name__ moduleName= 'Products.%s.wrappers' % self.getTool().getAppName() exec 'import %s' % moduleName exec 'parent = %s.%s' % (moduleName, parentName) res.update(parent) return res def getAppyType(self, name, asDict=False, className=None): '''Returns the Appy type named p_name. If no p_className is defined, the field is supposed to belong to self's class.''' isInnerType = '*' in name # An inner type lies within a List type. subName = None if isInnerType: elems = name.split('*') if len(elems) == 2: name, subName = elems else: name, subName, i = elems if not className: klass = self.__class__.wrapperClass else: klass = self.getTool().getAppyClass(className, wrapper=True) res = getattr(klass, name, None) if res and isInnerType: res = res.getField(subName) if res and asDict: return res.__dict__ return res def getAllAppyTypes(self, className=None): '''Returns the ordered list of all Appy types for self's class if p_className is not specified, or for p_className else.''' if not className: klass = self.__class__.wrapperClass else: klass = self.getTool().getAppyClass(className, wrapper=True) return klass.__fields__ def getGroupedAppyTypes(self, layoutType, pageName, cssJs=None): '''Returns the fields sorted by group. For every field, the appyType (dict version) is given. If a dict is given in p_cssJs, we will add it in the css and js files required by the fields.''' res = [] groups = {} # The already encountered groups # If a dict is given in p_cssJs, we must fill it with the CSS and JS # files required for every returned appyType. collectCssJs = isinstance(cssJs, dict) css = js = None # If param "refresh" is there, we must reload the Python class refresh = ('refresh' in self.REQUEST) if refresh: klass = self.getClass(reloaded=True) for appyType in self.getAllAppyTypes(): if refresh: appyType = appyType.reload(klass, self) if appyType.page.name != pageName: continue if not appyType.isShowable(self, layoutType): continue if collectCssJs: if css == None: css = [] appyType.getCss(layoutType, css) if js == None: js = [] appyType.getJs(layoutType, js) if not appyType.group: res.append(appyType.__dict__) else: # Insert the GroupDescr instance corresponding to # appyType.group at the right place groupDescr = appyType.group.insertInto(res, groups, appyType.page, self.meta_type) GroupDescr.addWidget(groupDescr, appyType.__dict__) if collectCssJs: cssJs['css'] = css cssJs['js'] = js return res def getAppyTypes(self, layoutType, pageName): '''Returns the list of fields that belong to a given page (p_pageName) for a given p_layoutType. If p_pageName is None, fields of all pages are returned.''' res = [] for field in self.getAllAppyTypes(): if pageName and (field.page.name != pageName): continue if not field.isShowable(self, layoutType): continue res.append(field) return res def getCssJs(self, fields, layoutType, res): '''Gets, in p_res ~{'css':[s_css], 'js':[s_js]}~ the lists of Javascript and CSS files required by Appy types p_fields when shown on p_layoutType.''' # Lists css and js below are not sets, because order of Javascript # inclusion can be important, and this could be losed by using sets. css = [] js = [] for field in fields: field.getCss(layoutType, css) field.getJs(layoutType, js) res['css'] = css res['js'] = js def getColumnsSpecifiers(self, columnLayouts, dir): '''Extracts and returns, from a list of p_columnLayouts, the information that is necessary for displaying a column in a result screen or for a Ref field.''' res = [] tool = self.getTool() for info in columnLayouts: fieldName, width, align = ColumnLayout(info).get() align = tool.flipLanguageDirection(align, dir) field = self.getAppyType(fieldName, asDict=True) if not field: self.log('Field "%s", used in a column specifier, was not ' \ 'found.' % fieldName, type='warning') else: res.append({'field':field, 'width':width, 'align': align}) return res def getAppyTransitions(self, includeFake=True, includeNotShowable=False): '''This method returns info about transitions that one can trigger from the user interface. * if p_includeFake is True, it retrieves transitions that the user can't trigger, but for which he needs to know for what reason he can't trigger it; * if p_includeNotShowable is True, it includes transitions for which show=False. Indeed, because "showability" is only a GUI concern, and not a security concern, in some cases it has sense to set includeNotShowable=True, because those transitions are triggerable from a security point of view. ''' res = [] wf = self.getWorkflow() currentState = self.State(name=False) # Loop on every transition for name in dir(wf): transition = getattr(wf, name) if (transition.__class__.__name__ != 'Transition'): continue # Filter transitions that do not have currentState as start state if not transition.hasState(currentState, True): continue # Check if the transition can be triggered mayTrigger = transition.isTriggerable(self, wf) # Compute the condition that will lead to including or not this # transition if not includeFake: includeIt = mayTrigger else: includeIt = mayTrigger or isinstance(mayTrigger, gen.No) if not includeNotShowable: includeIt = includeIt and transition.isShowable(wf, self) if not includeIt: continue # Add transition-info to the result. label = self.getWorkflowLabel(name) tInfo = {'name': name, 'title': self.translate(label), 'confirm': '', 'may_trigger': True} if transition.confirm: cLabel = '%s_confirm' % label tInfo['confirm'] = self.translate(cLabel, format='js') if not mayTrigger: tInfo['may_trigger'] = False tInfo['reason'] = mayTrigger.msg res.append(tInfo) return res def getAppyPhases(self, currentOnly=False, layoutType='view'): '''Gets the list of phases that are defined for this content type. If p_currentOnly is True, the search is limited to the phase where the current page (as defined in the request) lies.''' # Get the list of phases res = [] # Ordered list of phases phases = {} # Dict of phases for appyType in self.getAllAppyTypes(): typePhase = appyType.page.phase if typePhase not in phases: phase = PhaseDescr(typePhase, self) res.append(phase.__dict__) phases[typePhase] = phase else: phase = phases[typePhase] phase.addPage(appyType, self, layoutType) if (appyType.type == 'Ref') and appyType.navigable: phase.addPageLinks(appyType, self) # Remove phases that have no visible page for i in range(len(res)-1, -1, -1): if not res[i]['pages']: del phases[res[i]['name']] del res[i] # Compute next/previous phases of every phase for ph in phases.itervalues(): ph.computeNextPrevious(res) ph.totalNbOfPhases = len(res) # Restrict the result to the current phase if required if currentOnly: rq = self.REQUEST page = rq.get('page', None) if not page: if layoutType == 'edit': page = self.getDefaultEditPage() else: page = self.getDefaultViewPage() for phaseInfo in res: if page in phaseInfo['pages']: return phaseInfo # If I am here, it means that the page as defined in the request, # or the default page, is not existing nor visible in any phase. # In this case I find the first visible page among all phases. viewAttr = 'showOn%s' % layoutType.capitalize() for phase in res: for page in phase['pages']: if phase['pagesInfo'][page][viewAttr]: rq.set('page', page) pageFound = True break return phase else: # Return an empty list if we have a single, link-free page within # a single phase. if (len(res) == 1) and (len(res[0]['pages']) == 1) and \ not res[0]['pagesInfo'][res[0]['pages'][0]].get('links'): return None return res def getIcons(self): '''Gets the icons that can be shown besides the title of an object.''' appyObj = self.appy() if hasattr(appyObj, 'getIcons'): return appyObj.getIcons() return '' def getSubTitle(self): '''Gets the content that must appear below the title of an object.''' appyObj = self.appy() if hasattr(appyObj, 'getSubTitle'): return appyObj.getSubTitle() return '' def getPreviousPage(self, phase, page): '''Returns the page that precedes p_page which is in p_phase.''' try: pageIndex = phase['pages'].index(page) except ValueError: # The current page is probably not visible anymore. Return the # first available page in current phase. res = phase['pages'][0] return res, phase['pagesInfo'][res] if pageIndex > 0: # We stay on the same phase, previous page res = phase['pages'][pageIndex-1] resInfo = phase['pagesInfo'][res] return res, resInfo else: if phase['previousPhase']: # We go to the last page of previous phase previousPhase = phase['previousPhase'] res = previousPhase['pages'][-1] resInfo = previousPhase['pagesInfo'][res] return res, resInfo else: return None, None def getNextPage(self, phase, page): '''Returns the page that follows p_page which is in p_phase.''' try: pageIndex = phase['pages'].index(page) except ValueError: # The current page is probably not visible anymore. Return the # first available page in current phase. res = phase['pages'][0] return res, phase['pagesInfo'][res] if pageIndex < len(phase['pages'])-1: # We stay on the same phase, next page res = phase['pages'][pageIndex+1] resInfo = phase['pagesInfo'][res] return res, resInfo else: if phase['nextPhase']: # We go to the first page of next phase nextPhase = phase['nextPhase'] res = nextPhase['pages'][0] resInfo = nextPhase['pagesInfo'][res] return res, resInfo else: return None, None def changeRefOrder(self, fieldName, objectUid, newIndex, isDelta): '''This method changes the position of object with uid p_objectUid in reference field p_fieldName to p_newIndex i p_isDelta is False, or to actualIndex+p_newIndex if p_isDelta is True.''' refs = getattr(self.aq_base, fieldName, None) oldIndex = refs.index(objectUid) refs.remove(objectUid) if isDelta: newIndex = oldIndex + newIndex else: pass # To implement later on refs.insert(newIndex, objectUid) def onChangeRefOrder(self): '''This method is called when the user wants to change order of an item in a reference field.''' rq = self.REQUEST # Move the item up (-1), down (+1) ? move = -1 # Move up if rq['move'] == 'down': move = 1 # Down isDelta = True self.changeRefOrder(rq['fieldName'], rq['refObjectUid'], move, isDelta) def onSortReference(self): '''This method is called when the user wants to sort the content of a reference field.''' rq = self.REQUEST fieldName = rq.get('fieldName') sortKey = rq.get('sortKey') reverse = rq.get('reverse') == 'True' self.appy().sort(fieldName, sortKey=sortKey, reverse=reverse) def notifyWorkflowCreated(self): '''This method is called every time an object is created, be it temp or not. The objective here is to initialise workflow-related data on the object.''' wf = self.getWorkflow() # Get the initial workflow state initialState = self.State(name=False) # Create a Transition instance representing the initial transition. initialTransition = gen.Transition((initialState, initialState)) initialTransition.trigger('_init_', self, wf, '') def getWorkflow(self, name=False, className=None): '''Returns the workflow applicable for p_self (or for any instance of p_className if given), or its name, if p_name is True.''' if not className: wrapperClass = self.wrapperClass else: wrapperClass = self.getTool().getAppyClass(className, wrapper=True) wf = wrapperClass.getWorkflow() if not name: return wf return WorkflowDescriptor.getWorkflowName(wf) def getWorkflowLabel(self, stateName=None): '''Gets the i18n label for p_stateName, or for the current object state if p_stateName is not given. Note that if p_stateName is given, it can also represent the name of a transition.''' stateName = stateName or self.State() return '%s_%s' % (self.getWorkflow(name=True), stateName) def refreshSecurity(self): '''Refresh security info on this object. Returns True if the info has effectively been updated.''' wf = self.getWorkflow() try: # Get the state definition of the object's current state. state = getattr(wf, self.State()) except AttributeError: # The workflow information for this object does not correspond to # its current workflow attribution. Add a new fake event # representing passage of this object to the initial state of his # currently attributed workflow. stateName = self.State(name=True, initial=True) self.addHistoryEvent(None, review_state=stateName) state = self.State(name=False, initial=True) self.log('Wrong workflow info for a "%s"; is now in state "%s".' % \ (self.meta_type, stateName)) # Update permission attributes on the object if required updated = state.updatePermissions(wf, self) if updated: # Reindex the object because security-related info is indexed. self.reindex() return updated def applyUserIdChange(self, oldId, newId): '''A user whose ID was p_oldId has now p_newId. If the old ID was mentioned in self's local roles, update it to the new ID. This method returns 1 if a change occurred, 0 else.''' if oldId in self.__ac_local_roles__: localRoles = self.__ac_local_roles__.copy() localRoles[newId] = localRoles[oldId] del localRoles[oldId] self.__ac_local_roles__ = localRoles self.reindex() return 1 return 0 def findNewValue(self, field, history, stopIndex): '''This function tries to find a more recent version of value of p_field on p_self. It first tries to find it in history[:stopIndex+1]. If it does not find it there, it returns the current value on p_obj.''' i = stopIndex + 1 while (i-1) >= 0: i -= 1 if history[i]['action'] != '_datachange_': continue if field.name not in history[i]['changes']: continue # We have found it! return history[i]['changes'][field.name][0] or '' return field.getValue(self) or '' def getHistoryTexts(self, event): '''Returns a tuple (insertText, deleteText) containing texts to show on, respectively, inserted and deleted chunks of text in a XHTML diff.''' tool = self.getTool() userName = tool.getUserName(event['actor']) mapping = {'userName': userName.decode('utf-8')} res = [] for type in ('insert', 'delete'): msg = self.translate('history_%s' % type, mapping=mapping) date = tool.formatDate(event['time'], withHour=True) msg = '%s: %s' % (date, msg) res.append(msg.encode('utf-8')) return res def hasHistory(self, fieldName=None): '''Has this object an history? If p_fieldName is specified, the question becomes: has this object an history for field p_fieldName?''' if hasattr(self.aq_base, 'workflow_history') and self.workflow_history: history = self.workflow_history.values()[0] if not fieldName: for event in history: if event['action'] and (event['comments'] != '_invisible_'): return True else: for event in history: if (event['action'] == '_datachange_') and \ (fieldName in event['changes']) and \ event['changes'][fieldName][0]: return True def getHistory(self, startNumber=0, reverse=True, includeInvisible=False, batchSize=5): '''Returns the history for this object, sorted in reverse order (most recent change first) if p_reverse is True.''' # Get a copy of the history, reversed if needed, whose invisible events # have been removed if needed. key = self.workflow_history.keys()[0] history = list(self.workflow_history[key][1:]) if not includeInvisible: history = [e for e in history if e['comments'] != '_invisible_'] if reverse: history.reverse() # Keep only events which are within the batch. res = [] stopIndex = startNumber + batchSize - 1 i = -1 while (i+1) < len(history): i += 1 # Ignore events outside range startNumber:startNumber+batchSize if i < startNumber: continue if i > stopIndex: break if history[i]['action'] == '_datachange_': # Take a copy of the event: we will modify it and replace # fields' old values by their formatted counterparts. event = history[i].copy() event['changes'] = {} for name, oldValue in history[i]['changes'].iteritems(): # oldValue is a tuple (value, fieldName). field = self.getAppyType(name) # Field 'name' may not exist, if the history has been # transferred from another site. In this case we can't show # this data change. if not field: continue if (field.type == 'String') and \ (field.format == gen.String.XHTML): # For rich text fields, instead of simply showing the # previous value, we propose a diff with the next # version, excepted if the previous value is empty. if field.isEmptyValue(oldValue[0]): val = '-' else: newValue = self.findNewValue(field, history, i-1) # Compute the diff between oldValue and newValue iMsg, dMsg = self.getHistoryTexts(event) comparator= HtmlDiff(oldValue[0],newValue,iMsg,dMsg) val = comparator.get() event['changes'][name] = (val, oldValue[1]) else: val = field.getFormattedValue(self, oldValue[0]) or '-' if isinstance(val, list) or isinstance(val, tuple): val = '