'''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, re, sys, types, urllib, cgi from appy import Object from appy.px import Px from appy.fields.workflow import UiTransition import appy.gen as gen from appy.gen.utils import * from appy.gen.layout import Table, defaultPageLayouts from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor from appy.shared import utils as sutils from appy.shared.data import rtlLanguages from appy.shared.xml_parser import XmlMarshaller from appy.shared.diff import HtmlDiff # ------------------------------------------------------------------------------ NUMBERED_ID = re.compile('.+\d{4}$') # ------------------------------------------------------------------------------ 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() # The app may define a method klass.generateUid for producing an UID # for instance of this class. If no such method is found, we use the # standard Appy method to produce an UID. id = None klass = tool.getAppyClass(obj.portal_type) if hasattr(klass, 'generateUid'): id = klass.generateUid(obj.REQUEST) if not id: 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) # 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 updateField(self, name, value): '''Updates a single field p_name with new p_value.''' field = self.getAppyType(name) # Remember previous value if the field is historized. previousData = self.rememberPreviousData([field]) # Store the new value into the database field.store(self, value) # Update the object history when relevant if previousData: self.historizeData(previousData) # Update last modification date from DateTime import DateTime self.modified = DateTime() 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 filesystem folder corresponding to this object folder = os.path.join(*self.getFsFolder()) if os.path.exists(folder): sutils.FolderDeleter.delete(folder) sutils.FolderDeleter.deleteEmpty(os.path.dirname(folder)) # 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('action_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.login, 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('action_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 getDbFolder(self): '''Gets the folder, on the filesystem, where the database (Data.fs and sub-folders) lies.''' return os.path.dirname(self.getTool().getApp()._p_jar.db().getName()) def getFsFolder(self, create=False): '''Gets the folder where binary files tied to this object will be stored on the filesystem. If p_create is True and the folder does not exist, it is created (together with potentially missing parent folders). This folder is returned as a tuple (s_baseDbFolder, s_subPath).''' objId = self.id # Get the root folder where Data.fs lies. dbFolder = self.getDbFolder() # Build the list of path elements within this db folder. path = [] inConfig = False for elem in self.getPhysicalPath(): if not elem: continue if elem == 'data': continue if elem == 'config': inConfig = True if not path or ((len(path) == 1) and inConfig): # This object is at the root of the filesystem. if NUMBERED_ID.match(elem): path.append(elem[-4:]) path.append(elem) # We are done if elem corresponds to the object id. if elem == objId: break path = os.sep.join(path) if create: fullPath = os.path.join(dbFolder, path) if not os.path.exists(fullPath): os.makedirs(fullPath) return dbFolder, path def view(self): '''Returns the view PX.''' obj = self.appy() return obj.pxView({'obj': obj, 'tool': obj.tool}) def edit(self): '''Returns the edit PX.''' obj = self.appy() return obj.pxEdit({'obj': obj, 'tool': obj.tool}) def ajax(self): '''Called via an Ajax request to render some PX whose name is in the request.''' obj = self.appy() return obj.pxAjax({'obj': obj, 'tool': obj.tool}) 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.login 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.login != self.locks[page][0]): return self.locks[page] def removeLock(self, page, force=False): '''Removes the lock on the current page. This happens: - after the page has been saved: the lock must be released; - or when an admin wants to force the deletion of a lock that was left on p_page for too long (p_force=True). ''' 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 (excepted if p_force is True) if not force: userId = self.getTool().getUser().login 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 pxView 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.login == self.locks[page][0]): del self.locks[page] def onUnlock(self): '''Called when an admin wants to remove a lock that was left for too long by some user.''' rq = self.REQUEST tool = self.getTool() obj = tool.getObject(rq['objectUid']) obj.removeLock(rq['pageName'], force=True) urlBack = self.getUrl(rq['HTTP_REFERER']) self.say(self.translate('action_done')) self.goto(urlBack) 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 field in self.getAppyTypes('edit', rq.form.get('page')): if not field.validable: continue value = field.getRequestValue(rq) message = field.validate(self, value) if message: setattr(errors, field.name, message) else: setattr(values, field.name, field.getStorableValue(value)) # Validate sub-fields within Lists if field.type != 'List': continue i = -1 for row in value: i += 1 for name, subField in field.fields: message = subField.validate(self, getattr(row,name,None)) if message: setattr(errors, '%s*%d' % (subField.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__: for k,v in errors.__dict__.iteritems(): rq.set('%s_error' % k, v) self.say(errorMessage) return self.gotoEdit() # Trigger inter-field validation msg = self.interFieldValidation(errors, values) if not msg: msg = errorMessage if errors.__dict__: for k,v in errors.__dict__.iteritems(): rq.set('%s_error' % k, v) 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('read'): 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. phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit') pageName, pageInfo = phaseObj.getPreviousPage(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'] phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit') pageName, pageInfo = phaseObj.getNextPage(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, 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('read'): 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)() if isinstance(methodRes, Px): res = methodRes({'self': self.appy()}) elif isinstance(methodRes, file): res = methodRes.read() methodRes.close() elif isinstance(methodRes, basestring) and \ methodRes.startswith('= 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 = '' % \ ''.join(['
  • %s
  • ' % v for v in val]) event['changes'][name] = (val, oldValue[1]) else: event = history[i] res.append(event) return Object(events=res, totalNumber=len(history)) def mayNavigate(self): '''May the currently logged user see the navigation panel linked to this object?''' appyObj = self.appy() if hasattr(appyObj, 'mayNavigate'): return appyObj.mayNavigate() return True def getDefaultViewPage(self): '''Which view page must be shown by default?''' appyObj = self.appy() if hasattr(appyObj, 'getDefaultViewPage'): return appyObj.getDefaultViewPage() return 'main' def getDefaultEditPage(self): '''Which edit page must be shown by default?''' appyObj = self.appy() if hasattr(appyObj, 'getDefaultEditPage'): return appyObj.getDefaultEditPage() return 'main' def mayAct(self): '''May the currently logged user see column "actions" for this object? This can be used for hiding the "edit" icon, for example: when a user may edit only a restricted set of fields on an object, we may avoid showing him the global "edit" icon.''' appyObj = self.appy() if hasattr(appyObj, 'mayAct'): return appyObj.mayAct() return True def mayDelete(self): '''May the currently logged user delete this object?''' res = self.allows('delete') if not res: return # An additional, user-defined condition, may refine the base permission. appyObj = self.appy() if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete() return True def mayEdit(self, permission='write'): '''May the currently logged user edit this object? p_perm can be a field-specific permission.''' res = self.allows(permission) if not res: return # An additional, user-defined condition, may refine the base permission. appyObj = self.appy() if hasattr(appyObj, 'mayEdit'): return appyObj.mayEdit() return True def executeAppyAction(self, actionName, reindex=True): '''Executes action with p_fieldName on this object.''' appyType = self.getAppyType(actionName) actionRes = appyType(self.appy()) parent = self.getParentNode() parentAq = getattr(parent, 'aq_base', parent) if not hasattr(parentAq, self.id): # Else, it means that the action has led to self's deletion. self.reindex() return appyType.result, actionRes def onExecuteAppyAction(self): '''This method is called every time a user wants to execute an Appy action on an object.''' rq = self.REQUEST resultType, actionResult = self.executeAppyAction(rq['fieldName']) successfull, msg = actionResult if not msg: # Use the default i18n messages suffix = 'ko' if successfull: suffix = 'ok' msg = self.translate('action_%s' % suffix) if (resultType == 'computation') or not successfull: self.say(msg) return self.goto(self.getUrl(rq['HTTP_REFERER'])) elif resultType.startswith('file'): # msg does not contain a message, but a file instance. response = self.REQUEST.RESPONSE response.setHeader('Content-Type', sutils.getMimeType(msg.name)) response.setHeader('Content-Disposition', 'inline;filename="%s"' %\ os.path.basename(msg.name)) response.write(msg.read()) msg.close() if resultType == 'filetmp': # p_msg is a temp file. We need to delete it. try: os.remove(msg.name) self.log('Temp file "%s" was deleted.' % msg.name) except IOError, err: self.log('Could not remove temp "%s" (%s).' % \ (msg.name, str(err)), type='warning') except OSError, err: self.log('Could not remove temp "%s" (%s).' % \ (msg.name, str(err)), type='warning') elif resultType == 'redirect': # msg does not contain a message, but the URL where to redirect # the user. return self.goto(msg) def trigger(self, transitionName, comment='', doAction=True, doNotify=True, doHistory=True, doSay=True, noSecurity=False): '''Triggers transition named p_transitionName.''' # Check that this transition exists. wf = self.getWorkflow() if not hasattr(wf, transitionName) or \ getattr(wf, transitionName).__class__.__name__ != 'Transition': raise 'Transition "%s" was not found.' % transitionName # Is this transition triggerable? transition = getattr(wf, transitionName) if not transition.isTriggerable(self, wf, noSecurity=noSecurity): raise 'Transition "%s" can\'t be triggered.' % transitionName # Trigger the transition transition.trigger(transitionName, self, wf, comment, doAction=doAction, doNotify=doNotify, doHistory=doHistory, doSay=doSay) def onTrigger(self): '''This method is called whenever a user wants to trigger a workflow transition on an object.''' rq = self.REQUEST self.trigger(rq['workflow_action'], comment=rq.get('comment', '')) self.reindex() return self.goto(self.getUrl(rq['HTTP_REFERER'])) def getRolesFor(self, permission): '''Gets, according to the workflow, the roles that are currently granted p_permission on this object.''' state = self.State(name=False) roles = state.permissions[permission] if roles: return [role.name for role in roles] return () def appy(self): '''Returns a wrapper object allowing to manipulate p_self the Appy way.''' # Create the dict for storing Appy wrapper on the REQUEST if needed. rq = getattr(self, 'REQUEST', None) if not rq: # We are in test mode or Zope is starting. Use static variable # config.fakeRequest instead. rq = self.getProductConfig().fakeRequest if not hasattr(rq, 'wrappers'): rq.wrappers = {} # Return the Appy wrapper if already present in the cache uid = self.UID() if uid in rq.wrappers: return rq.wrappers[uid] # Create the Appy wrapper, cache it in rq.wrappers and return it wrapper = self.wrapperClass(self) rq.wrappers[uid] = wrapper return wrapper # -------------------------------------------------------------------------- # Methods for computing values of standard Appy indexes # -------------------------------------------------------------------------- def UID(self): '''Returns the unique identifier for this object.''' return self._at_uid def Title(self): '''Returns the title for this object.''' title = self.getAppyType('title') if title: return title.getValue(self) return self.id def SortableTitle(self): '''Returns the title as must be stored in index "SortableTitle".''' return sutils.normalizeText(self.Title()) def SearchableText(self): '''This method concatenates the content of every field with searchable=True for indexing purposes.''' res = [] for field in self.getAllAppyTypes(): if not field.searchable: continue res.append(field.getIndexValue(self, forSearch=True)) return res def Creator(self): '''Who create this object?''' return self.creator def Created(self): '''When was this object created ?''' return self.created def Modified(self): '''When was this object last modified ?''' if hasattr(self.aq_base, 'modified'): return self.modified return self.created def State(self, name=True, initial=False): '''Returns information about the current object state. If p_name is True, the returned info is the state name. Else, it is the State instance. If p_initial is True, instead of returning info about the current state, it returns info about the workflow initial state.''' wf = self.getWorkflow() if initial or not hasattr(self.aq_base, 'workflow_history'): # No workflow information is available (yet) on this object, or # initial state is asked. In both cases, return info about this # initial state. res = 'active' for elem in dir(wf): attr = getattr(wf, elem) if (attr.__class__.__name__ == 'State') and attr.initial: res = elem break else: # Return info about the current object state key = self.workflow_history.keys()[0] res = self.workflow_history[key][-1]['review_state'] # Return state name or state definition? if name: return res else: return getattr(wf, res) def ClassName(self): '''Returns the name of the (Zope) class for self.''' return self.portal_type def Allowed(self): '''Returns the list of roles and users that are allowed to view this object. This index value will be used within catalog queries for filtering objects the user is allowed to see.''' # Get, from the workflow, roles having permission 'read'. res = self.getRolesFor('read') # Add users or groups having, locally, this role on this object. localRoles = getattr(self.aq_base, '__ac_local_roles__', None) if not localRoles: return res for id, roles in localRoles.iteritems(): for role in roles: if role in res: usr = 'user:%s' % id if usr not in res: res.append(usr) return res def showState(self): '''Must I show self's current state ?''' stateShow = self.State(name=False).show if callable(stateShow): return stateShow(self.getWorkflow(), self.appy()) return stateShow def showTransitions(self, layoutType): '''Must we show the buttons/icons for triggering transitions on p_layoutType?''' # Never show transitions on edit pages. if layoutType == 'edit': return # Use the default value if self's class does not specify it. klass = self.getClass() if not hasattr(klass, 'showTransitions'): return (layoutType=='view') showValue = klass.showTransitions # This value can be a single value or a tuple/list of values. if isinstance(showValue, basestring): return layoutType == showValue return layoutType in showValue getUrlDefaults = {'page':True, 'nav':True} def getUrl(self, base=None, mode='view', **kwargs): '''Returns an URL for this object. * If p_base is None, it will be the base URL for this object (ie, Zope self.absolute_url()). * p_mode can be "edit", "view" or "raw" (a non-param, base URL) * p_kwargs can store additional parameters to add to the URL. In this dict, every value that is a string will be added to the URL as-is. Every value that is True will be replaced by the value in the request for the corresponding key (if existing; else, the param will not be included in the URL at all).''' # Define the URL suffix suffix = '' if mode != 'raw': suffix = '/%s' % mode # Define base URL if omitted if not base: base = self.absolute_url() + suffix # If a raw URL is asked, remove any param and suffix. if mode == 'raw': if '?' in base: base = base[:base.index('?')] base = base.strip('/') for mode in ('view', 'edit'): if base.endswith(mode): base = base[:-len(mode)].strip('/') break return base # Manage default args if not kwargs: kwargs = self.getUrlDefaults if 'page' not in kwargs: kwargs['page'] = True if 'nav' not in kwargs: kwargs['nav'] = True # Create URL parameters from kwargs params = [] for name, value in kwargs.iteritems(): if isinstance(value, basestring): params.append('%s=%s' % (name, value)) elif self.REQUEST.get(name, ''): params.append('%s=%s' % (name, self.REQUEST[name])) if params: params = '&'.join(params) if base.find('?') != -1: params = '&' + params else: params = '?' + params else: params = '' return '%s%s' % (base, params) def getTool(self): '''Returns the application tool.''' return self.getPhysicalRoot().config def getProductConfig(self, app=False): '''Returns a reference to the config module. If p_app is True, it returns the application config.''' res = self.__class__.config if app: res = res.appConfig return res def getParent(self): '''If this object is stored within another one, this method returns it. Else (if the object is stored directly within the tool or the root data folder) it returns None.''' parent = self.getParentNode() # Not-Managers can't navigate back to the tool if (parent.id == 'config') and \ not self.getTool().getUser().has_role('Manager'): return False if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent def getBreadCrumb(self): '''Gets breadcrumb info about this object and its parents (if it must be shown).''' # Return an empty breadcrumb if it must not be shown. klass = self.getClass() if hasattr(klass, 'breadcrumb') and not klass.breadcrumb: return () # Compute the breadcrumb res = [Object(url=self.absolute_url(), title=self.getFieldValue('title', layoutType='view'))] parent = self.getParent() if parent: res = parent.getBreadCrumb() + res return res def index_html(self): '''Base method called when hitting this object. - The standard behaviour is to redirect to /view. - If a parameter named "do" is present in the request, it is supposed to contain the name of a method to call on this object. In this case, we call this method and return its result as XML. - If method is POST, we consider the request to be XML data, that we marshall to Python, and we call the method in param "do" with, as arg, this marshalled Python object. While this could sound strange to expect a query string containing a param "do" in a HTTP POST, the HTTP spec does not prevent to do it.''' rq = self.REQUEST if (rq.REQUEST_METHOD == 'POST') and rq.QUERY_STRING: # A POST method containing XML data. rq.args = XmlUnmarshaller().parse(rq.stdin.getvalue()) # Find the name of the method to call. methodName = rq.QUERY_STRING.split('=')[1] return self.xml(action=methodName) elif rq.has_key('do'): # The user wants to call a method on this object and get its result # as XML. return self.xml(action=rq['do']) else: # The user wants to consult the view page for this object return rq.RESPONSE.redirect(self.getUrl()) def getUserLanguage(self): '''Gets the language (code) of the current user.''' if not hasattr(self, 'REQUEST'): return 'en' # Try the value which comes from the cookie. Indeed, if such a cookie is # present, it means that the user has explicitly chosen this language # via the language selector. rq = self.REQUEST if '_ZopeLg' in rq.cookies: return rq.cookies['_ZopeLg'] # Try the LANGUAGE key from the request: it corresponds to the language # as configured in the user's browser. res = self.REQUEST.get('LANGUAGE', None) if res: return res # Try the HTTP_ACCEPT_LANGUAGE key from the request, which stores # language preferences as defined in the user's browser. Several # languages can be listed, from most to less wanted. res = self.REQUEST.get('HTTP_ACCEPT_LANGUAGE', None) if not res: return 'en' if ',' in res: res = res[:res.find(',')] if '-' in res: res = res[:res.find('-')] return res def getLanguageDirection(self, lang): '''Determines if p_lang is a LTR or RTL language.''' if lang in rtlLanguages: return 'rtl' return 'ltr' def formatText(self, text, format='html'): '''Produces a representation of p_text into the desired p_format, which is "html" by default.''' if 'html' in format: if format == 'html_from_text': text = cgi.escape(text) res = text.replace('\r\n', '
    ').replace('\n', '
    ') elif format == 'text': res = text.replace('
    ', '\n') else: res = text return res def translate(self, label, mapping={}, domain=None, default=None, language=None, format='html', field=None): '''Translates a given p_label into p_domain with p_mapping. If p_field is given, p_label does not correspond to a full label name, but to a label type linked to p_field: "label", "descr" or "help". Indeed, in this case, a specific i18n mapping may be available on the field, so we must merge this mapping into p_mapping.''' cfg = self.getProductConfig() if not domain: domain = cfg.PROJECTNAME # Get the label name, and the field-specific mapping if any. if field: if field.type != 'group': fieldMapping = field.mapping[label] if fieldMapping: if callable(fieldMapping): fieldMapping = field.callMethod(self, fieldMapping) mapping.update(fieldMapping) label = getattr(field, '%sId' % label) # We will get the translation from a Translation object. # In what language must we get the translation? if not language: language = self.getUserLanguage() tool = self.getTool() try: translation = getattr(tool, language).appy() except AttributeError: # We have no translation for this language. Fallback to 'en'. translation = getattr(tool, 'en').appy() res = getattr(translation, label, '') if not res: # Fallback to 'en'. translation = getattr(tool, 'en').appy() res = getattr(translation, label, '') # If still no result, put the label instead of a translated message if not res: res = label else: # Perform replacements, according to p_format. res = self.formatText(res, format) # Perform variable replacements for name, repl in mapping.iteritems(): if not isinstance(repl, basestring): repl = str(repl) res = res.replace('${%s}' % name, repl) return res def getPageLayout(self, layoutType): '''Returns the layout corresponding to p_layoutType for p_self.''' appyClass = self.wrapperClass.__bases__[-1] if hasattr(appyClass, 'layouts'): layout = appyClass.layouts[layoutType] if isinstance(layout, basestring): layout = Table(layout) else: layout = defaultPageLayouts[layoutType] return layout def download(self, name=None): '''Downloads the content of the file that is in the File field whose name is in the request. This name can also represent an attribute storing an image within a rich text field. If p_name is not given, it is retrieved from the request.''' name = self.REQUEST.get('name') if not name: return if '_img_' not in name: appyType = self.getAppyType(name) else: appyType = self.getAppyType(name.split('_img_')[0]) if (not appyType.isShowable(self, 'view')) and \ (not appyType.isShowable(self, 'result')): from zExceptions import NotFound raise NotFound() info = getattr(self.aq_base, name, None) if info: # Write the file in the HTTP response. info.writeResponse(self.REQUEST.RESPONSE, self.getDbFolder()) def upload(self): '''Receives an image uploaded by the user via ckeditor and stores it in a special field on this object.''' # Get the name of the rich text field for which an image must be stored. params = self.REQUEST['QUERY_STRING'].split('&') fieldName = params[0].split('=')[1] ckNum = params[1].split('=')[1] # We will store the image in a field named [fieldName]_img_[nb]. i = 1 attrName = '%s_img_%d' % (fieldName, i) while True: if not hasattr(self.aq_base, attrName): break else: i += 1 attrName = '%s_img_%d' % (fieldName, i) # Store the image. Create a fake File instance for doing the job. fakeFile = gen.File(isImage=True) fakeFile.name = attrName fakeFile.store(self, self.REQUEST['upload']) # Return the URL of the image. url = '%s/download?name=%s' % (self.absolute_url(), attrName) response = self.REQUEST.RESPONSE response.setHeader('Content-Type', 'text/html') resp = "" % (ckNum, url) response.write(resp) def allows(self, permission, raiseError=False): '''Has the logged user p_permission on p_self ?''' res = self.getTool().getUser().has_permission(permission, self) if not res and raiseError: from AccessControl import Unauthorized raise Unauthorized return res def isTemporary(self): '''Is this object temporary ?''' parent = self.getParentNode() if not parent: # Is propably being created through code return False return parent.getId() == 'temp_folder' def onProcess(self): '''This method is a general hook for transfering processing of a request to a given field, whose name must be in the request.''' return self.getAppyType(self.REQUEST['name']).process(self) # ------------------------------------------------------------------------------