'''This package contains base classes for wrappers that hide to the Appy developer the real classes used by the underlying web framework.''' # ------------------------------------------------------------------------------ import os, os.path, mimetypes import appy.pod from appy.gen import Field, Search, Ref, String, WorkflowAnonymous from appy.gen.indexer import defaultIndexes from appy.gen.utils import createObject from appy.px import Px from appy.shared.utils import getOsTempFolder, executeCommand, \ normalizeString, sequenceTypes from appy.shared.xml_parser import XmlMarshaller from appy.shared.csv_parser import CsvMarshaller # ------------------------------------------------------------------------------ class AbstractWrapper(object): '''Any real Appy-managed Zope object has a companion object that is an instance of this class.''' # Buttons for going to next/previous objects if this one is among bunch of # referenced or searched objects. currentNumber starts with 1. pxNavigateSiblings = Px('''
:ni.currentNumber // :ni.totalNumber
''') pxNavigationStrip = Px('''
:obj.pxNavigateSiblings
:phases[0].pxAllPhases''') # The template PX for all pages. pxTemplate = Px(''' :_('app_name')
:bi
:tool.pxLinks :page.title :_('app_connect')
:tool.pxMessage
:tool.pxIcons :userInfo[0]
:tool.pxPortlet
:obj.pxNavigationStrip
:content
:tool.pxFooter
''', prologue=Px.xhtmlPrologue) # -------------------------------------------------------------------------- # PXs for rendering graphical elements tied to a given object # -------------------------------------------------------------------------- # This PX displays an object's history. pxHistory = Px(''' :tool.pxNavigate
:_('object_action') :_('object_author') :_('action_date') :_('action_comment')
:_('data_change') :_(zobj.getWorkflowLabel(action)) ? :ztool.getUserName(actorId) :ztool.formatDate(event['time'], withHour=True) ::zobj.formatText(rhComments) -
:_('modified_field') :_('previous_value')
::_(field.labelId) :' (%s)' % ztool.getLanguageName(lg, True) ::change[1][0]
''') pxTransitions = Px('''
:transition.pxView :uiGroup.px
''') # Displays header information about an object: title, workflow-related info, # history... pxHeader = Px('''
:_('object_history') :_('object_created_by') :ztool.getUserName(creator) :_('object_created_on') :ztool.formatDate(creationDate, withHour=True) :_('object_modified_on') :ztool.formatDate(modificationDate, withHour=True) :_('workflow_state') : :_(zobj.getWorkflowLabel())
''') # Shows the range of buttons (next, previous, save,...) and the workflow # transitions for a given object. pxButtons = Px('''
:obj.pxTransitions
''') # Displays the fields of a given page for a given object. pxFields = Px('''
:field.pxView :field.pxRender
''') pxView = Px(''' :tool.pxPagePrologue :tool.pxLayoutedObject :tool.pxPageBottom ''', template=pxTemplate, hook='content') pxEdit = Px(''' :tool.pxPagePrologue
:tool.pxLayoutedObject
:tool.pxPageBottom
''', template=pxTemplate, hook='content') # PX called via asynchronous requests from the browser. Keys "Expires" and # "CacheControl" are used to prevent IE to cache returned pages (which is # the default IE behaviour with Ajax requests). pxAjax = Px(''' :getattr(obj, px[0]) :getattr(field, px[-1]) ''') # -------------------------------------------------------------------------- # Class methods # -------------------------------------------------------------------------- @classmethod def _getParentAttr(klass, attr): '''Gets value of p_attr on p_klass base classes (if this attr exists). Scan base classes in the reverse order as Python does. Used by classmethod m_getWorkflow below. Scanning base classes in reverse order allows user-defined elements to override default Appy elements.''' i = len(klass.__bases__) - 1 res = None while i >= 0: res = getattr(klass.__bases__[i], attr, None) if res: return res i -= 1 @classmethod def getWorkflow(klass): '''Returns the workflow tied to p_klass.''' res = klass._getParentAttr('workflow') # Return a default workflow if no workflow was found. if not res: res = WorkflowAnonymous return res @classmethod def getIndexes(klass, includeDefaults=True): '''Returns a dict whose keys are the names of the indexes that are applicable to instances of this class, and whose values are the (Zope) types of those indexes.''' # Start with the standard indexes applicable for any Appy class. if includeDefaults: res = defaultIndexes.copy() else: res = {} # Add the indexed fields found on this class for field in klass.__fields__: if not field.indexed or (field.name == 'title'): continue n = field.name indexName = 'get%s%s' % (n[0].upper(), n[1:]) res[indexName] = field.getIndexType() return res # -------------------------------------------------------------------------- # Instance methods # -------------------------------------------------------------------------- def __init__(self, o): self.__dict__['o'] = o def appy(self): return self def __setattr__(self, name, value): appyType = self.o.getAppyType(name) if not appyType: raise Exception('Attribute "%s" does not exist.' %name) appyType.store(self.o, value) def __getattribute__(self, name): '''Gets the attribute named p_name. Lot of cheating here.''' if name == 'o': return object.__getattribute__(self, name) elif name == 'tool': return self.o.getTool().appy() elif name == 'request': # The request may not be present, ie if we are at Zope startup. res = getattr(self.o, 'REQUEST', None) if res != None: return res return self.o.getProductConfig().fakeRequest elif name == 'session': return self.o.REQUEST.SESSION elif name == 'typeName': return self.__class__.__bases__[-1].__name__ elif name == 'id': return self.o.id elif name == 'uid': return self.o.id elif name == 'klass': return self.__class__.__bases__[-1] elif name == 'created': return self.o.created elif name == 'creator': return self.o.creator elif name == 'modified': return self.o.modified elif name == 'url': return self.o.absolute_url() elif name == 'state': return self.o.State() elif name == 'stateLabel': return self.o.translate(self.o.getWorkflowLabel()) elif name == 'history': o = self.o key = o.workflow_history.keys()[0] return o.workflow_history[key] elif name == 'user': return self.o.getTool().getUser() elif name == 'fields': return self.o.getAllAppyTypes() elif name == 'siteUrl': return self.o.getTool().getSiteUrl() elif name == 'initiator': return self.o.getInitiatorInfo(True) # Now, let's try to return a real attribute. res = object.__getattribute__(self, name) # If we got an Appy field, return its value for this object if isinstance(res, Field): o = self.o if isinstance(res, Ref): return res.getValue(o, noListIfSingleObj=True) else: return res.getValue(o) return res def __repr__(self): return '<%s at %s>' % (self.klass.__name__, id(self)) def __cmp__(self, other): if other: return cmp(self.o, other.o) return 1 def _getCustomMethod(self, methodName): '''See docstring of _callCustom below.''' if len(self.__class__.__bases__) > 1: # There is a custom user class custom = self.__class__.__bases__[-1] if custom.__dict__.has_key(methodName): return custom.__dict__[methodName] def _callCustom(self, methodName, *args, **kwargs): '''This wrapper implements some methods like "validate" and "onEdit". If the user has defined its own wrapper, its methods will not be called. So this method allows, from the methods here, to call the user versions.''' custom = self._getCustomMethod(methodName) if custom: return custom(self, *args, **kwargs) def getField(self, name): return self.o.getAppyType(name) def getValue(self, name, formatted=False, language=None): '''Gets the possibly p_formatted value of field p_name. If this formatting implies translating something, it will be done in p_language, or in the user language if not specified. If the "shown" value is required instead of the "formatted" value (see methods getFormattedValue and getShownValue from class appy.fields.Field), use p_formatted="shown" instead of p_formatted=True.''' field = self.o.getAppyType(name) obj = self.o val = field.getValue(obj) if not formatted: return val method = (formatted == 'shown') and 'getShownValue' or \ 'getFormattedValue' return getattr(field, method)(obj, val, language=language) def getLabel(self, name, type='field'): '''Gets the translated label of field named p_name. If p_type is "workflow", p_name denotes a workflow state or transition, not a field.''' o = self.o if type == 'field': return o.translate(o.getAppyType(name).labelId) elif type == 'workflow': return o.translate(o.getWorkflowLabel(name)) def isEmpty(self, name): '''Returns True if value of field p_name is considered as being empty.''' obj = self.o if hasattr(obj.aq_base, name): field = obj.getAppyType(name) return field.isEmptyValue(obj, getattr(obj, name)) return True def isTemp(self): '''Is this object a temporary object being created?''' return self.o.isTemporary() def link(self, fieldName, obj): '''This method links p_obj (which can be a list of objects) to this one through reference field p_fieldName.''' return self.getField(fieldName).linkObject(self, obj) def unlink(self, fieldName, obj): '''This method unlinks p_obj (which can be a list of objects) from this one through reference field p_fieldName.''' return self.getField(fieldName).unlinkObject(self, obj) def sort(self, fieldName, sortKey='title', reverse=False): '''Sorts referred elements linked to p_self via p_fieldName according to a given p_sortKey which must be an attribute set on referred objects ("title", by default).''' refs = getattr(self.o, fieldName, None) if not refs: return tool = self.tool # refs is a PersistentList: param "key" is not available. So perform the # sort on the real list and then indicate that the persistent list has # changed (the ZODB way). refs.data.sort(key=lambda x: getattr(tool.getObject(x), sortKey), reverse=reverse) refs._p_changed = 1 def create(self, fieldNameOrClass, noSecurity=False, **kwargs): '''If p_fieldNameOrClass is the name of a field, this method allows to create an object and link it to the current one (self) through reference field named p_fieldName. If p_fieldNameOrClass is a class from the gen-application, it must correspond to a root class and this method allows to create a root object in the application folder.''' isField = isinstance(fieldNameOrClass, basestring) tool = self.tool.o # Determine the class of the object to create if isField: fieldName = fieldNameOrClass appyType = self.o.getAppyType(fieldName) portalType = tool.getPortalType(appyType.klass) else: klass = fieldNameOrClass portalType = tool.getPortalType(klass) # Determine object id if kwargs.has_key('id'): objId = kwargs['id'] del kwargs['id'] else: objId = tool.generateUid(portalType) # Determine if object must be created from external data externalData = None if kwargs.has_key('_data'): externalData = kwargs['_data'] del kwargs['_data'] # Where must I create the object? if not isField: folder = tool.getPath('/data') else: folder = self.o.getCreateFolder() if not noSecurity: # Check that the user can edit this field. appyType.checkAdd(self.o) # Create the object zopeObj = createObject(folder, objId, portalType, tool.getAppName(), noSecurity=noSecurity) appyObj = zopeObj.appy() # Set object attributes for attrName, attrValue in kwargs.iteritems(): setattr(appyObj, attrName, attrValue) if isField: # Link the object to this one appyType.linkObject(self, appyObj) # Call custom initialization if externalData: param = externalData else: param = True if hasattr(appyObj, 'onEdit'): appyObj.onEdit(param) zopeObj.reindex() return appyObj def freeze(self, fieldName, template=None, format='pdf', noSecurity=True, freezeOdtOnError=True): '''This method freezes the content of pod field named p_fieldName, for the given p_template (several templates can be given in podField.template), in the given p_format ("pdf" by default).''' field = self.o.getAppyType(fieldName) if field.type!= 'Pod': raise Exception('Cannot freeze non-Pod field.') return field.freeze(self, template, format, noSecurity=noSecurity, freezeOdtOnError=freezeOdtOnError) def unfreeze(self, fieldName, template=None, format='pdf', noSecurity=True): '''This method unfreezes a pod field.''' field = self.o.getAppyType(fieldName) if field.type!= 'Pod': raise Exception('Cannot unfreeze non-Pod field.') field.unfreeze(self, template, format, noSecurity=noSecurity) def delete(self): '''Deletes myself.''' self.o.delete() def translate(self, label, mapping={}, domain=None, language=None, format='html'): '''Check documentation of self.o.translate.''' return self.o.translate(label, mapping, domain, language=language, format=format) def do(self, name, comment='', doAction=True, doNotify=True, doHistory=True, noSecurity=False): '''Triggers on p_self a transition named p_name programmatically.''' o = self.o wf = o.getWorkflow() tr = getattr(wf, name, None) if not tr or (tr.__class__.__name__ != 'Transition'): raise Exception('Transition "%s" not found.' % name) return tr.trigger(name, o, wf, comment, doAction=doAction, doNotify=doNotify, doHistory=doHistory, doSay=False, noSecurity=noSecurity) def log(self, message, type='info'): return self.o.log(message, type) def say(self, message, type='info'): return self.o.say(message, type) def normalize(self, s, usage='fileName'): '''Returns a version of string p_s whose special chars have been replaced with normal chars.''' return normalizeString(s, usage) def search(self, klass, sortBy='', sortOrder='asc', maxResults=None, noSecurity=False, **fields): '''Searches objects of p_klass. p_sortBy must be the name of an indexed field (declared with indexed=True); p_sortOrder can be "asc" (ascending, the defaut) or "desc" (descending); every param in p_fields must take the name of an indexed field and take a possible value of this field. You can optionally specify a maximum number of results in p_maxResults. If p_noSecurity is specified, you get all objects, even if the logged user does not have the permission to view it.''' # Find the content type corresponding to p_klass tool = self.tool.o contentType = tool.getPortalType(klass) # Create the Search object search = Search('customSearch', sortBy=sortBy, sortOrder=sortOrder, **fields) if not maxResults: maxResults = 'NO_LIMIT' # If I let maxResults=None, only a subset of the results will be # returned by method executeResult. res = tool.executeQuery(contentType, search=search, maxResults=maxResults, noSecurity=noSecurity) return [o.appy() for o in res.objects] def search1(self, *args, **kwargs): '''Identical to m_search above, but returns a single result (if any).''' res = self.search(*args, **kwargs) if res: return res[0] def count(self, klass, noSecurity=False, **fields): '''Identical to m_search above, but returns the number of objects that match the search instead of returning the objects themselves. Use this method instead of writing len(self.search(...)).''' tool = self.tool.o contentType = tool.getPortalType(klass) search = Search('customSearch', **fields) res = tool.executeQuery(contentType, search=search, brainsOnly=True, noSecurity=noSecurity, maxResults='NO_LIMIT') if res: return res._len # It is a LazyMap instance else: return 0 def ids(self, fieldName): '''Returns the identifiers of the objects linked to this one via field name p_fieldName. WARNING: do not modify this list, it is the true list that is stored in the database (excepted if empty). Modifying it will probably corrupt the database.''' return getattr(self.o.aq_base, fieldName, ()) def countRefs(self, fieldName): '''Counts the number of objects linked to this one via Ref field p_fieldName.''' uids = getattr(self.o.aq_base, fieldName, None) if not uids: return 0 return len(uids) def compute(self, klass, sortBy='', maxResults=None, context=None, expression=None, noSecurity=False, **fields): '''This method, like m_search and m_count above, performs a query on objects of p_klass. But in this case, instead of returning a list of matching objects (like m_search) or counting elements (like p_count), it evaluates, on every matching object, a Python p_expression (which may be an expression or a statement), and returns, if needed, a result. The result may be initialized through parameter p_context. p_expression is evaluated with 2 variables in its context: "obj" which is the currently walked object, instance of p_klass, and "ctx", which is the context as initialized (or not) by p_context. p_context may be used as (1) a variable or instance that is updated on every call to produce a result; (2) an input variable or instance; (3) both. The method returns p_context, modified or not by evaluation of p_expression on every matching object. When you need to perform an action or computation on a lot of objects, use this method instead of doing things like "for obj in self.search(MyClass,...)" ''' tool = self.tool.o contentType = tool.getPortalType(klass) search = Search('customSearch', sortBy=sortBy, **fields) # Initialize the context variable "ctx" ctx = context for brain in tool.executeQuery(contentType, search=search, \ brainsOnly=True, maxResults=maxResults, noSecurity=noSecurity): # Get the Appy object from the brain if noSecurity: method = '_unrestrictedGetObject' else: method = 'getObject' exec 'obj = brain.%s().appy()' % method exec expression return ctx def reindex(self, fields=None, unindex=False): '''Asks a direct object reindexing. In most cases you don't have to reindex objects "manually" with this method. When an object is modified after some user action has been performed, Appy reindexes this object automatically. But if your code modifies other objects, Appy may not know that they must be reindexed, too. So use this method in those cases. ''' if fields: # Get names of indexes from field names. indexes = [Search.getIndexName(name) for name in fields] else: indexes = None self.o.reindex(indexes=indexes, unindex=unindex) def export(self, at='string', format='xml', include=None, exclude=None): '''Creates an "exportable" version of this object. p_format is "xml" by default, but can also be "csv". If p_format is: * "xml", if p_at is "string", this method returns the XML version, without the XML prologue. Else, (a) if not p_at, the XML will be exported on disk, in the OS temp folder, with an ugly name; (b) else, it will be exported at path p_at. * "csv", if p_at is "string", this method returns the CSV data as a string. If p_at is an opened file handler, the CSV line will be appended in it. If p_include is given, only fields whose names are in it will be included. p_exclude, if given, contains names of fields that will not be included in the result. ''' if format == 'xml': # Todo: take p_include and p_exclude into account. # Determine where to put the result toDisk = (at != 'string') if toDisk and not at: at = getOsTempFolder() + '/' + self.o.id + '.xml' # Create the XML version of the object marshaller = XmlMarshaller(cdata=True, dumpUnicode=True, dumpXmlPrologue=toDisk, rootTag=self.klass.__name__) xml = marshaller.marshall(self.o, objectType='appy') # Produce the desired result if toDisk: f = file(at, 'w') f.write(xml.encode('utf-8')) f.close() return at else: return xml elif format == 'csv': if isinstance(at, basestring): marshaller = CsvMarshaller(include=include, exclude=exclude) return marshaller.marshall(self) else: marshaller = CsvMarshaller(at, include=include, exclude=exclude) marshaller.marshall(self) def historize(self, data): '''This method allows to add "manually" a "data-change" event into the object's history. Indeed, data changes are "automatically" recorded only when an object is edited through the edit form, not when a setter is called from the code. p_data must be a dictionary whose keys are field names (strings) and whose values are the previous field values.''' self.o.addDataChange(data) def getLastEvent(self, transition, notBefore=None): '''Gets, from the object history, the last occurrence of transition named p_transition. p_transition can be a list of names: in this case, it returns the most recent occurrence of those transitions. If p_notBefore is given, it corresponds to a kind of start transition for the search: we will not search in the history preceding the last occurrence of this transition.''' history = self.history i = len(history)-1 while i >= 0: event = history[i] if notBefore and (event['action'] == notBefore): return if isinstance(transition, basestring): condition = event['action'] == transition else: condition = event['action'] in transition if condition: return event i -= 1 def formatText(self, text, format='html'): '''Produces a representation of p_text into the desired p_format, which is 'html' by default.''' return self.o.formatText(text, format) def listStates(self): '''Lists the possible states for this object.''' res = [] o = self.o workflow = o.getWorkflow() for name in dir(workflow): if getattr(workflow, name).__class__.__name__ != 'State': continue res.append((name, o.translate(o.getWorkflowLabel(name)))) return res def path(self, name): '''Returns the absolute file name of file stored in File field p_nnamed p_name.''' v = getattr(self, name) if v: return v.getFilePath(self) def getIndexOf(self, name, obj): '''Returns, as an integer starting at 0, the position of p_obj within objects linked to p_self via field p_name.''' o = self.o return o.getAppyType(name).getIndexOf(o, obj.uid) def allows(self, permission, raiseError=False): '''Check doc @Mixin.allows.''' return self.o.allows(permission, raiseError=raiseError) def resetLocalRoles(self): '''Removes all local roles defined on this object, excepted local role Owner, granted to the item creator.''' from persistent.mapping import PersistentMapping localRoles = PersistentMapping({ self.o.creator: ['Owner'] }) self.o.__ac_local_roles__ = localRoles return localRoles def raiseUnauthorized(self, msg=None): return self.o.raiseUnauthorized(msg) # ------------------------------------------------------------------------------