'''This package contains mixin classes that are mixed in with generated classes: - mixins/BaseMixin is mixed in with Standard Archetypes classes; - mixins/ToolMixin is mixed in with the generated application Tool class.''' # ------------------------------------------------------------------------------ import os, os.path, sys, types, mimetypes, urllib import appy.gen from appy.gen import Type, String, Selection, Role, No from appy.gen.utils import * from appy.gen.layout import Table, defaultPageLayouts from appy.gen.plone25.descriptors import ClassDescriptor from appy.gen.plone25.utils import updateRolesForPermission,checkTransitionGuard # ------------------------------------------------------------------------------ class BaseMixin: '''Every Archetype 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 createOrUpdate(self, created, values): '''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 (p_created is True), p_self is a temporary object created in the request by portal_factory, and this method creates the corresponding final object. In the case of an update, this method simply updates fields of p_self.''' rq = self.REQUEST obj = self if created: obj = self.portal_factory.doCreate(self, self.id) # portal_factory # creates the final object from the temp object. previousData = None if not created: previousData = self.rememberPreviousData() # Perform the change on the object, unless self is a tool being created. if (obj._appy_meta_type == 'Tool') and created: # We do not process form data (=real update on the object) if the # tool itself is being created. pass else: # Store in the database the new value coming from the form for appyType in self.getAppyTypes('edit', rq.get('page')): value = getattr(values, appyType.name, None) appyType.store(obj, value) if created: # Now we have a title for the object, so we derive a nice id obj.unmarkCreationFlag() obj._renameAfterCreation(check_auto_id=True) if previousData: # Keep in history potential changes on historized fields self.historizeData(previousData) # Manage potential link with an initiator object if created and rq.get('nav', None): # Get the initiator splitted = rq['nav'].split('.') if splitted[0] == 'search': return # Not an initiator but a search. initiator = self.uid_catalog(UID=splitted[1])[0].getObject() fieldName = splitted[2].split(':')[0] initiator.appy().link(fieldName, 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) # Manage "add" permissions and reindex the object obj._appy_managePermissions() obj.reindexObject() return obj, msg def delete(self): '''This methods is self's suicide.''' # Call a custom "onDelete" if it exists appyObj = self.appy() if hasattr(appyObj, 'onDelete'): appyObj.onDelete() # Delete the object self.getParentNode().manage_delObjects([self.id]) def onDelete(self): 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 onCreate(self): '''This method is called when a user wants to create a root object in the application folder or an object through a reference field.''' rq = self.REQUEST typeName = rq.get('type_name') # Create the params to add to the URL we will redirect the user to # create the object. urlParams = {'mode':'edit', 'page':'main', 'nav':''} if rq.get('nav', None): # 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) # Determine base URL baseUrl = self.absolute_url() if (self._appy_meta_type == 'Tool') and not urlParams['nav']: # This is the creation of a root object in the app folder baseUrl = self.getAppFolder().absolute_url() objId = self.generateUniqueId(typeName) editUrl = '%s/portal_factory/%s/%s/skyn/edit' % \ (baseUrl, typeName, objId) return self.goto(self.getUrl(editUrl, **urlParams)) 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)) 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 by portal_factory in the request. In this case, the update consists in the creation of the "final" object in the database. 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( 'Please correct the indicated errors.', domain='plone') isNew = rq.get('is_new') == 'True' # Go back to the consult view if the user clicked on 'Cancel' if rq.get('buttonCancel.x', None): if isNew: if rq.get('nav', ''): # We can go back to the initiator page. splitted = rq['nav'].split('.') initiator = tool.getObject(splitted[1]) initiatorPage = splitted[2].split(':')[1] urlBack = initiator.getUrl(page=initiatorPage, nav='') else: # Go back to the root of the site. urlBack = tool.getSiteUrl() else: urlBack = self.getUrl() self.say(self.translate('Changes canceled.', domain='plone')) return self.goto(urlBack) # Object for storing validation errors errors = AppyObject() # Object for storing the (converted) values from the request values = AppyObject() # 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) # Redirect the user to the appropriate page if not msg: msg = obj.translate('Changes saved.', domain='plone') # 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(), obj.id, None): obj.unindexObject() return self.goto(tool.getSiteUrl(), msg) # If the user can't access the object anymore, redirect him to the # main site page. user = self.portal_membership.getAuthenticatedMember() if not user.has_permission('View', obj): return self.goto(tool.getSiteUrl(), msg) if rq.get('buttonOk.x', None) or saveConfirmed: # Go to the consult view for this object obj.say(msg) return self.goto(obj.getUrl()) if rq.get('buttonPrevious.x', None): # 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) return obj.gotoEdit() else: return self.goto(obj.getUrl(page=pageName)) else: obj.say(msg) return self.goto(obj.getUrl()) if rq.get('buttonNext.x', None): # 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']: rq.set('page', pageName) return obj.gotoEdit() else: return self.goto(obj.getUrl(page=pageName)) else: obj.say(msg) return self.goto(obj.getUrl()) return obj.gotoEdit() def say(self, msg, type='info'): '''Prints a p_msg in the user interface. p_logLevel may be "info", "warning" or "error".''' mType = type if mType == 'warning': mType = 'warn' elif mType == 'error': mType = 'stop' try: self.plone_utils.addPortalMessage(msg, type=mType) except UnicodeDecodeError: self.plone_utils.addPortalMessage(msg.decode('utf-8'), type=mType) 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 rememberPreviousData(self): '''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 appyType in self.getAllAppyTypes(): if appyType.historized: res[appyType.name] = appyType.getValue(self) return res 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) # Create the event to record in the history DateTime = self.getProductConfig().DateTime state = self.portal_workflow.getInfoFor(self, 'review_state') user = self.portal_membership.getAuthenticatedMember() event = {'action': '_datachange_', 'changes': changes, 'review_state': state, 'actor': user.id, 'time': DateTime(), 'comments': ''} # Add the event to the history histKey = self.workflow_history.keys()[0] self.workflow_history[histKey] += (event,) 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: # Remove previous message if any if 'portal_status_message=' in url: url = url[:url.find('portal_status_message=')-1] if '?' in url: op = '&' else: op = '?' url += op + urllib.urlencode([('portal_status_message',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 == 3): self.REQUEST.set(field.name, '') return self.skyn.edit(self) def showField(self, name, layoutType='view'): '''Must I show field named p_name on this p_layoutType ?''' if name == 'state': return False return self.getAppyType(name).isShowable(self, layoutType) def getMethod(self, methodName): '''Returns the method named p_methodName.''' return getattr(self, methodName, None) def getFieldValue(self, name, onlyIfSync=False, layoutType=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]): return appyType.getValue(self) return None def getFormattedFieldValue(self, name, value): '''Gets a nice, string representation of p_value which is a value from field named p_name.''' return self.getAppyType(name).getFormattedValue(self, value) 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.''' appyType = self.getAppyType(name) return appyType.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): '''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) res = refObject.title if 'title' in appyType.shownInfo: # We may place it at another place 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.''' sortedObjectsUids = self._appy_getSortedField(fieldName) res = sortedObjectsUids.index(obj.UID()) return res def isDebug(self): '''Are we in debug mode ?''' for arg in sys.argv: if arg == 'debug-mode=on': return True return False 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.Extensions.appyWrappers' % \ 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.''' className = className or self.__class__.__name__ attrs = self.getProductConfig().attributesDict[className] appyType = attrs.get(name, None) if appyType and asDict: return appyType.__dict__ return appyType 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.''' className = className or self.__class__.__name__ return self.getProductConfig().attributes[className] def getGroupedAppyTypes(self, layoutType, pageName): '''Returns the fields sorted by group. For every field, the appyType (dict version) is given.''' res = [] groups = {} # The already encountered groups # In debug mode, reload the module containing self's class. debug = self.isDebug() if debug: klass = self.getClass(reloaded=True) for appyType in self.getAllAppyTypes(): if debug: appyType = appyType.reload(klass, self) if appyType.page.name != pageName: continue if not appyType.isShowable(self, layoutType): continue 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__) return res def getAppyTypes(self, layoutType, pageName): '''Returns the list of appyTypes that belong to a given p_page, for a given p_layoutType.''' res = [] for appyType in self.getAllAppyTypes(): if appyType.page.name != pageName: continue if not appyType.isShowable(self, layoutType): continue res.append(appyType) return res def getCssAndJs(self, fields, layoutType): '''Gets the list 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: fieldCss = field.getCss(layoutType) if fieldCss: for fcss in fieldCss: if fcss not in css: css.append(fcss) fieldJs = field.getJs(layoutType) if fieldJs: for fjs in fieldJs: if fjs not in js: js.append(fjs) return {'css':css, 'js':js} def getAppyTypesFromNames(self, fieldNames, asDict=True, addTitle=True): '''Gets the Appy types named p_fieldNames. If 'title' is not among p_fieldNames and p_addTitle is True, field 'title' is prepended to the result.''' res = [] for name in fieldNames: appyType = self.getAppyType(name, asDict) if appyType: res.append(appyType) elif name == 'state': # We do not return a appyType if the attribute is not a *real* # attribute, but the workfow state. res.append({'name': name, 'labelId': 'workflow_state', 'filterable': False}) else: self.appy().log('Field "%s", used as shownInfo in a Ref, ' \ 'was not found.' % name, type='warning') if addTitle and ('title' not in fieldNames): res.insert(0, self.getAppyType('title', asDict)) return res def getAppyStates(self, phase, currentOnly=False): '''Returns information about the states that are related to p_phase. If p_currentOnly is True, we return the current state, even if not related to p_phase.''' res = [] dcWorkflow = self.getWorkflow(appy=False) if not dcWorkflow: return res currentState = self.portal_workflow.getInfoFor(self, 'review_state') if currentOnly: return [StateDescr(currentState,'current').get()] workflow = self.getWorkflow(appy=True) if workflow: stateStatus = 'done' for stateName in workflow._states: if stateName == currentState: stateStatus = 'current' elif stateStatus != 'done': stateStatus = 'future' state = getattr(workflow, stateName) if (state.phase == phase) and \ (self._appy_showState(workflow, state.show)): res.append(StateDescr(stateName, stateStatus).get()) return res def getAppyTransitions(self, includeFake=True, includeNotShowable=False): '''This method is similar to portal_workflow.getTransitionsFor, but: * is able (or not, depending on boolean p_includeFake) to retrieve transitions that the user can't trigger, but for which he needs to know for what reason he can't trigger it; * is able (or not, depending on p_includeNotShowable) to include transitions for which show=False at the Appy level. 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; * the transition-info is richer: it contains fake-related info (as described above) and confirm-related info (ie, when clicking on the button, do we ask the user to confirm via a popup?)''' res = [] # Get some Plone stuff from the Plone-level config.py TRIGGER_USER_ACTION = self.getProductConfig().TRIGGER_USER_ACTION sm = self.getProductConfig().getSecurityManager # Get the workflow definition for p_obj. workflow = self.getWorkflow(appy=False) if not workflow: return res appyWorkflow = self.getWorkflow(appy=True) # What is the current state for this object? currentState = workflow._getWorkflowStateOf(self) if not currentState: return res # Analyse all the transitions that start from this state. for transitionId in currentState.transitions: transition = workflow.transitions.get(transitionId, None) appyTr = appyWorkflow._transitionsMapping[transitionId] if not transition or (transition.trigger_type!=TRIGGER_USER_ACTION)\ or not transition.actbox_name: continue # We have a possible candidate for a user-triggerable transition if transition.guard is None: mayTrigger = True else: mayTrigger = checkTransitionGuard(transition.guard, sm(), workflow, self) # Compute the condition that will lead to including or not this # transition if not includeFake: includeIt = mayTrigger else: includeIt = mayTrigger or isinstance(mayTrigger, No) if not includeNotShowable: includeIt = includeIt and appyTr.isShowable(appyWorkflow, self) if not includeIt: continue # Add transition-info to the result. tInfo = {'id': transition.id, 'title': transition.title, 'title_or_id': transition.title_or_id(), 'description': transition.description, 'confirm': '', 'name': transition.actbox_name, 'may_trigger': True, 'url': transition.actbox_url % {'content_url': self.absolute_url(), 'portal_url' : '', 'folder_url' : ''}} if appyTr.confirm: label = '%s_confirm' % tInfo['name'] tInfo['confirm'] = self.translate(label, 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: states = self.getAppyStates(typePhase) phase = PhaseDescr(typePhase, states, self) res.append(phase.__dict__) phases[typePhase] = phase else: phase = phases[typePhase] phase.addPage(appyType, self, layoutType) # 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] # Then, compute status of phases for ph in phases.itervalues(): ph.computeStatus(res) ph.totalNbOfPhases = len(res) # Restrict the result to the current phase if required if currentOnly: rq = self.REQUEST page = rq.get('page', 'main') 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 'main' by default, 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 res 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.''' sortedObjectsUids = self._appy_getSortedField(fieldName) oldIndex = sortedObjectsUids.index(objectUid) sortedObjectsUids.remove(objectUid) if isDelta: newIndex = oldIndex + newIndex else: pass # To implement later on sortedObjectsUids.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 getWorkflow(self, appy=True): '''Returns the Appy workflow instance that is relevant for this object. If p_appy is False, it returns the DC workflow.''' res = None if appy: # Get the workflow class first workflowClass = None if self.wrapperClass: appyClass = self.wrapperClass.__bases__[-1] if hasattr(appyClass, 'workflow'): workflowClass = appyClass.workflow if workflowClass: # Get the corresponding prototypical workflow instance res = self.getProductConfig().workflowInstances[workflowClass] else: dcWorkflows = self.portal_workflow.getWorkflowsFor(self) if dcWorkflows: res = dcWorkflows[0] return res def getWorkflowLabel(self, stateName=None): '''Gets the i18n label for the workflow current state. If no p_stateName is given, workflow label is given for the current state.''' res = '' wf = self.getWorkflow(appy=False) if wf: res = stateName if not res: res = self.portal_workflow.getInfoFor(self, 'review_state') appyWf = self.getWorkflow(appy=True) if appyWf: res = '%s_%s' % (wf.id, res) return res def hasHistory(self): '''Has this object an history?''' if hasattr(self.aq_base, 'workflow_history') and self.workflow_history: key = self.workflow_history.keys()[0] for event in self.workflow_history[key]: if event['action'] and (event['comments'] != '_invisible_'): return True return False 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.''' 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() return {'events': history[startNumber:startNumber+batchSize], 'totalNumber': len(history)} def may(self, transitionName): '''May the user execute transition named p_transitionName?''' # Get the Appy workflow instance workflow = self.getWorkflow() res = False if workflow: # Get the corresponding Appy transition transition = workflow._transitionsMapping[transitionName] user = self.portal_membership.getAuthenticatedMember() if isinstance(transition.condition, Role): # It is a role. Transition may be triggered if the user has this # role. res = user.has_role(transition.condition.name, self) elif type(transition.condition) == types.FunctionType: res = transition.condition(workflow, self.appy()) elif type(transition.condition) in (tuple, list): # It is a list of roles and or functions. Transition may be # triggered if user has at least one of those roles and if all # functions return True. hasRole = None for roleOrFunction in transition.condition: if isinstance(roleOrFunction, basestring): if hasRole == None: hasRole = False if user.has_role(roleOrFunction, self): hasRole = True elif type(roleOrFunction) == types.FunctionType: if not roleOrFunction(workflow, self.appy()): return False if hasRole != False: res = True return res 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 mayDelete(self): '''May the currently logged user delete this object? This condition comes as an addition/refinement to the corresponding workflow permission.''' appyObj = self.appy() if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete() return True def executeAppyAction(self, actionName, reindex=True): '''Executes action with p_fieldName on this object.''' appyType = self.getAppyType(actionName) actionRes = appyType(self.appy()) if self.getParentNode().get(self.id): # Else, it means that the action has led to self's deletion. self.reindexObject() 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' appyType = self.getAppyType(rq['fieldName']) label = '%s_action_%s' % (appyType.labelId, suffix) msg = self.translate(label) 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',mimetypes.guess_type(msg.name)[0]) 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 onTriggerTransition(self): '''This method is called whenever a user wants to trigger a workflow transition on an object.''' rq = self.REQUEST self.portal_workflow.doActionFor(self, rq['workflow_action'], comment = rq.get('comment', '')) self.reindexObject() # Where to redirect the user back ? # TODO (?): remove the "phase" param for redirecting the user to the # next phase when relevant. return self.goto(self.getUrl(rq['HTTP_REFERER'])) def fieldValueSelected(self, fieldName, vocabValue, dbValue): '''When displaying a selection box (ie a String with a validator being a list), must the _vocabValue appear as selected?''' rq = self.REQUEST # Get the value we must compare (from request or from database) if rq.has_key(fieldName): compValue = rq.get(fieldName) else: compValue = dbValue # Compare the value if type(compValue) in sequenceTypes: if vocabValue in compValue: return True else: if vocabValue == compValue: return True def checkboxChecked(self, fieldName, dbValue): '''When displaying a checkbox, must it be checked or not?''' rq = self.REQUEST # Get the value we must compare (from request or from database) if rq.has_key(fieldName): compValue = rq.get(fieldName) compValue = compValue in ('True', 1, '1') else: compValue = dbValue # Compare the value return compValue def dateValueSelected(self, fieldName, fieldPart, dateValue, dbValue): '''When displaying a date field, must the particular p_dateValue be selected in the field corresponding to the date part?''' # Get the value we must compare (from request or from database) rq = self.REQUEST partName = '%s_%s' % (fieldName, fieldPart) if rq.has_key(partName): compValue = rq.get(partName) if compValue.isdigit(): compValue = int(compValue) else: compValue = dbValue if compValue: compValue = getattr(compValue, fieldPart)() # Compare the value return compValue == dateValue def getPossibleValues(self, name, withTranslations, withBlankValue, className=None): '''Gets the possible values for field named p_name. This field must be a String with isSelection()=True. If p_withTranslations is True, instead of returning a list of string values, the result is a list of tuples (s_value, s_translation). If p_withBlankValue is True, a blank value is prepended to the list. If no p_className is defined, the field is supposed to belong to self's class''' appyType = self.getAppyType(name, className=className) if className: # We need an instance of className, but self can be an instance of # another class. So here we will search such an instance. brains = self.executeQuery(className, maxResults=1, brainsOnly=True) if brains: obj = brains[0].getObject() else: obj = self else: obj = self return appyType.getPossibleValues(obj, withTranslations, withBlankValue) 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 = self.REQUEST if not hasattr(rq, 'appyWrappers'): rq.appyWrappers = {} # Return the Appy wrapper from rq.appyWrappers if already there uid = self.UID() if uid in rq.appyWrappers: return rq.appyWrappers[uid] # Create the Appy wrapper, cache it in rq.appyWrappers and return it wrapper = self.wrapperClass(self) rq.appyWrappers[uid] = wrapper return wrapper def _appy_showState(self, workflow, stateShow): '''Must I show a state whose "show value" is p_stateShow?''' if callable(stateShow): return stateShow(workflow, self.appy()) else: return stateShow def _appy_managePermissions(self): '''When an object is created or updated, we must update "add" permissions accordingly: if the object is a folder, we must set on it permissions that will allow to create, inside it, objects through Ref fields; if it is not a folder, we must update permissions on its parent folder instead.''' # Determine on which folder we need to set "add" permissions folder = self if not self.isPrincipiaFolderish: folder = self.getParentNode() # On this folder, set "add" permissions for every content type that will # be created through reference fields allCreators = {} # One key for every add permission addPermissions = self.getProductConfig().ADD_CONTENT_PERMISSIONS for appyType in self.getAllAppyTypes(): if appyType.type != 'Ref': continue if appyType.isBack or appyType.link: continue # Indeed, no possibility to create objects with such Ref refType = self.getTool().getPortalType(appyType.klass) if refType not in addPermissions: continue # Get roles that may add this content type creators = getattr(appyType.klass, 'creators', None) if not creators: creators = self.getProductConfig().defaultAddRoles # Add those creators to the list of creators for this meta_type addPermission = addPermissions[refType] if addPermission in allCreators: allCreators[addPermission] = allCreators[\ addPermission].union(creators) else: allCreators[addPermission] = set(creators) # Update the permissions for permission, creators in allCreators.iteritems(): updateRolesForPermission(permission, tuple(creators), folder) # Beyond content-type-specific "add" permissions, creators must also # have the main permission "Add portal content". permission = 'Add portal content' for creators in allCreators.itervalues(): updateRolesForPermission(permission, tuple(creators), folder) def _appy_getPortalType(self, request): '''Guess the portal_type of p_self from info about p_self and p_request.''' res = None # If the object is being created, self.portal_type is not correctly # initialized yet. if request.has_key('__factory__info__'): factoryInfo = request['__factory__info__'] if factoryInfo.has_key('stack'): res = factoryInfo['stack'][0] if not res: res = self.portal_type return res def _appy_getSortedField(self, fieldName): '''Gets, for reference field p_fieldName, the Appy persistent list that contains the sorted list of referred object UIDs. If this list does not exist, it is created.''' sortedFieldName = '_appy_%s' % fieldName if not hasattr(self.aq_base, sortedFieldName): pList = self.getProductConfig().PersistentList exec 'self.%s = pList()' % sortedFieldName return getattr(self, sortedFieldName) getUrlDefaults = {'page':True, 'nav':True} def getUrl(self, base=None, mode='view', **kwargs): '''Returns a Appy URL. * If p_base is None, it will be the base URL for this object (ie, 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 = '/skyn/%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'): suffix = 'skyn/%s' % mode if base.endswith(suffix): base = base[:-len(suffix)].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 getUserLanguage(self): '''Gets the language (code) of the current user.''' # Try first the "LANGUAGE" key from the request res = self.REQUEST.get('LANGUAGE', None) if res: return res # Try then 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 formatText(self, text, format='html'): '''Produces a representation of p_text into the desired p_format, which is 'html' by default.''' if format in ('html', 'xhtml'): res = text.replace('\r\n', '
').replace('\n', '
') elif format == 'js': res = text.replace('\r\n', '').replace('\n', '') res = res.replace("'", "\\'") else: res = text return res def translate(self, label, mapping={}, domain=None, default=None, language=None, format='html', field=None, className=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. If p_className is not given, we consider p_self being an instance of the class where p_field is defined.''' cfg = self.getProductConfig() if not domain: domain = cfg.PROJECTNAME if domain != cfg.PROJECTNAME: # We need to translate something that is in a standard Zope catalog try: res = self.Control_Panel.TranslationService.utranslate( domain, label, mapping, self, default=default, target_language=language) except AttributeError: # When run in test mode, Zope does not create the # TranslationService res = label else: # Get the label name, and the field-specific mapping if any. if field: # Maybe we do not have the field itself, but only its name if isinstance(field, basestring): field = self.getAppyType(field, className=className) # Include field-specific mapping if any. fieldMapping = field.mapping[label] if fieldMapping: if callable(fieldMapping): fieldMapping = field.callMethod(self, fieldMapping) mapping.update(fieldMapping) # Get the label label = getattr(field, label+'Id') # 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(): 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.get() def getPageTemplate(self, skyn, templateName): '''Returns, in the skyn folder, the page template corresponding to p_templateName.''' res = skyn for name in templateName.split('/'): res = res.get(name) return res def download(self): '''Downloads the content of the file that is in the File field named p_name.''' name = self.REQUEST.get('name') if not name: return appyType = self.getAppyType(name) if (not appyType.type =='File') or not appyType.isShowable(self,'view'): return theFile = getattr(self, name, None) if theFile: response = self.REQUEST.RESPONSE response.setHeader('Content-Disposition', 'inline;filename="%s"' % \ theFile.filename) response.setHeader('Cachecontrol', 'no-cache') response.setHeader('Expires', 'Thu, 11 Dec 1975 12:05:05 GMT') return theFile.index_html(self.REQUEST, self.REQUEST.RESPONSE) 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 # ------------------------------------------------------------------------------