class ElementSpec(Persistent): """ Represent all the tool knows about a single metadata element. """ # # Default values. # is_multi_valued = 0 def __init__(self, is_multi_valued): self.is_multi_valued = is_multi_valued self.policies = PersistentMapping() self.policies[None] = self._makePolicy() # set default policy def _makePolicy(self): return MetadataElementPolicy(self.is_multi_valued) def isMultiValued(self): """ Is this element multi-valued? """ return self.is_multi_valued def getPolicy(self, typ=None): """ Find the policy this element for objects whose type object name is 'typ'; return a default, if none found. """ try: return self.policies[typ] except KeyError: return self.policies[None] def listPolicies(self): """ Return a list of all policies for this element. """ return self.policies.items() def addPolicy(self, typ): """ Add a policy to this element for objects whose type object name is 'typ'. """ if typ is None: raise MetadataError, "Can't replace default policy." if self.policies.has_key(typ): raise MetadataError, "Existing policy for content type:" + typ self.policies[typ] = self._makePolicy() def removePolicy(self, typ): """ Remove the policy from this element for objects whose type object name is 'typ' (*not* the default, however). """ if typ is None: raise MetadataError, "Can't remove default policy." del self.policies[typ]
class Resource(Persistent): security = ClassSecurityInfo() def __init__(self, id, **kwargs): self._data = PersistentMapping() self._data['id'] = id self._data['expression'] = kwargs.get('expression', '') self._data['enabled'] = kwargs.get('enabled', True) self._data['cookable'] = kwargs.get('cookable', True) self._data['cacheable'] = kwargs.get('cacheable', True) def copy(self): result = self.__class__(self.getId()) for key, value in self._data.items(): if key != 'id': result._data[key] = value return result security.declarePublic('getId') def getId(self): return self._data['id'] def _setId(self, id): self._data['id'] = id security.declarePublic('getExpression') def getExpression(self): return self._data['expression'] security.declareProtected(permissions.ManagePortal, 'setExpression') def setExpression(self, expression): self._data['expression'] = expression security.declarePublic('getEnabled') def getEnabled(self): return bool(self._data['enabled']) security.declareProtected(permissions.ManagePortal, 'setEnabled') def setEnabled(self, enabled): self._data['enabled'] = enabled security.declarePublic('getCookable') def getCookable(self): return self._data['cookable'] security.declareProtected(permissions.ManagePortal, 'setCookable') def setCookable(self, cookable): self._data['cookable'] = cookable security.declarePublic('getCacheable') def getCacheable(self): # as this is a new property, old instance might not have that value, so # return True as default return self._data.get('cacheable', True) security.declareProtected(permissions.ManagePortal, 'setCacheable') def setCacheable(self, cacheable): self._data['cacheable'] = cacheable
class DiscussionItemContainer( Persistent, Implicit, Traversable ): """ Store DiscussionItem objects. Discussable content that has DiscussionItems associated with it will have an instance of DiscussionItemContainer injected into it to hold the discussion threads. """ __implements__ = Discussable # for the security machinery to allow traversal #__roles__ = None security = ClassSecurityInfo() def __init__(self): self.id = 'talkback' self._container = PersistentMapping() security.declareProtected(View, 'getId') def getId( self ): return self.id security.declareProtected(View, 'getReply') def getReply( self, reply_id ): """ Return a discussion item, given its ID; raise KeyError if not found. """ return self._container.get( reply_id ).__of__(self) # Is this right? security.declareProtected(View, '__bobo_traverse__') def __bobo_traverse__(self, REQUEST, name): """ This will make this container traversable """ target = getattr(self, name, None) if target is not None: return target else: try: return self.getReply(name) except: parent = aq_parent( aq_inner( self ) ) if parent.getId() == name: return parent else: REQUEST.RESPONSE.notFoundError("%s\n%s" % (name, '')) security.declarePrivate('manage_afterAdd') def manage_afterAdd(self, item, container): """ We have juste been added or moved. Add the contained items to the catalog. """ if aq_base(container) is not aq_base(self): for obj in self.objectValues(): obj.__of__(self).manage_afterAdd(item, container) security.declarePrivate('manage_afterClone') def manage_afterClone(self, item): """ We have just been cloned. Notify the workflow about the contained items. """ for obj in self.objectValues(): obj.__of__(self).manage_afterClone(item) security.declarePrivate( 'manage_beforeDelete' ) def manage_beforeDelete(self, item, container): """ Remove the contained items from the catalog. """ if aq_base(container) is not aq_base(self): for obj in self.objectValues(): obj.__of__( self ).manage_beforeDelete( item, container ) # # OFS.ObjectManager query interface. # security.declareProtected(AccessContentsInformation, 'objectIds') def objectIds( self, spec=None ): """ Return a list of the ids of our DiscussionItems. """ if spec and spec is not DiscussionItem.meta_type: return [] return self._container.keys() security.declareProtected(AccessContentsInformation, 'objectItems') def objectItems(self, spec=None): """ Return a list of (id, subobject) tuples for our DiscussionItems. """ r=[] a=r.append g=self._container.get for id in self.objectIds(spec): a( (id, g( id ) ) ) return r security.declareProtected(AccessContentsInformation, 'objectValues') def objectValues(self): """ Return a list of our DiscussionItems. """ return self._container.values() # # Discussable interface # security.declareProtected(ReplyToItem, 'createReply') def createReply( self, title, text, Creator=None, text_format='structured-text' ): """ Create a reply in the proper place """ container = self._container id = int(DateTime().timeTime()) while self._container.get( str(id), None ) is not None: id = id + 1 id = str( id ) item = DiscussionItem( id, title=title, description=title ) item._edit( text_format=text_format, text=text ) item.__of__(self).addCreator(Creator) item.__of__(self).indexObject() item.setReplyTo( self._getDiscussable() ) item.__of__(self).notifyWorkflowCreated() self._container[ id ] = item return id security.declareProtected(ManagePortal, 'deleteReply') def deleteReply( self, reply_id ): """ Remove a reply from this container """ if self._container.has_key( reply_id ): reply = self._container.get( reply_id ).__of__( self ) my_replies = reply.talkback.getReplies() for my_reply in my_replies: my_reply_id = my_reply.getId() if hasattr( my_reply, 'unindexObject' ): my_reply.unindexObject() del self._container[my_reply_id] if hasattr( reply, 'unindexObject' ): reply.unindexObject() del self._container[reply_id] security.declareProtected(View, 'hasReplies') def hasReplies( self, content_obj ): """ Test to see if there are any dicussion items """ outer = self._getDiscussable( outer=1 ) if content_obj == outer: return not not len( self._container ) else: return not not len( content_obj.talkback._getReplyResults() ) security.declareProtected(View, 'replyCount') def replyCount( self, content_obj ): """ How many replies do i have? """ outer = self._getDiscussable( outer=1 ) if content_obj == outer: return len( self._container ) else: replies = content_obj.talkback.getReplies() return self._repcount( replies ) security.declarePrivate('_repcount') def _repcount( self, replies ): """ counts the total number of replies by recursing thru the various levels """ count = 0 for reply in replies: count = count + 1 #if there is at least one reply to this reply replies = reply.talkback.getReplies() if replies: count = count + self._repcount( replies ) return count security.declareProtected(View, 'getReplies') def getReplies( self ): """ Return a sequence of the DiscussionResponse objects which are associated with this Discussable """ objects = [] a = objects.append result_ids = self._getReplyResults() for id in result_ids: a( self._container.get( id ).__of__( self ) ) return objects security.declareProtected(View, 'quotedContents') def quotedContents(self): """ Return this object's contents in a form suitable for inclusion as a quote in a response. """ return "" # # Utility methods # security.declarePrivate( '_getReplyParent' ) def _getReplyParent( self, in_reply_to ): """ Return the object indicated by the 'in_reply_to', where 'None' represents the "outer" content object. """ outer = self._getDiscussable( outer=1 ) if in_reply_to is None: return outer parent = self._container[ in_reply_to ].__of__( aq_inner( self ) ) return parent.__of__( outer ) security.declarePrivate( '_getDiscussable' ) def _getDiscussable( self, outer=0 ): """ """ tb = outer and aq_inner( self ) or self return getattr( tb, 'aq_parent', None ) security.declarePrivate( '_getReplyResults' ) def _getReplyResults( self ): """ Get a list of ids of DiscussionItems which are replies to our Discussable. """ discussable = self._getDiscussable() outer = self._getDiscussable( outer=1 ) if discussable == outer: in_reply_to = None else: in_reply_to = discussable.getId() result = [] a = result.append for key, value in self._container.items(): if value.in_reply_to == in_reply_to: a( ( key, value ) ) result.sort( lambda a, b: cmp(a[1].creation_date, b[1].creation_date) ) return [ x[0] for x in result ]
class DiscussionItemContainer(Persistent, Implicit, Traversable): """ Store DiscussionItem objects. Discussable content that has DiscussionItems associated with it will have an instance of DiscussionItemContainer injected into it to hold the discussion threads. """ __implements__ = Discussable # for the security machinery to allow traversal #__roles__ = None security = ClassSecurityInfo() def __init__(self): self.id = 'talkback' self._container = PersistentMapping() security.declareProtected(View, 'getId') def getId(self): return self.id security.declareProtected(View, 'getReply') def getReply(self, reply_id): """ Return a discussion item, given its ID; raise KeyError if not found. """ return self._container.get(reply_id).__of__(self) # Is this right? security.declareProtected(View, '__bobo_traverse__') def __bobo_traverse__(self, REQUEST, name): """ This will make this container traversable """ target = getattr(self, name, None) if target is not None: return target else: try: return self.getReply(name) except: parent = aq_parent(aq_inner(self)) if parent.getId() == name: return parent else: REQUEST.RESPONSE.notFoundError("%s\n%s" % (name, '')) security.declarePrivate('manage_afterAdd') def manage_afterAdd(self, item, container): """ We have juste been added or moved. Add the contained items to the catalog. """ if aq_base(container) is not aq_base(self): for obj in self.objectValues(): obj.__of__(self).manage_afterAdd(item, container) security.declarePrivate('manage_afterClone') def manage_afterClone(self, item): """ We have just been cloned. Notify the workflow about the contained items. """ for obj in self.objectValues(): obj.__of__(self).manage_afterClone(item) security.declarePrivate('manage_beforeDelete') def manage_beforeDelete(self, item, container): """ Remove the contained items from the catalog. """ if aq_base(container) is not aq_base(self): for obj in self.objectValues(): obj.__of__(self).manage_beforeDelete(item, container) # # OFS.ObjectManager query interface. # security.declareProtected(AccessContentsInformation, 'objectIds') def objectIds(self, spec=None): """ Return a list of the ids of our DiscussionItems. """ if spec and spec is not DiscussionItem.meta_type: return [] return self._container.keys() security.declareProtected(AccessContentsInformation, 'objectItems') def objectItems(self, spec=None): """ Return a list of (id, subobject) tuples for our DiscussionItems. """ r = [] a = r.append g = self._container.get for id in self.objectIds(spec): a((id, g(id))) return r security.declareProtected(AccessContentsInformation, 'objectValues') def objectValues(self): """ Return a list of our DiscussionItems. """ return self._container.values() # # Discussable interface # security.declareProtected(ReplyToItem, 'createReply') def createReply(self, title, text, Creator=None): """ Create a reply in the proper place """ container = self._container id = int(DateTime().timeTime()) while self._container.get(str(id), None) is not None: id = id + 1 id = str(id) item = DiscussionItem(id, title=title, description=title) item._edit(text_format='structured-text', text=text) if Creator: item.creator = Creator item.__of__(self).indexObject() item.setReplyTo(self._getDiscussable()) self._container[id] = item return id security.declareProtected(ManagePortal, 'deleteReply') def deleteReply(self, reply_id): """ Remove a reply from this container """ if self._container.has_key(reply_id): reply = self._container.get(reply_id).__of__(self) my_replies = reply.talkback.getReplies() for my_reply in my_replies: my_reply_id = my_reply.getId() if hasattr(my_reply, 'unindexObject'): my_reply.unindexObject() del self._container[my_reply_id] if hasattr(reply, 'unindexObject'): reply.unindexObject() del self._container[reply_id] security.declareProtected(View, 'hasReplies') def hasReplies(self, content_obj): """ Test to see if there are any dicussion items """ outer = self._getDiscussable(outer=1) if content_obj == outer: return not not len(self._container) else: return not not len(content_obj.talkback._getReplyResults()) security.declareProtected(View, 'replyCount') def replyCount(self, content_obj): """ How many replies do i have? """ outer = self._getDiscussable(outer=1) if content_obj == outer: return len(self._container) else: replies = content_obj.talkback.getReplies() return self._repcount(replies) security.declarePrivate('_repcount') def _repcount(self, replies): """ counts the total number of replies by recursing thru the various levels """ count = 0 for reply in replies: count = count + 1 #if there is at least one reply to this reply replies = reply.talkback.getReplies() if replies: count = count + self._repcount(replies) return count security.declareProtected(View, 'getReplies') def getReplies(self): """ Return a sequence of the DiscussionResponse objects which are associated with this Discussable """ objects = [] a = objects.append result_ids = self._getReplyResults() for id in result_ids: a(self._container.get(id).__of__(self)) return objects security.declareProtected(View, 'quotedContents') def quotedContents(self): """ Return this object's contents in a form suitable for inclusion as a quote in a response. """ return "" # # Utility methods # security.declarePrivate('_getReplyParent') def _getReplyParent(self, in_reply_to): """ Return the object indicated by the 'in_reply_to', where 'None' represents the "outer" content object. """ outer = self._getDiscussable(outer=1) if in_reply_to is None: return outer parent = self._container[in_reply_to].__of__(aq_inner(self)) return parent.__of__(outer) security.declarePrivate('_getDiscussable') def _getDiscussable(self, outer=0): """ """ tb = outer and aq_inner(self) or self return getattr(tb, 'aq_parent', None) security.declarePrivate('_getReplyResults') def _getReplyResults(self): """ Get a list of ids of DiscussionItems which are replies to our Discussable. """ discussable = self._getDiscussable() outer = self._getDiscussable(outer=1) if discussable == outer: in_reply_to = None else: in_reply_to = discussable.getId() result = [] a = result.append for key, value in self._container.items(): if value.in_reply_to == in_reply_to: a(key) return result
class MetadataSchema( SimpleItem ): """ Describe a metadata schema. """ security = ClassSecurityInfo() meta_type = 'Metadata Schema' publisher = '' def __init__( self, id, element_specs=() ): self._setId( id ) self.element_specs = PersistentMapping() for name, is_multi_valued in element_specs: self.element_specs[ name ] = ElementSpec( is_multi_valued ) # # ZMI methods # manage_options = ( ( { 'label' : 'Elements' , 'action' : 'elementPoliciesForm' } , ) + SimpleItem.manage_options ) security.declareProtected(ManagePortal, 'elementPoliciesForm') elementPoliciesForm = DTMLFile( 'metadataElementPolicies', _dtmldir ) security.declareProtected(ManagePortal, 'addElementPolicy') def addElementPolicy( self , element , content_type , is_required , supply_default , default_value , enforce_vocabulary , allowed_vocabulary , REQUEST=None ): """ Add a type-specific policy for one of our elements. """ if content_type == '<default>': content_type = None spec = self.getElementSpec( element ) spec.addPolicy( content_type ) policy = spec.getPolicy( content_type ) policy.edit( is_required , supply_default , default_value , enforce_vocabulary , allowed_vocabulary ) if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+added.' ) security.declareProtected(ManagePortal, 'removeElementPolicy') def removeElementPolicy( self , element , content_type , REQUEST=None ): """ Remvoe a type-specific policy for one of our elements. """ if content_type == '<default>': content_type = None spec = self.getElementSpec( element ) spec.removePolicy( content_type ) if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+removed.' ) security.declareProtected(ManagePortal, 'updateElementPolicy') def updateElementPolicy( self , element , content_type , is_required , supply_default , default_value , enforce_vocabulary , allowed_vocabulary , REQUEST=None ): """ Update a policy for one of our elements o 'content_type' will be '<default>' when we edit the default. """ if content_type == '<default>': content_type = None spec = self.getElementSpec( element ) policy = spec.getPolicy( content_type ) policy.edit( is_required , supply_default , default_value , enforce_vocabulary , allowed_vocabulary ) if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+updated.' ) # # Element spec manipulation. # security.declareProtected(ManagePortal, 'listElementSpecs') def listElementSpecs( self ): """ Return a list of ElementSpecs representing the elements we manage. """ res = [] for k, v in self.element_specs.items(): res.append((k, v.__of__(self))) return res security.declareProtected(ManagePortal, 'getElementSpec') def getElementSpec( self, element ): """ Return an ElementSpec for the given 'element'. """ return self.element_specs[ element ].__of__( self ) security.declareProtected(ManagePortal, 'addElementSpec') def addElementSpec( self, element, is_multi_valued, REQUEST=None ): """ Add 'element' to our list of managed elements. """ # Don't replace. if self.element_specs.has_key( element ): return self.element_specs[ element ] = ElementSpec( is_multi_valued ) if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/propertiesForm' + '?manage_tabs_message=Element+' + element + '+added.' ) security.declareProtected(ManagePortal, 'removeElementSpec') def removeElementSpec( self, element, REQUEST=None ): """ Remove 'element' from our list of managed elements. """ del self.element_specs[ element ] if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/propertiesForm' + '?manage_tabs_message=Element+' + element + '+removed.' ) security.declareProtected(ManagePortal, 'listPolicies') def listPolicies( self, typ=None ): """ Show all policies for a given content type o If 'typ' is none, return the list of default policies. """ result = [] for element, spec in self.listElementSpecs(): result.append( ( element, spec.getPolicy( typ ) ) ) return result
class MetadataTool(UniqueObject, SimpleItem): id = 'portal_metadata' meta_type = 'Default Metadata Tool' security = ClassSecurityInfo() # # Default values. # publisher = '' element_specs = None #initial_values_hook = None #validation_hook = None def __init__( self, publisher=None #, initial_values_hook=None #, validation_hook=None , element_specs=DEFAULT_ELEMENT_SPECS): self.editProperties(publisher #, initial_values_hook #, validation_hook ) self.element_specs = PersistentMapping() for name, is_multi_valued in element_specs: self.element_specs[name] = ElementSpec(is_multi_valued) # # ZMI methods # manage_options = (( { 'label': 'Overview', 'action': 'manage_overview' }, { 'label': 'Properties', 'action': 'propertiesForm' }, { 'label': 'Elements', 'action': 'elementPoliciesForm' } # TODO , { 'label' : 'Types' # , 'action' : 'typesForm' # } ) + SimpleItem.manage_options) security.declareProtected(CMFCorePermissions.ManagePortal, 'manage_overview') manage_overview = DTMLFile('explainMetadataTool', _dtmldir) security.declareProtected(CMFCorePermissions.ManagePortal, 'propertiesForm') propertiesForm = DTMLFile('metadataProperties', _dtmldir) security.declareProtected(CMFCorePermissions.ManagePortal, 'editProperties') def editProperties( self, publisher=None # TODO , initial_values_hook=None # TODO , validation_hook=None , REQUEST=None): """ Form handler for "tool-wide" properties (including list of metadata elements). """ if publisher is not None: self.publisher = publisher # TODO self.initial_values_hook = initial_values_hook # TODO self.validation_hook = validation_hook if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url() + '/propertiesForm' + '?manage_tabs_message=Tool+updated.') security.declareProtected(CMFCorePermissions.ManagePortal, 'elementPoliciesForm') elementPoliciesForm = DTMLFile('metadataElementPolicies', _dtmldir) security.declareProtected(CMFCorePermissions.ManagePortal, 'addElementPolicy') def addElementPolicy(self, element, content_type, is_required, supply_default, default_value, enforce_vocabulary, allowed_vocabulary, REQUEST): """ Add a type-specific policy for one of our elements. """ if content_type == '<default>': content_type = None spec = self.getElementSpec(element) spec.addPolicy(content_type) policy = spec.getPolicy(content_type) policy.edit(is_required, supply_default, default_value, enforce_vocabulary, allowed_vocabulary) if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+added.') security.declareProtected(CMFCorePermissions.ManagePortal, 'removeElementPolicy') def removeElementPolicy(self, element, content_type, REQUEST): """ Remvoe a type-specific policy for one of our elements. """ if content_type == '<default>': content_type = None spec = self.getElementSpec(element) spec.removePolicy(content_type) if REQUEST is not None: REQUEST['RESPONSE'].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+removed.') security.declareProtected(CMFCorePermissions.ManagePortal, 'updateElementPolicy') def updateElementPolicy(self, element, content_type, is_required, supply_default, default_value, enforce_vocabulary, allowed_vocabulary, REQUEST=None): """ Update a policy for one of our elements ('content_type' will be '<default>' when we edit the default). """ if content_type == '<default>': content_type = None spec = self.getElementSpec(element) policy = spec.getPolicy(content_type) policy.edit(is_required, supply_default, default_value, enforce_vocabulary, allowed_vocabulary) if REQUEST is not None: REQUEST['RESPONSE'].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+updated.') # # Element spec manipulation. # security.declareProtected(CMFCorePermissions.ManagePortal, 'listElementSpecs') def listElementSpecs(self): """ Return a list of ElementSpecs representing the elements managed by the tool. """ return tuple(self.element_specs.items()) security.declareProtected(CMFCorePermissions.ManagePortal, 'getElementSpec') def getElementSpec(self, element): """ Return an ElementSpec representing the tool's knowledge of 'element'. """ return self.element_specs[element] security.declareProtected(CMFCorePermissions.ManagePortal, 'addElementSpec') def addElementSpec(self, element, is_multi_valued): """ Add 'element' to our list of managed elements. """ # Don't replace. if self.element_specs.has_key(element): return self.element_specs[element] = ElementSpec(is_multi_valued) security.declareProtected(CMFCorePermissions.ManagePortal, 'removeElementSpec') def removeElementSpec(self, element): """ Remove 'element' from our list of managed elements. """ del self.element_specs[element] security.declareProtected('listPolicies') def listPolicies(self, typ=None): """ Show all policies for a given content type, or the default if None. """ result = [] for element, spec in self.listElementSpecs(): result.append((element, spec.getPolicy(typ))) return result # # 'portal_metadata' interface # security.declarePrivate('getFullName') def getFullName(self, userid): """ Convert an internal userid to a "formal" name, if possible, perhaps using the 'portal_membership' tool. Used to map userid's for Creator, Contributor DCMI queries. """ return userid # TODO: do lookup here security.declarePublic('getPublisher') def getPublisher(self): """ Return the "formal" name of the publisher of the portal. """ return self.publisher security.declarePrivate('_listAllowedVocabulary') def _listAllowedVocabulary(self, element, content=None): """ List allowed keywords for a given meta_type, or all possible keywords if none supplied. """ spec = self.getElementSpec(element) typ = content and content.Type() or None return spec.getPolicy(typ).allowedVocabulary() security.declarePublic('listAllowedSubjects') def listAllowedSubjects(self, content=None): """ List allowed keywords for a given meta_type, or all possible keywords if none supplied. """ return self._listAllowedVocabulary('Subject', content) security.declarePublic('listAllowedFormats') def listAllowedFormats(self, content=None): """ List the allowed 'Content-type' values for a particular meta_type, or all possible formats if none supplied. """ return self._listAllowedVocabulary('Format', content) security.declarePublic('listAllowedLanguages') def listAllowedLanguages(self, content=None): """ List the allowed language values. """ return self._listAllowedVocabulary('Language', content) security.declarePublic('listAllowedRights') def listAllowedRights(self, content=None): """ List the allowed values for a "Rights" selection list; this gets especially important where syndication is involved. """ return self._listAllowedVocabulary('Rights', content) security.declarePrivate('setInitialMetadata') def setInitialMetadata(self, content): """ Set initial values for content metatdata, supplying any site-specific defaults. """ for element, policy in self.listPolicies(content.Type()): if not getattr(content, element)(): if policy.supplyDefault(): setter = getattr(content, 'set%s' % element) setter(policy.defaultValue()) elif policy.isRequired(): raise MetadataError, \ 'Metadata element %s is required.' % element # TODO: Call initial_values_hook, if present security.declarePrivate('validateMetadata') def validateMetadata(self, content): """ Enforce portal-wide policies about DCI, e.g., requiring non-empty title/description, etc. Called by the CMF immediately before saving changes to the metadata of an object. """ for element, policy in self.listPolicies(content.Type()): value = getattr(content, element)() if not value and policy.isRequired(): raise MetadataError, \ 'Metadata element %s is required.' % element if policy.enforceVocabulary(): values = policy.isMultiValued() and value or [value] for value in values: if not value in policy.allowedValues(): raise MetadataError, \ 'Value %s is not in allowed vocabulary for' \ 'metadata element %s.' % ( value, element )
class ElementSpec( Persistent ): """ Represent all the tool knows about a single metadata element. """ # # Default values. # is_multi_valued = 0 def __init__( self, is_multi_valued ): self.is_multi_valued = is_multi_valued self.policies = PersistentMapping() self.policies[ None ] = self._makePolicy() # set default policy def _makePolicy( self ): return MetadataElementPolicy( self.is_multi_valued ) def isMultiValued( self ): """ Is this element multi-valued? """ return self.is_multi_valued def getPolicy( self, typ=None ): """ Find the policy this element for objects whose type object name is 'typ'; return a default, if none found. """ try: return self.policies[ typ ] except KeyError: return self.policies[ None ] def listPolicies( self ): """ Return a list of all policies for this element. """ return self.policies.items() def addPolicy( self, typ ): """ Add a policy to this element for objects whose type object name is 'typ'. """ if typ is None: raise MetadataError, "Can't replace default policy." if self.policies.has_key( typ ): raise MetadataError, "Existing policy for content type:" + typ self.policies[ typ ] = self._makePolicy() def removePolicy( self, typ ): """ Remove the policy from this element for objects whose type object name is 'typ' (*not* the default, however). """ if typ is None: raise MetadataError, "Can't remove default policy." del self.policies[ typ ]
class DiscussionItemContainer( Persistent, Implicit, Traversable ): """ Store DiscussionItem objects. Discussable content that has DiscussionItems associated with it will have an instance of DiscussionItemContainer injected into it to hold the discussion threads. """ # for the security machinery to allow traversal #__roles__ = None security = ClassSecurityInfo() def __init__(self): self.id = 'talkback' self._container = PersistentMapping() security.declareProtected( CMFCorePermissions.View, 'getId' ) def getId( self ): return self.id # Is this right? security.declareProtected( CMFCorePermissions.View, '__bobo_traverse__' ) def __bobo_traverse__(self, REQUEST, name): """ This will make this container traversable """ target = getattr(self, name, None) if target is not None: return target else: try: return self._container.get(name).__of__(self) except: REQUEST.RESPONSE.notFoundError("%s\n%s" % (name, '')) security.declarePrivate( 'manage_beforeDelete' ) def manage_beforeDelete(self, item, container): """ Remove the contained items from the catalog. """ if aq_base(container) is not aq_base(self): for obj in self.objectValues(): obj.__of__( self ).manage_beforeDelete( item, container ) # # OFS.ObjectManager query interface. # security.declareProtected( CMFCorePermissions.AccessContentsInformation , 'objectIds' ) def objectIds( self, spec=None ): """ Return a list of the ids of our DiscussionItems. """ if spec and spec is not DiscussionItem.meta_type: return [] return self._container.keys() security.declareProtected( CMFCorePermissions.AccessContentsInformation , 'objectItems' ) def objectItems(self, spec=None): """ Return a list of (id, subobject) tuples for our DiscussionItems. """ r=[] a=r.append g=self._container.get for id in self.objectIds(spec): a( (id, g( id ) ) ) return r security.declareProtected( CMFCorePermissions.AccessContentsInformation , 'objectValues' ) def objectValues(self): """ Return a list of our DiscussionItems. """ return self._container.values() # # Discussable interface # security.declareProtected( CMFCorePermissions.ReplyToItem, 'createReply' ) def createReply( self, title, text, Creator=None ): """ Create a reply in the proper place """ container = self._container id = int(DateTime().timeTime()) while self._container.get( str(id), None ) is not None: id = id + 1 id = str( id ) item = DiscussionItem( id, title=title, description=title ) item._edit( text_format='structured-text', text=text ) if Creator: item.creator = Creator item.__of__( self ).indexObject() item.setReplyTo( self._getDiscussable() ) self._container[ id ] = item return id security.declareProtected( CMFCorePermissions.View, 'hasReplies' ) def hasReplies( self ): """ Test to see if there are any dicussion items """ if len(self._container) == 0: return 0 return len( self._getReplyResults() ) security.declareProtected( CMFCorePermissions.View, 'getReplies' ) def getReplies( self ): """ Return a sequence of the DiscussionResponse objects which are associated with this Discussable """ objects = [] a = objects.append result_ids = self._getReplyResults() for id in result_ids: a( self._container.get( id ).__of__( self ) ) return objects security.declareProtected( CMFCorePermissions.View, 'quotedContents' ) def quotedContents(self): """ Return this object's contents in a form suitable for inclusion as a quote in a response. """ return "" # # Utility methods # security.declarePrivate( '_getReplyParent' ) def _getReplyParent( self, in_reply_to ): """ Return the object indicated by the 'in_reply_to', where 'None' represents the "outer" content object. """ outer = self._getDiscussable( outer=1 ) if in_reply_to is None: return outer parent = self._container[ in_reply_to ].__of__( aq_inner( self ) ) return parent.__of__( outer ) security.declarePrivate( '_getDiscussable' ) def _getDiscussable( self, outer=0 ): """ """ tb = outer and aq_inner( self ) or self return getattr( tb, 'aq_parent', None ) security.declarePrivate( '_getReplyResults' ) def _getReplyResults( self ): """ Get a list of ids of DiscussionItems which are replies to our Discussable. """ discussable = self._getDiscussable() outer = self._getDiscussable( outer=1 ) if discussable == outer: in_reply_to = None else: in_reply_to = discussable.getId() result = [] a = result.append for key, value in self._container.items(): if value.in_reply_to == in_reply_to: a( key ) return result
class MetadataSchema(SimpleItem): """ Describe a metadata schema. """ security = ClassSecurityInfo() meta_type = 'Metadata Schema' publisher = '' def __init__(self, id, element_specs=()): self._setId(id) self.element_specs = PersistentMapping() for name, is_multi_valued in element_specs: self.element_specs[name] = ElementSpec(is_multi_valued) # # ZMI methods # manage_options = (({ 'label': 'Elements', 'action': 'elementPoliciesForm' }, ) + SimpleItem.manage_options) security.declareProtected(ManagePortal, 'elementPoliciesForm') elementPoliciesForm = DTMLFile('metadataElementPolicies', _dtmldir) security.declareProtected(ManagePortal, 'addElementPolicy') def addElementPolicy(self, element, content_type, is_required, supply_default, default_value, enforce_vocabulary, allowed_vocabulary, REQUEST=None): """ Add a type-specific policy for one of our elements. """ if content_type == '<default>': content_type = None spec = self.getElementSpec(element) spec.addPolicy(content_type) policy = spec.getPolicy(content_type) policy.edit(is_required, supply_default, default_value, enforce_vocabulary, allowed_vocabulary) if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+added.') security.declareProtected(ManagePortal, 'removeElementPolicy') def removeElementPolicy(self, element, content_type, REQUEST=None): """ Remvoe a type-specific policy for one of our elements. """ if content_type == '<default>': content_type = None spec = self.getElementSpec(element) spec.removePolicy(content_type) if REQUEST is not None: REQUEST['RESPONSE'].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+removed.') security.declareProtected(ManagePortal, 'updateElementPolicy') def updateElementPolicy(self, element, content_type, is_required, supply_default, default_value, enforce_vocabulary, allowed_vocabulary, REQUEST=None): """ Update a policy for one of our elements o 'content_type' will be '<default>' when we edit the default. """ if content_type == '<default>': content_type = None spec = self.getElementSpec(element) policy = spec.getPolicy(content_type) policy.edit(is_required, supply_default, default_value, enforce_vocabulary, allowed_vocabulary) if REQUEST is not None: REQUEST['RESPONSE'].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+updated.') # # Element spec manipulation. # security.declareProtected(ManagePortal, 'listElementSpecs') def listElementSpecs(self): """ Return a list of ElementSpecs representing the elements we manage. """ res = [] for k, v in self.element_specs.items(): res.append((k, v.__of__(self))) return res security.declareProtected(ManagePortal, 'getElementSpec') def getElementSpec(self, element): """ Return an ElementSpec for the given 'element'. """ return self.element_specs[element].__of__(self) security.declareProtected(ManagePortal, 'addElementSpec') def addElementSpec(self, element, is_multi_valued, REQUEST=None): """ Add 'element' to our list of managed elements. """ # Don't replace. if self.element_specs.has_key(element): return self.element_specs[element] = ElementSpec(is_multi_valued) if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url() + '/propertiesForm' + '?manage_tabs_message=Element+' + element + '+added.') security.declareProtected(ManagePortal, 'removeElementSpec') def removeElementSpec(self, element, REQUEST=None): """ Remove 'element' from our list of managed elements. """ del self.element_specs[element] if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url() + '/propertiesForm' + '?manage_tabs_message=Element+' + element + '+removed.') security.declareProtected(ManagePortal, 'listPolicies') def listPolicies(self, typ=None): """ Show all policies for a given content type o If 'typ' is none, return the list of default policies. """ result = [] for element, spec in self.listElementSpecs(): result.append((element, spec.getPolicy(typ))) return result
class DiscussionItemContainer(Persistent, Implicit, Traversable): """ Store DiscussionItem objects. Discussable content that has DiscussionItems associated with it will have an instance of DiscussionItemContainer injected into it to hold the discussion threads. """ # for the security machinery to allow traversal #__roles__ = None security = ClassSecurityInfo() def __init__(self): self.id = 'talkback' self._container = PersistentMapping() security.declareProtected(CMFCorePermissions.View, 'getId') def getId(self): return self.id # Is this right? security.declareProtected(CMFCorePermissions.View, '__bobo_traverse__') def __bobo_traverse__(self, REQUEST, name): """ This will make this container traversable """ target = getattr(self, name, None) if target is not None: return target else: try: return self._container.get(name).__of__(self) except: REQUEST.RESPONSE.notFoundError("%s\n%s" % (name, '')) security.declarePrivate('manage_beforeDelete') def manage_beforeDelete(self, item, container): """ Remove the contained items from the catalog. """ if aq_base(container) is not aq_base(self): for obj in self.objectValues(): obj.__of__(self).manage_beforeDelete(item, container) # # OFS.ObjectManager query interface. # security.declareProtected(CMFCorePermissions.AccessContentsInformation, 'objectIds') def objectIds(self, spec=None): """ Return a list of the ids of our DiscussionItems. """ if spec and spec is not DiscussionItem.meta_type: return [] return self._container.keys() security.declareProtected(CMFCorePermissions.AccessContentsInformation, 'objectItems') def objectItems(self, spec=None): """ Return a list of (id, subobject) tuples for our DiscussionItems. """ r = [] a = r.append g = self._container.get for id in self.objectIds(spec): a((id, g(id))) return r security.declareProtected(CMFCorePermissions.AccessContentsInformation, 'objectValues') def objectValues(self): """ Return a list of our DiscussionItems. """ return self._container.values() # # Discussable interface # security.declareProtected(CMFCorePermissions.ReplyToItem, 'createReply') def createReply(self, title, text, Creator=None): """ Create a reply in the proper place """ container = self._container id = int(DateTime().timeTime()) while self._container.get(str(id), None) is not None: id = id + 1 id = str(id) item = DiscussionItem(id, title=title, description=title) item._edit(text_format='structured-text', text=text) if Creator: item.creator = Creator item.__of__(self).indexObject() item.setReplyTo(self._getDiscussable()) self._container[id] = item return id security.declareProtected(CMFCorePermissions.View, 'hasReplies') def hasReplies(self): """ Test to see if there are any dicussion items """ if len(self._container) == 0: return 0 return len(self._getReplyResults()) security.declareProtected(CMFCorePermissions.View, 'getReplies') def getReplies(self): """ Return a sequence of the DiscussionResponse objects which are associated with this Discussable """ objects = [] a = objects.append result_ids = self._getReplyResults() for id in result_ids: a(self._container.get(id).__of__(self)) return objects security.declareProtected(CMFCorePermissions.View, 'quotedContents') def quotedContents(self): """ Return this object's contents in a form suitable for inclusion as a quote in a response. """ return "" # # Utility methods # security.declarePrivate('_getReplyParent') def _getReplyParent(self, in_reply_to): """ Return the object indicated by the 'in_reply_to', where 'None' represents the "outer" content object. """ outer = self._getDiscussable(outer=1) if in_reply_to is None: return outer parent = self._container[in_reply_to].__of__(aq_inner(self)) return parent.__of__(outer) security.declarePrivate('_getDiscussable') def _getDiscussable(self, outer=0): """ """ tb = outer and aq_inner(self) or self return getattr(tb, 'aq_parent', None) security.declarePrivate('_getReplyResults') def _getReplyResults(self): """ Get a list of ids of DiscussionItems which are replies to our Discussable. """ discussable = self._getDiscussable() outer = self._getDiscussable(outer=1) if discussable == outer: in_reply_to = None else: in_reply_to = discussable.getId() result = [] a = result.append for key, value in self._container.items(): if value.in_reply_to == in_reply_to: a(key) return result
class MessageCatalog(LanguageManager, ObjectManager, SimpleItem): """Stores messages and their translations... """ meta_type = "MessageCatalog" security = ClassSecurityInfo() def __init__(self, id, title, sourcelang, languages): self.id = id self.title = title # Language Manager data self._languages = tuple(languages) self._default_language = sourcelang # Here the message translations are stored self._messages = PersistentMapping() # Data for the PO files headers self._po_headers = PersistentMapping() for lang in self._languages: self._po_headers[lang] = empty_po_header ####################################################################### # Private API ####################################################################### def get_message_key(self, message): if message in self._messages: return message # A message may be stored as unicode or byte string encoding = HTTPRequest.default_encoding if isinstance(message, unicode): message = message.encode(encoding) else: message = unicode(message, encoding) if message in self._messages: return message def get_translations(self, message): message = self.get_message_key(message) return self._messages[message] ####################################################################### # Public API ####################################################################### security.declarePublic("message_exists") def message_exists(self, message): """ """ return self._messages.has_key(message) security.declareProtected("Manage messages", "message_edit") def message_edit(self, message, language, translation, note): """ """ self._messages[message][language] = translation self._messages[message]["note"] = note security.declareProtected("Manage messages", "message_del") def message_del(self, message): """ """ del self._messages[message] security.declarePublic("gettext") def gettext(self, message, lang=None, add=1, default=None): """Returns the message translation from the database if available. If add=1, add any unknown message to the database. If a default is provided, use it instead of the message id as a translation for unknown messages. """ if not isinstance(message, (str, unicode)): raise TypeError, "only strings can be translated." message = message.strip() # assume empty message is always translated as empty message if not message: return message if default is None: default = message # Add it if it's not in the dictionary if add and not self._messages.has_key(message) and message: update_transaction_note() self._messages[message] = PersistentMapping() if message and not self._messages[message].has_key("en"): self._messages[message]["en"] = default # Get the string if self._messages.has_key(message): m = self._messages[message] if lang is None: # Builds the list of available languages # should the empty translations be filtered? available_languages = list(self._languages) # Imagine that the default language is 'en'. There is no # translation from 'en' to 'en' in the message catalog # The user has the preferences 'en' and 'nl' in that order # The next two lines make certain 'en' is shown, not 'nl' if not self._default_language in available_languages: available_languages.append(self._default_language) # Get the language! lang = lang_negotiator(available_languages) # Is it None? use the default if lang is None: lang = self._default_language if lang is not None: return m.get(lang) or default return default __call__ = gettext def translate(self, domain, msgid, *args, **kw): """This method is required to get the i18n namespace from ZPT working. """ default = kw.get("default") if default is not None and not default.strip(): default = None msgstr = self.gettext(msgid, default=default) mapping = kw.get("mapping") return interpolate(msgstr, mapping) ####################################################################### # Management screens ####################################################################### def manage_options(self): """ """ options = ( ( {"label": u"Messages", "action": "manage_messages", "help": ("Localizer", "MC_messages.stx")}, {"label": u"Properties", "action": "manage_propertiesForm"}, {"label": u"Import", "action": "manage_Import_form", "help": ("Localizer", "MC_importExport.stx")}, {"label": u"Export", "action": "manage_Export_form", "help": ("Localizer", "MC_importExport.stx")}, ) + LanguageManager.manage_options + SimpleItem.manage_options ) r = [] for option in options: option = option.copy() option["label"] = _(option["label"]) r.append(option) return r ####################################################################### # Management screens -- Messages ####################################################################### security.declareProtected("Manage messages", "manage_messages") manage_messages = LocalDTMLFile("ui/MC_messages", globals()) security.declarePublic("get_namespace") def get_namespace(self, REQUEST): """For the management interface, allows to filter the messages to show. """ # Check whether there are languages or not languages = self.get_languages_mapping() if not languages: return {} # Input batch_start = REQUEST.get("batch_start", 0) batch_size = REQUEST.get("batch_size", 15) empty = REQUEST.get("empty", 0) regex = REQUEST.get("regex", "") message = REQUEST.get("msg", None) # Build the namespace namespace = {} namespace["batch_size"] = batch_size namespace["empty"] = empty namespace["regex"] = regex # The language lang = REQUEST.get("lang", None) or languages[0]["code"] namespace["language"] = lang # Filter the messages query = regex.strip() try: query = compile(query) except: query = compile("") messages = [] for m, t in self._messages.items(): if query.search(m) and (not empty or not t.get(lang, "").strip()): messages.append(m) messages.sort(filter_sort) # How many messages n = len(messages) namespace["n_messages"] = n # Calculate the start while batch_start >= n: batch_start = batch_start - batch_size if batch_start < 0: batch_start = 0 namespace["batch_start"] = batch_start # Select the batch to show batch_end = batch_start + batch_size messages = messages[batch_start:batch_end] # Batch links namespace["previous"] = get_url(REQUEST.URL, batch_start - batch_size, batch_size, regex, lang, empty) namespace["next"] = get_url(REQUEST.URL, batch_start + batch_size, batch_size, regex, lang, empty) # Get the message message_encoded = None translations = {} if message is None: if messages: message = messages[0] translations = self.get_translations(message) message = to_unicode(message) message_encoded = message_encode(message) else: message_encoded = message message = message_decode(message_encoded) translations = self.get_translations(message) message = to_unicode(message) namespace["message"] = message namespace["message_encoded"] = message_encoded namespace["translations"] = translations namespace["translation"] = translations.get(lang, "") namespace["note"] = translations.get("note", "") # Calculate the current message namespace["messages"] = [] for x in messages: x = to_unicode(x) x_encoded = message_encode(x) url = get_url(REQUEST.URL, batch_start, batch_size, regex, lang, empty, msg=x_encoded) namespace["messages"].append( {"message": x, "message_encoded": x_encoded, "current": x == message, "url": url} ) # The languages for language in languages: code = language["code"] language["name"] = _(language["name"], language=code) language["url"] = get_url(REQUEST.URL, batch_start, batch_size, regex, code, empty, msg=message_encoded) namespace["languages"] = languages return namespace security.declareProtected("Manage messages", "manage_editMessage") def manage_editMessage(self, message, language, translation, note, REQUEST, RESPONSE): """Modifies a message. """ message_encoded = message message = message_decode(message_encoded) message_key = self.get_message_key(message) self.message_edit(message_key, language, translation, note) url = get_url( REQUEST.URL1 + "/manage_messages", REQUEST["batch_start"], REQUEST["batch_size"], REQUEST["regex"], REQUEST.get("lang", ""), REQUEST.get("empty", 0), msg=message_encoded, manage_tabs_message=_(u"Saved changes."), ) RESPONSE.redirect(url) security.declareProtected("Manage messages", "manage_delMessage") def manage_delMessage(self, message, REQUEST, RESPONSE): """ """ message = message_decode(message) message_key = self.get_message_key(message) self.message_del(message_key) url = get_url( REQUEST.URL1 + "/manage_messages", REQUEST["batch_start"], REQUEST["batch_size"], REQUEST["regex"], REQUEST.get("lang", ""), REQUEST.get("empty", 0), manage_tabs_message=_(u"Saved changes."), ) RESPONSE.redirect(url) ####################################################################### # Management screens -- Properties # Management screens -- Import/Export # FTP access ####################################################################### security.declareProtected("View management screens", "manage_propertiesForm") manage_propertiesForm = LocalDTMLFile("ui/MC_properties", globals()) security.declareProtected("View management screens", "manage_properties") def manage_properties(self, title, REQUEST=None, RESPONSE=None): """Change the Message Catalog properties. """ self.title = title if RESPONSE is not None: RESPONSE.redirect("manage_propertiesForm") # Properties management screen security.declareProtected("View management screens", "get_po_header") def get_po_header(self, lang): """ """ # For backwards compatibility if not hasattr(aq_base(self), "_po_headers"): self._po_headers = PersistentMapping() return self._po_headers.get(lang, empty_po_header) security.declareProtected("View management screens", "update_po_header") def update_po_header( self, lang, last_translator_name=None, last_translator_email=None, language_team=None, charset=None, REQUEST=None, RESPONSE=None, ): """ """ header = self.get_po_header(lang) if last_translator_name is None: last_translator_name = header["last_translator_name"] if last_translator_email is None: last_translator_email = header["last_translator_email"] if language_team is None: language_team = header["language_team"] if charset is None: charset = header["charset"] header = { "last_translator_name": last_translator_name, "last_translator_email": last_translator_email, "language_team": language_team, "charset": charset, } self._po_headers[lang] = header if RESPONSE is not None: RESPONSE.redirect("manage_propertiesForm") security.declareProtected("View management screens", "manage_Import_form") manage_Import_form = LocalDTMLFile("ui/MC_Import_form", globals()) security.declarePublic("get_charsets") def get_charsets(self): """ """ return charsets[:] security.declarePublic("manage_export") def manage_export(self, x, REQUEST=None, RESPONSE=None): """Exports the content of the message catalog either to a template file (locale.pot) or to an language specific PO file (<x>.po). """ # Get the PO header info header = self.get_po_header(x) last_translator_name = header["last_translator_name"] last_translator_email = header["last_translator_email"] language_team = header["language_team"] charset = header["charset"] # PO file header, empty message. po_revision_date = strftime("%Y-%m-%d %H:%m+%Z", gmtime(time())) pot_creation_date = po_revision_date last_translator = "%s <%s>" % (last_translator_name, last_translator_email) if x == "locale.pot": language_team = "LANGUAGE <*****@*****.**>" else: language_team = "%s <%s>" % (x, language_team) r = [ 'msgid ""', 'msgstr "Project-Id-Version: %s\\n"' % self.title, '"POT-Creation-Date: %s\\n"' % pot_creation_date, '"PO-Revision-Date: %s\\n"' % po_revision_date, '"Last-Translator: %s\\n"' % last_translator, '"Language-Team: %s\\n"' % language_team, '"MIME-Version: 1.0\\n"', '"Content-Type: text/plain; charset=%s\\n"' % charset, '"Content-Transfer-Encoding: 8bit\\n"', "", "", ] # Get the messages, and perhaps its translations. d = {} if x == "locale.pot": filename = x for k in self._messages.keys(): d[k] = "" else: filename = "%s.po" % x for k, v in self._messages.items(): try: d[k] = v[x] except KeyError: d[k] = "" # Generate the file def backslashescape(x): quote_esc = compile(r'"') x = quote_esc.sub('\\"', x) trans = [("\n", "\\n"), ("\r", "\\r"), ("\t", "\\t")] for a, b in trans: x = x.replace(a, b) return x # Generate sorted msgids to simplify diffs dkeys = d.keys() dkeys.sort() for k in dkeys: r.append('msgid "%s"' % backslashescape(k)) v = d[k] r.append('msgstr "%s"' % backslashescape(v)) r.append("") if RESPONSE is not None: RESPONSE.setHeader("Content-type", "application/data") RESPONSE.setHeader("Content-Disposition", "inline;filename=%s" % filename) r2 = [] for x in r: if isinstance(x, unicode): r2.append(x.encode(charset)) else: r2.append(x) return "\n".join(r2) security.declareProtected("Manage messages", "po_import") def po_import(self, lang, data): """ """ messages = self._messages # Load the data po = POFile(string=data) for msgid in po.get_msgids(): if msgid: msgstr = po.get_msgstr(msgid) or "" if not messages.has_key(msgid): messages[msgid] = PersistentMapping() messages[msgid][lang] = msgstr # Set the encoding (the full header should be loaded XXX) self.update_po_header(lang, charset=po.get_encoding()) security.declareProtected("Manage messages", "manage_import") def manage_import(self, lang, file, REQUEST=None, RESPONSE=None): """ """ # XXX For backwards compatibility only, use "po_import" instead. if isinstance(file, str): content = file else: content = file.read() self.po_import(lang, content) if RESPONSE is not None: RESPONSE.redirect("manage_messages") def objectItems(self, spec=None): """ """ for lang in self._languages: if not hasattr(aq_base(self), lang): self._setObject(lang, POFile(lang)) r = MessageCatalog.inheritedAttribute("objectItems")(self, spec) return r ####################################################################### # TMX support security.declareProtected("View management screens", "manage_Export_form") manage_Export_form = LocalDTMLFile("ui/MC_Export_form", globals()) security.declareProtected("Manage messages", "tmx_export") def tmx_export(self, REQUEST, RESPONSE=None): """Exports the content of the message catalog to a TMX file """ orglang = self._default_language # Get the header info header = self.get_po_header(orglang) charset = header["charset"] # build data structure for the xml header xml_header = {} xml_header["standalone"] = -1 xml_header["xml_version"] = u"1.0" xml_header["document_type"] = (u"tmx", u"http://www.lisa.org/tmx/tmx14.dtd") # build data structure for the tmx header version = u"1.4" tmx_header = {} tmx_header["creationtool"] = u"Localizer" tmx_header["creationtoolversion"] = u"1.x" tmx_header["datatype"] = u"plaintext" tmx_header["segtype"] = u"paragraph" tmx_header["adminlang"] = u"%s" % orglang tmx_header["srclang"] = u"%s" % orglang tmx_header["o-encoding"] = u"%s" % charset.lower() # handle messages d = {} filename = "%s.tmx" % self.id for msgkey, transunit in self._messages.items(): sentences = {} for lang in transunit.keys(): if lang != "note": s = Sentence(transunit[lang], {"lang": "%s" % lang}) sentences[lang] = s if orglang not in transunit.keys(): s = Sentence(msgkey, {"lang": "%s" % orglang}) sentences[orglang] = s if transunit.has_key("note"): d[msgkey] = Message(sentences, {}, [Note(transunit.get("note"))]) else: d[msgkey] = Message(sentences) tmx = TMX() tmx.build(xml_header, version, tmx_header, d) if RESPONSE is not None: RESPONSE.setHeader("Content-type", "application/data") RESPONSE.setHeader("Content-Disposition", 'attachment; filename="%s"' % filename) return tmx.to_str() security.declareProtected("Manage messages", "tmx_import") def tmx_import(self, howmuch, file, REQUEST=None, RESPONSE=None): """Imports a TMX level 1 file. """ try: data = file.read() tmx = TMX(string=data) except: return MessageDialog( title="Parse error", message=_("impossible to parse the file"), action="manage_Import_form" ) num_notes = 0 num_trans = 0 if howmuch == "clear": # Clear the message catalogue prior to import self._messages = {} self._languages = () self._default_language = tmx.get_srclang() for (id, msg) in tmx.state.messages.items(): if not self._messages.has_key(id) and howmuch == "existing": pass else: msg.msgstr.pop(self._default_language) if not self._messages.has_key(id): self._messages[id] = {} for lang in msg.msgstr.keys(): # normalize the languageTag and extract the core (core, local) = LanguageTag.decode(lang) lang = LanguageTag.encode((core, local)) if lang not in self._languages: self._languages += (lang,) if msg.msgstr[lang].text: self._messages[id][lang] = msg.msgstr[lang].text if core != lang and core != self._default_language: if core not in self._languages: self._languages += (core,) if not msg.msgstr.has_key(core): self._messages[id][core] = msg.msgstr[lang].text if msg.notes: ns = [m.text for m in msg.notes] self._messages[id]["note"] = u" ".join(ns) num_notes += 1 num_trans += 1 if REQUEST is not None: return MessageDialog( title=_(u"Messages imported"), message=_(u"Imported %d messages and %d notes") % (num_trans, num_notes), action="manage_messages", ) ####################################################################### # Backwards compatibility (XXX) ####################################################################### hasmsg = message_exists hasLS = message_exists # CMFLocalizer uses it security.declareProtected("Manage messages", "xliff_export") def xliff_export(self, x, export_all=1, REQUEST=None, RESPONSE=None): """Exports the content of the message catalog to an XLIFF file """ orglang = self._default_language export_all = int(export_all) from DateTime import DateTime # Generate the XLIFF file header RESPONSE.setHeader("Content-Type", "text/xml; charset=UTF-8") RESPONSE.setHeader("Content-Disposition", 'attachment; filename="%s_%s_%s.xlf"' % (self.id, orglang, x)) # build data structure for the xml header xml_header = {} xml_header["standalone"] = -1 xml_header["xml_version"] = u"1.0" xml_header["document_type"] = (u"xliff", u"http://www.oasis-open.org/committees/xliff/documents/xliff.dtd") version = u"1.0" # build the data-stucture for the File tag attributes = {} attributes["original"] = u"/%s" % self.absolute_url(1) attributes["product-name"] = u"Localizer" attributes["product-version"] = u"1.1.x" attributes["data-type"] = u"plaintext" attributes["source-language"] = orglang attributes["target-language"] = x attributes["date"] = DateTime().HTML4() # Get the messages, and perhaps its translations. d = {} for msgkey, transunit in self._messages.items(): target = transunit.get(x, "") # if export_all=1 export all messages otherwise export # only untranslated messages if export_all or not target: id = md5text(msgkey) notes = [] if transunit.has_key("note") and transunit["note"]: notes = [xliff_Note(transunit["note"])] if target: t = Translation(msgkey, target, {"id": id}, notes) else: t = Translation(msgkey, msgkey, {"id": id}, notes) d[msgkey] = t files = [xliff_File(d, attributes)] xliff = XLIFF() xliff.build(xml_header, version, files) return xliff.to_str() security.declareProtected("Manage messages", "xliff_import") def xliff_import(self, howmuch, file, REQUEST=None): """XLIFF is the XML Localization Interchange File Format designed by a group of software providers. It is specified by www.oasis-open.org """ try: data = file.read() xliff = XLIFF(string=data) except: return MessageDialog( title="Parse error", message=_("impossible to parse the file"), action="manage_Import_form" ) num_notes = 0 num_trans = 0 (file_ids, sources, targets) = xliff.get_languages() if howmuch == "clear": # Clear the message catalogue prior to import self._messages = {} self._languages = () self._default_language = sources[0] # update languages if len(sources) > 1 or sources[0] != self._default_language: return MessageDialog( title="Language error", message=_("incompatible language sources"), action="manage_Import_form" ) for lang in targets: if lang != self._default_language and lang not in self._languages: self._languages += (lang,) # get messages for file in xliff.state.files: cur_target = file.attributes.get("target-language", "") for msg in file.body.keys(): if not self._messages.has_key(msg) and howmuch == "existing": pass else: if not self._messages.has_key(msg): self._messages[msg] = {} if cur_target and file.body[msg].target: self._messages[msg][cur_target] = file.body[msg].target num_trans += 1 if file.body[msg].notes: ns = [n.text for n in file.body[msg].notes] comment = " ".join(ns) self._messages[msg]["note"] = comment num_notes += 1 if REQUEST is not None: return MessageDialog( title=_(u"Messages imported"), message=(_(u"Imported %d messages and %d notes to %s") % (num_trans, num_notes, " ".join(targets))), action="manage_messages", )
class ArchetypeTool(UniqueObject, ActionProviderBase, \ SQLStorageConfig, Folder): """Archetypes tool, manage aspects of Archetype instances. """ id = TOOL_NAME meta_type = TOOL_NAME.title().replace('_', ' ') implements(IArchetypeTool) isPrincipiaFolderish = True # Show up in the ZMI security = ClassSecurityInfo() meta_types = all_meta_types = () manage_options = ( ( { 'label' : 'Types', 'action' : 'manage_debugForm', }, { 'label' : 'Catalogs', 'action' : 'manage_catalogs', }, { 'label' : 'Templates', 'action' : 'manage_templateForm', }, { 'label' : 'UIDs', 'action' : 'manage_uids', }, { 'label' : 'Update Schema', 'action' : 'manage_updateSchemaForm', }, { 'label' : 'Migration', 'action' : 'manage_migrationForm', }, ) + SQLStorageConfig.manage_options ) security.declareProtected(permissions.ManagePortal, 'manage_uids') manage_uids = PageTemplateFile('viewContents', _www) security.declareProtected(permissions.ManagePortal, 'manage_templateForm') manage_templateForm = PageTemplateFile('manageTemplates',_www) security.declareProtected(permissions.ManagePortal, 'manage_debugForm') manage_debugForm = PageTemplateFile('generateDebug', _www) security.declareProtected(permissions.ManagePortal, 'manage_updateSchemaForm') manage_updateSchemaForm = PageTemplateFile('updateSchemaForm', _www) security.declareProtected(permissions.ManagePortal, 'manage_migrationForm') manage_migrationForm = PageTemplateFile('migrationForm', _www) security.declareProtected(permissions.ManagePortal, 'manage_dumpSchemaForm') manage_dumpSchemaForm = PageTemplateFile('schema', _www) security.declareProtected(permissions.ManagePortal, 'manage_catalogs') manage_catalogs = PageTemplateFile('manage_catalogs', _www) def __init__(self): self._schemas = PersistentMapping() self._templates = PersistentMapping() self._registeredTemplates = PersistentMapping() # meta_type -> [names of CatalogTools] self.catalog_map = PersistentMapping() self.catalog_map['Reference'] = [] # References not in portal_catalog # DM (avoid persistency bug): "_types" now maps known schemas to signatures self._types = {} security.declareProtected(permissions.ManagePortal, 'manage_dumpSchema') def manage_dumpSchema(self, REQUEST=None): """XML Dump Schema of passed in class. """ from Products.Archetypes.Schema import getSchemata package = REQUEST.get('package', '') type_name = REQUEST.get('type_name', '') spec = self.getTypeSpec(package, type_name) type = self.lookupType(package, type_name) options = {} options['classname'] = spec options['schematas'] = getSchemata(type['klass']) REQUEST.RESPONSE.setHeader('Content-Type', 'text/xml') return self.manage_dumpSchemaForm(**options) # Template Management # Views can be pretty generic by iterating the schema so we don't # register by type anymore, we just create per site selection # lists # # We keep two lists, all register templates and their # names/titles and the mapping of type to template bindings both # are persistent security.declareProtected(permissions.ManagePortal, 'registerTemplate') def registerTemplate(self, template, name=None): # Lookup the template by name if not name: obj = self.unrestrictedTraverse(template, None) try: name = obj.title_or_id() except: name = template self._registeredTemplates[template] = name security.declareProtected(permissions.View, 'lookupTemplates') def lookupTemplates(self, instance_or_portaltype=None): """Lookup templates by giving an instance or a portal_type. Returns a DisplayList. """ results = [] if not isinstance(instance_or_portaltype, basestring): portal_type = instance_or_portaltype.getTypeInfo().getId() else: portal_type = instance_or_portaltype try: templates = self._templates[portal_type] except KeyError: return DisplayList() # XXX Look this up in the types tool later # self._templates[instance] = ['base_view',] # templates = self._templates[instance] for t in templates: results.append((t, self._registeredTemplates[t])) return DisplayList(results).sortedByValue() security.declareProtected(permissions.View, 'listTemplates') def listTemplates(self): """Lists all the templates. """ return DisplayList(self._registeredTemplates.items()).sortedByValue() security.declareProtected(permissions.ManagePortal, 'bindTemplate') def bindTemplate(self, portal_type, templateList): """Creates binding between a type and its associated views. """ self._templates[portal_type] = templateList security.declareProtected(permissions.ManagePortal, 'manage_templates') def manage_templates(self, REQUEST=None): """Sets all the template/type mappings. """ prefix = 'template_names_' for key in REQUEST.form.keys(): if key.startswith(prefix): k = key[len(prefix):] v = REQUEST.form.get(key) self.bindTemplate(k, v) add = REQUEST.get('addTemplate') name = REQUEST.get('newTemplate') if add and name: self.registerTemplate(name) return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_templateForm') security.declareProtected(permissions.View, 'typeImplementsInterfaces') def typeImplementsInterfaces(self, type, interfaces): """Checks if an type uses one of the given interfaces. """ if isinstance(type, dict) and type.has_key('klass'): type = type['klass'] for iface in interfaces: res = iface.isImplementedByInstancesOf(type) if res: return True return False security.declareProtected(permissions.View, 'isTemplateEnabled') def isTemplateEnabled(self, type): """Checks if an type uses ITemplateMixin. """ return self.typeImplementsInterfaces(type, [ITemplateMixin]) security.declareProtected(permissions.View, 'listTemplateEnabledPortalTypes') def listTemplateEnabledPortalTypes(self): """Return a list of portal_types with ITemplateMixin """ return self.listPortalTypesWithInterfaces([ITemplateMixin]) security.declareProtected(permissions.View, 'listPortalTypesWithInterfaces') def listPortalTypesWithInterfaces(self, ifaces): """Returns a list of ftis of which the types implement one of the given interfaces. Only returns AT types. Get a list of FTIs of types implementing IReferenceable: >>> tool = getToolByName(self.portal, TOOL_NAME) >>> meth = tool.listPortalTypesWithInterfaces >>> ftis = tool.listPortalTypesWithInterfaces([IReferenceable]) Sort the type ids and print them: >>> type_ids = [fti.getId() for fti in ftis] >>> type_ids.sort() >>> type_ids ['ATBIFolder', 'ComplexType', ...] """ pt = getToolByName(self, 'portal_types') value = [] for data in listTypes(): klass = data['klass'] for iface in ifaces: if iface.isImplementedByInstancesOf(klass): ti = pt.getTypeInfo(data['portal_type']) if ti is not None: value.append(ti) return value # Type/Schema Management security.declareProtected(permissions.View, 'listRegisteredTypes') def listRegisteredTypes(self, inProject=False, portalTypes=False): """Return the list of sorted types. """ def type_sort(a, b): v = cmp(a['package'], b['package']) if v != False: return v c = cmp(a['klass'].__class__.__name__, b['klass'].__class__.__name__) if c == False: return cmp(a['package'], b['package']) return c values = listTypes() values.sort(type_sort) if inProject: # portal_type can change (as it does after ATCT-migration), so we # need to check against the content_meta_type of each type-info ttool = getToolByName(self, 'portal_types') types = [ti.Metatype() for ti in ttool.listTypeInfo()] if portalTypes: values = [v for v in values if v['portal_type'] in types] else: values = [v for v in values if v['meta_type'] in types] return values security.declareProtected(permissions.View, 'getTypeSpec') def getTypeSpec(self, package, type): t = self.lookupType(package, type) module = t['klass'].__module__ klass = t['name'] return '%s.%s' % (module, klass) security.declareProtected(permissions.View, 'listTypes') def listTypes(self, package=None, type=None): """Just the class. """ if type is None: return [t['klass'] for t in listTypes(package)] else: return [getType(type, package)['klass']] security.declareProtected(permissions.View, 'lookupType') def lookupType(self, package, type): types = self.listRegisteredTypes() for t in types: if t['package'] != package: continue if t['meta_type'] == type: # We have to return the schema wrapped into the acquisition of # something to allow access. Otherwise we will end up with: # Your user account is defined outside the context of the object # being accessed. t['schema'] = ImplicitAcquisitionWrapper(t['schema'], self) return t return None security.declareProtected(permissions.ManagePortal, 'manage_installType') def manage_installType(self, typeName, package=None, uninstall=None, REQUEST=None): """Un/Install a type TTW. """ typesTool = getToolByName(self, 'portal_types') try: typesTool._delObject(typeName) except (ConflictError, KeyboardInterrupt): raise except: # XXX bare exception pass if uninstall is not None: if REQUEST: return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_debugForm') return typeinfo_name = '%s: %s' % (package, typeName) # We want to run the process/modify_fti code which might not # have been called typeDesc = getType(typeName, package) process_types([typeDesc], package) klass = typeDesc['klass'] # get the meta type of the FTI from the class, use the default FTI as default fti_meta_type = getattr(klass, '_at_fti_meta_type', None) if fti_meta_type in (None, 'simple item'): fti_meta_type = FactoryTypeInformation.meta_type typesTool.manage_addTypeInformation(fti_meta_type, id=typeName, typeinfo_name=typeinfo_name) t = getattr(typesTool, typeName, None) if t: t.title = getattr(klass, 'archetype_name', typeDesc['portal_type']) # and update the actions as needed fixActionsForType(klass, typesTool) if REQUEST: return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_debugForm') security.declarePublic('getSearchWidgets') def getSearchWidgets(self, package=None, type=None, context=None, nosort=None): """Empty widgets for searching. """ return self.getWidgets(package=package, type=type, context=context, mode='search', nosort=nosort) security.declarePublic('getWidgets') def getWidgets(self, instance=None, package=None, type=None, context=None, mode='edit', fields=None, schemata=None, nosort=None): """Empty widgets for standalone rendering. """ widgets = [] w_keys = {} context = context is not None and context or self instances = instance is not None and [instance] or [] f_names = fields if not instances: for t in self.listTypes(package, type): instance = t('fake_instance') instance._at_is_fake_instance = True wrapped = instance.__of__(context) wrapped.initializeArchetype() instances.append(wrapped) for instance in instances: if schemata is not None: schema = instance.Schemata()[schemata].copy() else: schema = instance.Schema().copy() fields = schema.fields() if mode == 'search': # Include only fields which have an index # XXX duplicate fieldnames may break this, # as different widgets with the same name # on different schemas means only the first # one found will be used indexes=self.portal_catalog.indexes() fields = [f for f in fields if (f.accessor and not w_keys.has_key(f.accessor) and f.accessor in indexes)] if f_names is not None: fields = filter(lambda f: f.getName() in f_names, fields) for field in fields: widget = field.widget field_name = field.getName() accessor = field.getAccessor(instance) if mode == 'search': field.required = False field.addable = False # for ReferenceField if not isinstance(field.vocabulary, DisplayList): field.vocabulary = field.Vocabulary(instance) if '' not in field.vocabulary.keys(): field.vocabulary = DisplayList([('', _(u'at_search_any', default=u'<any>'))]) + \ field.vocabulary widget.populate = False field_name = field.accessor # accessor must be a method which doesn't take an argument # this lambda is facking an accessor accessor = lambda: field.getDefault(instance) w_keys[field_name] = None widgets.append((field_name, WidgetWrapper( field_name=field_name, mode=mode, widget=widget, instance=instance, field=field, accessor=accessor))) if mode == 'search' and nosort == None: widgets.sort() return [widget for name, widget in widgets] security.declarePrivate('_rawEnum') def _rawEnum(self, callback, *args, **kwargs): """Finds all object to check if they are 'referenceable'. """ catalog = getToolByName(self, 'portal_catalog') brains = catalog(id=[]) for b in brains: o = b.getObject() if o is not None: if IBaseObject.isImplementedBy(o): callback(o, *args, **kwargs) else: log('no object for brain: %s:%s' % (b,b.getURL())) security.declareProtected(permissions.View, 'enum') def enum(self, callback, *args, **kwargs): catalog = getToolByName(self, UID_CATALOG) keys = catalog.uniqueValuesFor('UID') for uid in keys: o = self.getObject(uid) if o: callback(o, *args, **kwargs) else: log('No object for %s' % uid) security.declareProtected(permissions.View, 'Content') def Content(self): """Return a list of all the content ids. """ catalog = getToolByName(self, UID_CATALOG) keys = catalog.uniqueValuesFor('UID') results = catalog(UID=keys) return results ## Management Forms security.declareProtected(permissions.ManagePortal, 'manage_doGenerate') def manage_doGenerate(self, sids=(), REQUEST=None): """(Re)generate types. """ schemas = [] for sid in sids: schemas.append(self.getSchema(sid)) for s in schemas: s.generate() if REQUEST: return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_workspace') security.declareProtected(permissions.ManagePortal, 'manage_inspect') def manage_inspect(self, UID, REQUEST=None): """Dump some things about an object hook in the debugger for now. """ object = self.getObject(UID) log(object, object.Schema(), dir(object)) return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_uids') security.declareProtected(permissions.ManagePortal, 'manage_reindex') def manage_reindex(self, REQUEST=None): """Assign UIDs to all basecontent objects. """ def _index(object, archetype_tool): archetype_tool.registerContent(object) self._rawEnum(_index, self) return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_uids') security.declareProtected(permissions.ManagePortal, 'index') index = manage_reindex def _listAllTypes(self): """List all types -- either currently known or known to us. """ allTypes = _types.copy(); allTypes.update(self._types) return allTypes.keys() security.declareProtected(permissions.ManagePortal, 'getChangedSchema') def getChangedSchema(self): """Returns a list of tuples indicating which schema have changed. Tuples have the form (schema, changed). """ list = [] currentTypes = _types ourTypes = self._types modified = False keys = self._listAllTypes() keys.sort() for t in keys: if t not in ourTypes: # Add it ourTypes[t] = currentTypes[t]['signature'] modified = True list.append((t, 0)) elif t not in currentTypes: # Huh: what shall we do? We remove it -- this might be wrong! del ourTypes[t] modified = True # We do not add an entry because we cannot update # these objects (having no longer type information for them) else: list.append((t, ourTypes[t] != currentTypes[t]['signature'])) if modified: self._p_changed = True return list security.declareProtected(permissions.ManagePortal, 'manage_updateSchema') def manage_updateSchema(self, REQUEST=None, update_all=None, remove_instance_schemas=None): """Make sure all objects' schema are up to date. """ out = StringIO() print >> out, 'Updating schema...' update_types = [] if REQUEST is None: # DM (avoid persistency bug): avoid code duplication update_types = [ti[0] for ti in self.getChangedSchema() if ti[1]] else: # DM (avoid persistency bug): for t in self._listAllTypes(): if REQUEST.form.get(t, False): update_types.append(t) update_all = REQUEST.form.get('update_all', False) remove_instance_schemas = REQUEST.form.get( 'remove_instance_schemas', False) # XXX: Enter this block only when there are types to update! if update_types: # Use the catalog's ZopeFindAndApply method to walk through # all objects in the portal. This works much better than # relying on the catalog to find objects, because an object # may be uncatalogable because of its schema, and then you # can't update it if you require that it be in the catalog. catalog = getToolByName(self, 'portal_catalog') portal = getToolByName(self, 'portal_url').getPortalObject() meta_types = [_types[t]['meta_type'] for t in update_types] if remove_instance_schemas: func_update_changed = self._removeSchemaAndUpdateChangedObject func_update_all = self._removeSchemaAndUpdateObject else: func_update_changed = self._updateChangedObject func_update_all = self._updateObject if update_all: catalog.ZopeFindAndApply(portal, obj_metatypes=meta_types, search_sub=True, apply_func=func_update_all) else: catalog.ZopeFindAndApply(portal, obj_metatypes=meta_types, search_sub=True, apply_func=func_update_changed) for t in update_types: self._types[t] = _types[t]['signature'] self._p_changed = True print >> out, 'Done.' return out.getvalue() # A counter to ensure that in a given interval a subtransaction # commit is done. subtransactioncounter = 0 def _updateObject(self, o, path, remove_instance_schemas=None): o._updateSchema(remove_instance_schemas=remove_instance_schemas) # Subtransactions to avoid eating up RAM when used inside a # 'ZopeFindAndApply' like in manage_updateSchema self.subtransactioncounter += 1 # Only every 250 objects a sub-commit, otherwise it eats up all diskspace if not self.subtransactioncounter % 250: transaction.savepoint(optimistic=True) def _updateChangedObject(self, o, path): if not o._isSchemaCurrent(): self._updateObject(o, path) def _removeSchemaAndUpdateObject(self, o, path): self._updateObject(o, path, remove_instance_schemas=True) def _removeSchemaAndUpdateChangedObject(self, o, path): if not o._isSchemaCurrent(): self._removeSchemaAndUpdateObject(o, path) security.declareProtected(permissions.ManagePortal, 'manage_updateSchema') def manage_migrate(self, REQUEST=None): """Run Extensions.migrations.migrate. """ from Products.Archetypes.Extensions.migrations import migrate out = migrate(self) self.manage_updateSchema() return out # Catalog management security.declareProtected(permissions.View, 'listCatalogs') def listCatalogs(self): """Show the catalog mapping. """ return self.catalog_map security.declareProtected(permissions.ManagePortal, 'manage_updateCatalogs') def manage_updateCatalogs(self, REQUEST=None): """Set the catalog map for meta_type to include the list catalog_names. """ prefix = 'catalog_names_' for key in REQUEST.form.keys(): if key.startswith(prefix): k = key[len(prefix):] v = REQUEST.form.get(key) self.setCatalogsByType(k, v) return REQUEST.RESPONSE.redirect(self.absolute_url() + '/manage_catalogs') security.declareProtected(permissions.ManagePortal, 'setCatalogsByType') def setCatalogsByType(self, portal_type, catalogList): """ associate catalogList with meta_type. (unfortunally not portal_type). catalogList is a list of strings with the ids of the catalogs. Each catalog is has to be a tool, means unique in site root. """ self.catalog_map[portal_type] = catalogList security.declareProtected(permissions.View, 'getCatalogsByType') def getCatalogsByType(self, portal_type): """Return the catalog objects assoicated with a given type. """ catalogs = [] catalog_map = getattr(self, 'catalog_map', None) if catalog_map is not None: names = self.catalog_map.get(portal_type, ['portal_catalog']) else: names = ['portal_catalog'] portal = getToolByName(self, 'portal_url').getPortalObject() for name in names: try: catalogs.append(getToolByName(portal, name)) except (ConflictError, KeyboardInterrupt): raise except Exception, E: log('No tool', name, E) pass return catalogs
class TransformTool(UniqueObject, ActionProviderBase, Folder): id = 'portal_transforms' meta_type = id.title().replace('_', ' ') isPrincipiaFolderish = 1 # Show up in the ZMI __implements__ = iengine implements(IPortalTransformsTool) meta_types = all_meta_types = ( { 'name' : 'Transform', 'action' : 'manage_addTransformForm'}, { 'name' : 'TransformsChain', 'action' : 'manage_addTransformsChainForm'}, ) manage_addTransformForm = PageTemplateFile('addTransform', _www) manage_addTransformsChainForm = PageTemplateFile('addTransformsChain', _www) manage_cacheForm = PageTemplateFile('setCacheTime', _www) manage_editTransformationPolicyForm = PageTemplateFile('editTransformationPolicy', _www) manage_reloadAllTransforms = PageTemplateFile('reloadAllTransforms', _www) manage_options = ((Folder.manage_options[0],) + Folder.manage_options[2:] + ( { 'label' : 'Caches', 'action' : 'manage_cacheForm'}, { 'label' : 'Policy', 'action' : 'manage_editTransformationPolicyForm'}, { 'label' : 'Reload transforms', 'action' : 'manage_reloadAllTransforms'}, ) ) security = ClassSecurityInfo() def __init__(self, policies=None, max_sec_in_cache=3600): self._mtmap = PersistentMapping() self._policies = policies or PersistentMapping() self.max_sec_in_cache = max_sec_in_cache self._new_style_pt = 1 # mimetype oriented conversions (iengine interface) ######################## def unregisterTransform(self, name): """ unregister a transform name is the name of a registered transform """ self._unmapTransform(getattr(self, name)) if name in self.objectIds(): self._delObject(name) def convertTo(self, target_mimetype, orig, data=None, object=None, usedby=None, context=None, **kwargs): """Convert orig to a given mimetype * orig is an encoded string * data an optional idatastream object. If None a new datastream will be created and returned * optional object argument is the object on which is bound the data. If present that object will be used by the engine to bound cached data. * additional arguments (kwargs) will be passed to the transformations. Some usual arguments are : filename, mimetype, encoding return an object implementing idatastream or None if no path has been found. """ target_mimetype = str(target_mimetype) if object is not None: cache = Cache(object) data = cache.getCache(target_mimetype) if data is not None: time, data = data if self.max_sec_in_cache == 0 or time < self.max_sec_in_cache: return data if data is None: data = self._wrap(target_mimetype) registry = getToolByName(self, 'mimetypes_registry') if not getattr(aq_base(registry), 'classify', None): # avoid problems when importing a site with an old mimetype registry # XXX return None or orig? return None orig_mt = registry.classify(orig, mimetype=kwargs.get('mimetype'), filename=kwargs.get('filename')) orig_mt = str(orig_mt) if not orig_mt: log('Unable to guess input mime type (filename=%s, mimetype=%s)' %( kwargs.get('mimetype'), kwargs.get('filename')), severity=DEBUG) return None target_mt = registry.lookup(target_mimetype) if target_mt: target_mt = target_mt[0] else: log('Unable to match target mime type %s'% str(target_mimetype), severity=DEBUG) return None ## fastpath # If orig_mt and target_mt are the same, we only allow # a one-hop transform, a.k.a. filter. # XXX disabled filtering for now filter_only = False if orig_mt == str(target_mt): filter_only = True data.setData(orig) md = data.getMetadata() md['mimetype'] = str(orig_mt) if object is not None: cache.setCache(str(target_mimetype), data) return data ## get a path to output mime type requirements = self._policies.get(str(target_mt), []) path = self._findPath(orig_mt, target_mt, list(requirements)) if not path and requirements: log('Unable to satisfy requirements %s' % ', '.join(requirements), severity=DEBUG) path = self._findPath(orig_mt, target_mt) if not path: log('NO PATH FROM %s TO %s : %s' % (orig_mt, target_mimetype, path), severity=DEBUG) return None #XXX raise TransformError if len(path) > 1: ## create a chain on the fly (sly) transform = chain() for t in path: transform.registerTransform(t) else: transform = path[0] result = transform.convert(orig, data, context=context, usedby=usedby, **kwargs) assert(idatastream.isImplementedBy(result), 'result doesn\'t implemented idatastream') self._setMetaData(result, transform) # set cache if possible if object is not None and result.isCacheable(): cache.setCache(str(target_mimetype), result) # return idatastream object return result security.declarePublic('convertToData') def convertToData(self, target_mimetype, orig, data=None, object=None, usedby=None, context=None, **kwargs): """Convert to a given mimetype and return the raw data ignoring subobjects. see convertTo for more information """ data =self.convertTo(target_mimetype, orig, data, object, usedby, context, **kwargs) if data: return data.getData() return None security.declarePublic('convert') def convert(self, name, orig, data=None, context=None, **kwargs): """run a tranform of a given name on data * name is the name of a registered transform see convertTo docstring for more info """ if not data: data = self._wrap(name) try: transform = getattr(self, name) except AttributeError: raise Exception('No such transform "%s"' % name) data = transform.convert(orig, data, context=context, **kwargs) self._setMetaData(data, transform) return data def __call__(self, name, orig, data=None, context=None, **kwargs): """run a transform by its name, returning the raw data product * name is the name of a registered transform. return an encoded string. see convert docstring for more info on additional arguments. """ data = self.convert(name, orig, data, context, **kwargs) return data.getData() # utilities ############################################################### def _setMetaData(self, datastream, transform): """set metadata on datastream according to the given transform (mime type and optionaly encoding) """ md = datastream.getMetadata() if hasattr(transform, 'output_encoding'): md['encoding'] = transform.output_encoding md['mimetype'] = transform.output def _wrap(self, name): """wrap a data object in an icache""" return datastream(name) def _unwrap(self, data): """unwrap data from an icache""" if idatastream.isImplementedBy(data): data = data.getData() return data def _mapTransform(self, transform): """map transform to internal structures""" registry = getToolByName(self, 'mimetypes_registry') inputs = getattr(transform, 'inputs', None) if not inputs: raise TransformException('Bad transform %s : no input MIME type' % (transform)) for i in inputs: mts = registry.lookup(i) if not mts: msg = 'Input MIME type %r for transform %s is not registered '\ 'in the MIME types registry' % (i, transform.name()) raise TransformException(msg) for mti in mts: for mt in mti.mimetypes: mt_in = self._mtmap.setdefault(mt, PersistentMapping()) output = getattr(transform, 'output', None) if not output: msg = 'Bad transform %s : no output MIME type' raise TransformException(msg % transform.name()) mto = registry.lookup(output) if not mto: msg = 'Output MIME type %r for transform %s is not '\ 'registered in the MIME types registry' % \ (output, transform.name()) raise TransformException(msg) if len(mto) > 1: msg = 'Wildcarding not allowed in transform\'s output '\ 'MIME type' raise TransformException(msg) for mt2 in mto[0].mimetypes: try: if not transform in mt_in[mt2]: mt_in[mt2].append(transform) except KeyError: mt_in[mt2] = PersistentList([transform]) def _unmapTransform(self, transform): """unmap transform from internal structures""" registry = getToolByName(self, 'mimetypes_registry') for i in transform.inputs: for mti in registry.lookup(i): for mt in mti.mimetypes: mt_in = self._mtmap.get(mt, {}) output = transform.output mto = registry.lookup(output) for mt2 in mto[0].mimetypes: l = mt_in[mt2] for i in range(len(l)): if transform.name() == l[i].name(): l.pop(i) break else: log('Can\'t find transform %s from %s to %s' % ( transform.name(), mti, mt), severity=DEBUG) def _findPath(self, orig, target, required_transforms=()): """return the shortest path for transformation from orig mimetype to target mimetype """ path = [] if not self._mtmap: return None # naive algorithm : # find all possible paths with required transforms # take the shortest # # it should be enough since we should not have so much possible paths shortest, winner = 9999, None for path in self._getPaths(str(orig), str(target), required_transforms): if len(path) < shortest: winner = path shortest = len(path) return winner def _getPaths(self, orig, target, requirements, path=None, result=None): """return a all path for transformation from orig mimetype to target mimetype """ if path is None: result = [] path = [] requirements = list(requirements) outputs = self._mtmap.get(orig) if outputs is None: return result path.append(None) for o_mt, transforms in outputs.items(): for transform in transforms: required = 0 name = transform.name() if name in requirements: requirements.remove(name) required = 1 if transform in path: # avoid infinite loop... continue path[-1] = transform if o_mt == target: if not requirements: result.append(path[:]) else: self._getPaths(o_mt, target, requirements, path, result) if required: requirements.append(name) path.pop() return result security.declarePrivate('manage_afterAdd') def manage_afterAdd(self, item, container): """ overload manage_afterAdd to finish initialization when the transform tool is added """ Folder.manage_afterAdd(self, item, container) transforms.initialize(self) # XXX required? #try: # # first initialization # transforms.initialize(self) #except: # # may fail on copy # pass security.declareProtected(ManagePortal, 'manage_addTransform') def manage_addTransform(self, id, module, REQUEST=None): """ add a new transform to the tool """ transform = Transform(id, module) self._setObject(id, transform) self._mapTransform(transform) if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main') security.declareProtected(ManagePortal, 'manage_addTransform') def manage_addTransformsChain(self, id, description, REQUEST=None): """ add a new transform to the tool """ transform = TransformsChain(id, description) self._setObject(id, transform) self._mapTransform(transform) if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main') security.declareProtected(ManagePortal, 'manage_addTransform') def manage_setCacheValidityTime(self, seconds, REQUEST=None): """set the lifetime of cached data in seconds""" self.max_sec_in_cache = int(seconds) if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main') security.declareProtected(ManagePortal, 'reloadTransforms') def reloadTransforms(self, ids=()): """ reload transforms with the given ids if no ids, reload all registered transforms return a list of (transform_id, transform_module) describing reloaded transforms """ if not ids: ids = self.objectIds() reloaded = [] for id in ids: o = getattr(self, id) o.reload() reloaded.append((id, o.module)) return reloaded # Policy handling methods ################################################# def manage_addPolicy(self, output_mimetype, required_transforms, REQUEST=None): """ add a policy for a given output mime types""" registry = getToolByName(self, 'mimetypes_registry') if not registry.lookup(output_mimetype): raise TransformException('Unknown MIME type') if self._policies.has_key(output_mimetype): msg = 'A policy for output %s is yet defined' % output_mimetype raise TransformException(msg) required_transforms = tuple(required_transforms) self._policies[output_mimetype] = required_transforms if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm') def manage_delPolicies(self, outputs, REQUEST=None): """ remove policies for given output mime types""" for mimetype in outputs: del self._policies[mimetype] if REQUEST is not None: REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm') def listPolicies(self): """ return the list of defined policies a policy is a 2-uple (output_mime_type, [list of required transforms]) """ # XXXFIXME: backward compat, should be removed latter if not hasattr(self, '_policies'): self._policies = PersistentMapping() return self._policies.items() # mimetype oriented conversions (iengine interface) ######################## def registerTransform(self, transform): """register a new transform transform isn't a Zope Transform (the wrapper) but the wrapped transform the persistence wrapper will be created here """ # needed when call from transform.transforms.initialize which # register non zope transform module = str(transform.__module__) transform = Transform(transform.name(), module, transform) if not itransform.isImplementedBy(transform): raise TransformException('%s does not implement itransform' % transform) name = transform.name() __traceback_info__ = (name, transform) if name not in self.objectIds(): self._setObject(name, transform) self._mapTransform(transform) security.declareProtected(ManagePortal, 'ZopeFind') def ZopeFind(self, *args, **kwargs): """Don't break ZopeFind feature when a transform can't be loaded """ try: return Folder.ZopeFind(self, *args, **kwargs) except MissingBinary: log('ZopeFind: catched MissingBinary exception') security.declareProtected(View, 'objectItems') def objectItems(self, *args, **kwargs): """Don't break ZopeFind feature when a transform can't be loaded """ try: return Folder.objectItems(self, *args, **kwargs) except MissingBinary: log('objectItems: catched MissingBinary exception') return [] # available mimetypes #################################################### def listAvailableTextInputs(self): """ Returns a list of mimetypes that can be used as input for textfields by building a list of the inputs beginning with "text/" of all transforms. """ available_types = [] candidate_transforms = [object[1] for object in self.objectItems()] for candidate in candidate_transforms: for input in candidate.inputs: if input.startswith("text/") and input not in available_types: available_types.append(input) return available_types
class ElementSpec( SimpleItem ): """ Represent all the tool knows about a single metadata element. """ security = ClassSecurityInfo() # # Default values. # is_multi_valued = 0 def __init__( self, is_multi_valued ): self.is_multi_valued = is_multi_valued self.policies = PersistentMapping() self.policies[ None ] = self._makePolicy() # set default policy security.declarePrivate( '_makePolicy' ) def _makePolicy( self ): return MetadataElementPolicy( self.is_multi_valued ) security.declareProtected(View , 'isMultiValued') def isMultiValued( self ): """ Is this element multi-valued? """ return self.is_multi_valued security.declareProtected(View , 'getPolicy') def getPolicy( self, typ=None ): """ Find the policy this element for objects whose type object name is 'typ'; return a default, if none found. """ try: return self.policies[ typ ].__of__(self) except KeyError: return self.policies[ None ] security.declareProtected(View , 'listPolicies') def listPolicies( self ): """ Return a list of all policies for this element. """ res = [] for k, v in self.policies.items(): res.append((k, v.__of__(self))) return res security.declareProtected(ManagePortal , 'addPolicy') def addPolicy( self, typ ): """ Add a policy to this element for objects whose type object name is 'typ'. """ if typ is None: raise MetadataError, "Can't replace default policy." if self.policies.has_key( typ ): raise MetadataError, "Existing policy for content type:" + typ self.policies[ typ ] = self._makePolicy() security.declareProtected(ManagePortal, 'removePolicy') def removePolicy( self, typ ): """ Remove the policy from this element for objects whose type object name is 'typ' (*not* the default, however). """ if typ is None: raise MetadataError, "Can't remove default policy." del self.policies[ typ ]
class ElementSpec(SimpleItem): """ Represent all the tool knows about a single metadata element. """ security = ClassSecurityInfo() # # Default values. # is_multi_valued = 0 def __init__(self, is_multi_valued): self.is_multi_valued = is_multi_valued self.policies = PersistentMapping() self.policies[None] = self._makePolicy() # set default policy security.declarePrivate('_makePolicy') def _makePolicy(self): return MetadataElementPolicy(self.is_multi_valued) security.declareProtected(CMFCorePermissions.View, 'isMultiValued') def isMultiValued(self): """ Is this element multi-valued? """ return self.is_multi_valued security.declareProtected(CMFCorePermissions.View, 'getPolicy') def getPolicy(self, typ=None): """ Find the policy this element for objects whose type object name is 'typ'; return a default, if none found. """ try: return self.policies[typ].__of__(self) except KeyError: return self.policies[None] security.declareProtected(CMFCorePermissions.View, 'listPolicies') def listPolicies(self): """ Return a list of all policies for this element. """ res = [] for k, v in self.policies.items(): res.append((k, v.__of__(self))) return res security.declareProtected(CMFCorePermissions.ManagePortal, 'addPolicy') def addPolicy(self, typ): """ Add a policy to this element for objects whose type object name is 'typ'. """ if typ is None: raise MetadataError, "Can't replace default policy." if self.policies.has_key(typ): raise MetadataError, "Existing policy for content type:" + typ self.policies[typ] = self._makePolicy() security.declareProtected(CMFCorePermissions.ManagePortal, 'removePolicy') def removePolicy(self, typ): """ Remove the policy from this element for objects whose type object name is 'typ' (*not* the default, however). """ if typ is None: raise MetadataError, "Can't remove default policy." del self.policies[typ]
class MetadataTool( UniqueObject, SimpleItem, ActionProviderBase ): __implements__ = (IMetadataTool, ActionProviderBase.__implements__) id = 'portal_metadata' meta_type = 'Default Metadata Tool' _actions = () # # Default values. # publisher = '' element_specs = None #initial_values_hook = None #validation_hook = None security = ClassSecurityInfo() def __init__( self , publisher=None #, initial_values_hook=None #, validation_hook=None , element_specs=DEFAULT_ELEMENT_SPECS ): self.editProperties( publisher #, initial_values_hook #, validation_hook ) self.element_specs = PersistentMapping() for name, is_multi_valued in element_specs: self.element_specs[ name ] = ElementSpec( is_multi_valued ) # # ZMI methods # manage_options = ( ActionProviderBase.manage_options + ( { 'label' : 'Overview' , 'action' : 'manage_overview' } , { 'label' : 'Properties' , 'action' : 'propertiesForm' } , { 'label' : 'Elements' , 'action' : 'elementPoliciesForm' } # TODO , { 'label' : 'Types' # , 'action' : 'typesForm' # } ) + SimpleItem.manage_options ) security.declareProtected(ManagePortal, 'manage_overview') manage_overview = DTMLFile( 'explainMetadataTool', _dtmldir ) security.declareProtected(ManagePortal, 'propertiesForm') propertiesForm = DTMLFile( 'metadataProperties', _dtmldir ) security.declareProtected(ManagePortal, 'editProperties') def editProperties( self , publisher=None # TODO , initial_values_hook=None # TODO , validation_hook=None , REQUEST=None ): """ Form handler for "tool-wide" properties (including list of metadata elements). """ if publisher is not None: self.publisher = publisher # TODO self.initial_values_hook = initial_values_hook # TODO self.validation_hook = validation_hook if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/propertiesForm' + '?manage_tabs_message=Tool+updated.' ) security.declareProtected(ManagePortal, 'elementPoliciesForm') elementPoliciesForm = DTMLFile( 'metadataElementPolicies', _dtmldir ) security.declareProtected(ManagePortal, 'addElementPolicy') def addElementPolicy( self , element , content_type , is_required , supply_default , default_value , enforce_vocabulary , allowed_vocabulary , REQUEST=None ): """ Add a type-specific policy for one of our elements. """ if content_type == '<default>': content_type = None spec = self.getElementSpec( element ) spec.addPolicy( content_type ) policy = spec.getPolicy( content_type ) policy.edit( is_required , supply_default , default_value , enforce_vocabulary , allowed_vocabulary ) if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+added.' ) security.declareProtected(ManagePortal, 'removeElementPolicy') def removeElementPolicy( self , element , content_type , REQUEST=None ): """ Remvoe a type-specific policy for one of our elements. """ if content_type == '<default>': content_type = None spec = self.getElementSpec( element ) spec.removePolicy( content_type ) if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+removed.' ) security.declareProtected(ManagePortal, 'updateElementPolicy') def updateElementPolicy( self , element , content_type , is_required , supply_default , default_value , enforce_vocabulary , allowed_vocabulary , REQUEST=None ): """ Update a policy for one of our elements ('content_type' will be '<default>' when we edit the default). """ if content_type == '<default>': content_type = None spec = self.getElementSpec( element ) policy = spec.getPolicy( content_type ) policy.edit( is_required , supply_default , default_value , enforce_vocabulary , allowed_vocabulary ) if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/elementPoliciesForm' + '?element=' + element + '&manage_tabs_message=Policy+updated.' ) # # Element spec manipulation. # security.declareProtected(ManagePortal, 'listElementSpecs') def listElementSpecs( self ): """ Return a list of ElementSpecs representing the elements managed by the tool. """ res = [] for k, v in self.element_specs.items(): res.append((k, v.__of__(self))) return res security.declareProtected(ManagePortal, 'getElementSpec') def getElementSpec( self, element ): """ Return an ElementSpec representing the tool's knowledge of 'element'. """ return self.element_specs[ element ].__of__( self ) security.declareProtected(ManagePortal, 'addElementSpec') def addElementSpec( self, element, is_multi_valued, REQUEST=None ): """ Add 'element' to our list of managed elements. """ # Don't replace. if self.element_specs.has_key( element ): return self.element_specs[ element ] = ElementSpec( is_multi_valued ) if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/propertiesForm' + '?manage_tabs_message=Element+' + element + '+added.' ) security.declareProtected(ManagePortal, 'removeElementSpec') def removeElementSpec( self, element, REQUEST=None ): """ Remove 'element' from our list of managed elements. """ del self.element_specs[ element ] if REQUEST is not None: REQUEST[ 'RESPONSE' ].redirect( self.absolute_url() + '/propertiesForm' + '?manage_tabs_message=Element+' + element + '+removed.' ) security.declareProtected(ManagePortal, 'listPolicies') def listPolicies( self, typ=None ): """ Show all policies for a given content type, or the default if None. """ result = [] for element, spec in self.listElementSpecs(): result.append( ( element, spec.getPolicy( typ ) ) ) return result # # 'portal_metadata' interface # security.declarePrivate( 'getFullName' ) def getFullName( self, userid ): """ Convert an internal userid to a "formal" name, if possible, perhaps using the 'portal_membership' tool. Used to map userid's for Creator, Contributor DCMI queries. """ return userid # TODO: do lookup here security.declarePublic( 'getPublisher' ) def getPublisher( self ): """ Return the "formal" name of the publisher of the portal. """ return self.publisher security.declarePublic( 'listAllowedVocabulary' ) def listAllowedVocabulary( self, element, content=None, content_type=None ): """ List allowed keywords for a given portal_type, or all possible keywords if none supplied. """ spec = self.getElementSpec( element ) if content_type is None and content: content_type = content.getPortalTypeName() return spec.getPolicy( content_type ).allowedVocabulary() security.declarePublic( 'listAllowedSubjects' ) def listAllowedSubjects( self, content=None, content_type=None ): """ List allowed keywords for a given portal_type, or all possible keywords if none supplied. """ return self.listAllowedVocabulary( 'Subject', content, content_type ) security.declarePublic( 'listAllowedFormats' ) def listAllowedFormats( self, content=None, content_type=None ): """ List the allowed 'Content-type' values for a particular portal_type, or all possible formats if none supplied. """ return self.listAllowedVocabulary( 'Format', content, content_type ) security.declarePublic( 'listAllowedLanguages' ) def listAllowedLanguages( self, content=None, content_type=None ): """ List the allowed language values. """ return self.listAllowedVocabulary( 'Language', content, content_type ) security.declarePublic( 'listAllowedRights' ) def listAllowedRights( self, content=None, content_type=None ): """ List the allowed values for a "Rights" selection list; this gets especially important where syndication is involved. """ return self.listAllowedVocabulary( 'Rights', content, content_type ) security.declareProtected(ModifyPortalContent, 'setInitialMetadata') def setInitialMetadata( self, content ): """ Set initial values for content metatdata, supplying any site-specific defaults. """ for element, policy in self.listPolicies(content.getPortalTypeName()): if not getattr( content, element )(): if policy.supplyDefault(): setter = getattr( content, 'set%s' % element ) setter( policy.defaultValue() ) elif policy.isRequired(): raise MetadataError, \ 'Metadata element %s is required.' % element # TODO: Call initial_values_hook, if present security.declareProtected(View, 'validateMetadata') def validateMetadata( self, content ): """ Enforce portal-wide policies about DCI, e.g., requiring non-empty title/description, etc. Called by the CMF immediately before saving changes to the metadata of an object. """ for element, policy in self.listPolicies(content.getPortalTypeName()): value = getattr( content, element )() if not value and policy.isRequired(): raise MetadataError, \ 'Metadata element %s is required.' % element if value and policy.enforceVocabulary(): values = policy.isMultiValued() and value or [ value ] for value in values: if not value in policy.allowedVocabulary(): raise MetadataError, \ 'Value %s is not in allowed vocabulary for ' \ 'metadata element %s.' % ( value, element )
class MessageCatalog(LanguageManager, ObjectManager, SimpleItem): """ Stores messages and their translations... """ meta_type = 'MessageCatalog' security = ClassSecurityInfo() def __init__(self, id, title, sourcelang, languages): self.id = id self.title = title # Language Manager data self._languages = tuple(languages) self._default_language = sourcelang # Here the message translations are stored self._messages = PersistentMapping() # Data for the PO files headers self._po_headers = PersistentMapping() for lang in self._languages: self._po_headers[lang] = empty_po_header ####################################################################### # Public API ####################################################################### security.declarePublic('message_encode') def message_encode(self, message): """ Encodes a message to an ASCII string. To be used in the user interface, to avoid problems with the encodings, HTML entities, etc.. """ if type(message) is UnicodeType: msg = 'u' + message.encode('utf8') else: msg = 'n' + message return base64.encodestring(msg) security.declarePublic('message_decode') def message_decode(self, message): """ Decodes a message from an ASCII string. To be used in the user interface, to avoid problems with the encodings, HTML entities, etc.. """ message = base64.decodestring(message) type = message[0] message = message[1:] if type == 'u': return unicode(message, 'utf8') return message security.declarePublic('message_exists') def message_exists(self, message): """ """ return self._messages.has_key(message) security.declareProtected('Manage messages', 'message_edit') def message_edit(self, message, language, translation, note): """ """ self._messages[message][language] = translation self._messages[message]['note'] = note security.declareProtected('Manage messages', 'message_del') def message_del(self, message): """ """ del self._messages[message] security.declarePublic('gettext') def gettext(self, message, lang=None, add=1, default=_marker): """Returns the message translation from the database if available. If add=1, add any unknown message to the database. If a default is provided, use it instead of the message id as a translation for unknown messages. """ if type(message) not in (StringType, UnicodeType): raise TypeError, 'only strings can be translated.' message = message.strip() if default is _marker: default = message # Add it if it's not in the dictionary if add and not self._messages.has_key(message) and message: self._messages[message] = PersistentMapping() # Get the string if self._messages.has_key(message): m = self._messages[message] if lang is None: # Builds the list of available languages # should the empty translations be filtered? available_languages = list(self._languages) # Imagine that the default language is 'en'. There is no # translation from 'en' to 'en' in the message catalog # The user has the preferences 'en' and 'nl' in that order # The next two lines make certain 'en' is shown, not 'nl' if not self._default_language in available_languages: available_languages.append(self._default_language) # Get the language! lang = lang_negotiator(available_languages) # Is it None? use the default if lang is None: lang = self._default_language if lang is not None: return m.get(lang) or default return default __call__ = gettext def translate(self, domain, msgid, *args, **kw): """ This method is required to get the i18n namespace from ZPT working. """ return self.gettext(msgid) ####################################################################### # Management screens ####################################################################### def manage_options(self): """ """ options = ( {'label': N_('Messages'), 'action': 'manage_messages', 'help': ('Localizer', 'MC_messages.stx')}, {'label': N_('Properties'), 'action': 'manage_propertiesForm'}, {'label': N_('Import/Export'), 'action': 'manage_importExport', 'help': ('Localizer', 'MC_importExport.stx')}, {'label': N_('TMX'), 'action': 'manage_tmx'}) \ + LanguageManager.manage_options \ + SimpleItem.manage_options r = [] for option in options: option = option.copy() option['label'] = _(option['label']) r.append(option) return r ####################################################################### # Management screens -- Messages ####################################################################### security.declareProtected('Manage messages', 'manage_messages') manage_messages = LocalDTMLFile('ui/MC_messages', globals()) security.declareProtected('Manage messages', 'get_translations') def get_translations(self, message): """ """ return self._messages[message] security.declarePublic('get_url') def get_url(self, url, batch_start, batch_size, regex, lang, empty, **kw): """ """ params = [] for key, value in kw.items(): if value is not None: params.append('%s=%s' % (key, quote(value))) params.extend(['batch_start:int=%d' % batch_start, 'batch_size:int=%d' % batch_size, 'regex=%s' % quote(regex), 'empty=%s' % (empty and 'on' or '')]) if lang: params.append('lang=%s' % lang) return url + '?' + '&'.join(params) def to_unicode(self, x): """ In Zope the ISO-8859-1 encoding has an special status, normal strings are considered to be in this encoding by default. """ if type(x) is StringType: x = unicode(x, 'iso-8859-1') return x def filter_sort(self, x, y): x = self.to_unicode(x) y = self.to_unicode(y) return cmp(x, y) security.declarePublic('filter') def filter(self, message, lang, empty, regex, batch_start, batch_size=15): """ For the management interface, allows to filter the messages to show. """ # Filter the messages regex = regex.strip() try: regex = re.compile(regex) except: regex = re.compile('') messages = [] for m, t in self._messages.items(): if regex.search(m) and (not empty or not t.get(lang, '').strip()): messages.append(m) messages.sort(self.filter_sort) # How many messages n = len(messages) # Calculate the start while batch_start >= n: batch_start = batch_start - batch_size if batch_start < 0: batch_start = 0 # Select the batch to show batch_end = batch_start + batch_size messages = messages[batch_start:batch_end] # Get the message message_encoded = None if message is None: if messages: message = messages[0] message_encoded = self.message_encode(message) else: message_encoded = message message = self.message_decode(message) # Calculate the current message aux = [] for x in messages: current = type(x) is type(message) \ and self.to_unicode(x) == self.to_unicode(message) aux.append({'message': x, 'current': current}) return {'messages': aux, 'n_messages': n, 'batch_start': batch_start, 'message': message, 'message_encoded': message_encoded} security.declareProtected('Manage messages', 'manage_editMessage') def manage_editMessage(self, message, language, translation, note, REQUEST, RESPONSE): """Modifies a message.""" message_encoded = message message = self.message_decode(message_encoded) self.message_edit(message, language, translation, note) url = self.get_url(REQUEST.URL1 + '/manage_messages', REQUEST['batch_start'], REQUEST['batch_size'], REQUEST['regex'], REQUEST.get('lang', ''), REQUEST.get('empty', 0), msg=message_encoded, manage_tabs_message=_('Saved changes.')) RESPONSE.redirect(url) security.declareProtected('Manage messages', 'manage_delMessage') def manage_delMessage(self, message, REQUEST, RESPONSE): """ """ message = self.message_decode(message) self.message_del(message) url = self.get_url(REQUEST.URL1 + '/manage_messages', REQUEST['batch_start'], REQUEST['batch_size'], REQUEST['regex'], REQUEST.get('lang', ''), REQUEST.get('empty', 0), manage_tabs_message=_('Saved changes.')) RESPONSE.redirect(url) ####################################################################### # Management screens -- Properties # Management screens -- Import/Export # FTP access ####################################################################### security.declareProtected('View management screens', 'manage_propertiesForm') manage_propertiesForm = LocalDTMLFile('ui/MC_properties', globals()) security.declareProtected('View management screens', 'manage_properties') def manage_properties(self, title, REQUEST=None, RESPONSE=None): """Change the Message Catalog properties.""" self.title = title if RESPONSE is not None: RESPONSE.redirect('manage_propertiesForm') # Properties management screen security.declareProtected('View management screens', 'get_po_header') def get_po_header(self, lang): """ """ # For backwards compatibility if not hasattr(aq_base(self), '_po_headers'): self._po_headers = PersistentMapping() return self._po_headers.get(lang, empty_po_header) security.declareProtected('View management screens', 'update_po_header') def update_po_header(self, lang, last_translator_name=None, last_translator_email=None, language_team=None, charset=None, REQUEST=None, RESPONSE=None): """ """ header = self.get_po_header(lang) if last_translator_name is None: last_translator_name = header['last_translator_name'] if last_translator_email is None: last_translator_email = header['last_translator_email'] if language_team is None: language_team = header['language_team'] if charset is None: charset = header['charset'] header = {'last_translator_name': last_translator_name, 'last_translator_email': last_translator_email, 'language_team': language_team, 'charset': charset} self._po_headers[lang] = header if RESPONSE is not None: RESPONSE.redirect('manage_propertiesForm') security.declareProtected('View management screens', 'manage_importExport') manage_importExport = LocalDTMLFile('ui/MC_importExport', globals()) security.declarePublic('get_charsets') def get_charsets(self): """ """ return charsets[:] security.declarePublic('manage_export') def manage_export(self, x, REQUEST=None, RESPONSE=None): """ Exports the content of the message catalog either to a template file (locale.pot) or to an language specific PO file (<x>.po). """ # Get the PO header info header = self.get_po_header(x) last_translator_name = header['last_translator_name'] last_translator_email = header['last_translator_email'] language_team = header['language_team'] charset = header['charset'] # PO file header, empty message. po_revision_date = time.strftime('%Y-%m-%d %H:%m+%Z', time.gmtime(time.time())) pot_creation_date = po_revision_date last_translator = '%s <%s>' % (last_translator_name, last_translator_email) if x == 'locale.pot': language_team = 'LANGUAGE <*****@*****.**>' else: language_team = '%s <%s>' % (x, language_team) r = ['msgid ""', 'msgstr "Project-Id-Version: %s\\n"' % self.title, '"POT-Creation-Date: %s\\n"' % pot_creation_date, '"PO-Revision-Date: %s\\n"' % po_revision_date, '"Last-Translator: %s\\n"' % last_translator, '"Language-Team: %s\\n"' % language_team, '"MIME-Version: 1.0\\n"', '"Content-Type: text/plain; charset=%s\\n"' % charset, '"Content-Transfer-Encoding: 8bit\\n"', '', ''] # Get the messages, and perhaps its translations. d = {} if x == 'locale.pot': filename = x for k in self._messages.keys(): d[k] = "" else: filename = '%s.po' % x for k, v in self._messages.items(): try: d[k] = v[x] except KeyError: d[k] = "" # Generate the file def backslashescape(x): quote_esc = re.compile(r'"') x = quote_esc.sub('\\"', x) trans = [('\n', '\\n'), ('\r', '\\r'), ('\t', '\\t')] for a, b in trans: x = x.replace(a, b) return x # Generate sorted msgids to simplify diffs dkeys = d.keys() dkeys.sort() for k in dkeys: r.append('msgid "%s"' % backslashescape(k)) v = d[k] r.append('msgstr "%s"' % backslashescape(v)) r.append('') if RESPONSE is not None: RESPONSE.setHeader('Content-type','application/data') RESPONSE.setHeader('Content-Disposition', 'inline;filename=%s' % filename) r2 = [] for x in r: if type(x) is UnicodeType: r2.append(x.encode(charset)) else: r2.append(x) return '\n'.join(r2) security.declareProtected('Manage messages', 'po_import') def po_import(self, lang, data): """ """ messages = self._messages resource = memory.File(data) po = PO.PO(resource) # Load the data for msgid in po.get_msgids(): if msgid: msgstr = po.get_msgstr(msgid) or '' if not messages.has_key(msgid): messages[msgid] = PersistentMapping() messages[msgid][lang] = msgstr # Set the encoding (the full header should be loaded XXX) self.update_po_header(lang, charset=po.get_encoding()) security.declareProtected('Manage messages', 'manage_import') def manage_import(self, lang, file, REQUEST=None, RESPONSE=None): """ """ # XXX For backwards compatibility only, use "po_import" instead. if isinstance(file, str): content = file else: content = file.read() self.po_import(lang, content) if RESPONSE is not None: RESPONSE.redirect('manage_messages') def objectItems(self, spec=None): """ """ for lang in self._languages: if not hasattr(aq_base(self), lang): self._setObject(lang, POFile(lang)) r = MessageCatalog.inheritedAttribute('objectItems')(self, spec) return r ####################################################################### # TMX support security.declareProtected('View management screens', 'manage_tmx') manage_tmx = LocalDTMLFile('ui/MC_tmx', globals()) security.declareProtected('Manage messages', 'tmx_export') def tmx_export(self, REQUEST, RESPONSE=None): """ Exports the content of the message catalog to a TMX file """ orglang = self._default_language # orglang = orglang.lower() # Get the header info header = self.get_po_header(orglang) charset = header['charset'] r = [] # Generate the TMX file header r.append('<?xml version="1.0" encoding="utf-8"?>') r.append('<!DOCTYPE tmx SYSTEM "http://www.lisa.org/tmx/tmx14.dtd">') r.append('<tmx version="1.4">') r.append('<header') r.append('creationtool="Localizer"') r.append('creationtoolversion="1.x"') r.append('datatype="plaintext"') r.append('segtype="paragraph"') r.append('adminlang="%s"' % orglang) r.append('srclang="%s"' % orglang) r.append('o-encoding="%s"' % charset.lower()) r.append('>') r.append('</header>') r.append('') # Get the messages, and perhaps its translations. d = {} filename = '%s.tmx' % self.id for msgkey, transunit in self._messages.items(): try: d[msgkey] = transunit[orglang] except KeyError: d[msgkey] = "" # Generate sorted msgids to simplify diffs dkeys = d.keys() dkeys.sort() r.append('<body>') for msgkey in dkeys: r.append('<tu>') transunit = self._messages.get(msgkey) if transunit.has_key('note') and transunit['note']: r.append('<note>%s</note>' % escape(transunit['note'])) r.append('<tuv xml:lang="%s">' % orglang) # The key is the message r.append('<seg>%s</seg></tuv>' % escape(msgkey)) for tlang in self._languages: if tlang != orglang: v = transunit.get(tlang,'') v = escape(v) r.append('<tuv xml:lang="%s">' % tlang) r.append('<seg>%s</seg></tuv>' % v) r.append('</tu>') r.append('') r.append('</body>') r.append('</tmx>') if RESPONSE is not None: RESPONSE.setHeader('Content-type','application/data') RESPONSE.setHeader('Content-Disposition', 'attachment; filename="%s"' % filename) r2 = [] for x in r: if type(x) is UnicodeType: r2.append(x.encode('utf-8')) else: r2.append(x) return '\r\n'.join(r2) def _normalize_lang(self,langcode): """ Get the core language (The part before the '-') and return it in lowercase. If there is a local part, return it in uppercase. """ dash = langcode.find('-') if dash == -1: la = langcode.lower() return (la,la) else: la = langcode[:dash].lower() lo = langcode[dash+1:].upper() return (la+'-'+lo,la) def _tmx_header(self, attrs): """ Works on a header (<header>) """ if attrs.has_key('srclang'): self._v_srclang = attrs['srclang'] def _tmx_tu(self, unit): """ Works on a translation unit (<tu>) """ src_lang = self._default_language # We look up the message for the language we have chosen to be our # working language if unit.has_key(self._default_language): src_lang = self._default_language else: # This MUST exist. Otherwise the TMX file is bad src_lang = self._v_srclang key = unit[src_lang] if key == u'': return # Don't add empty messages keysum = md5text(key) # For future indexing on md5 sums messages = self._messages languages = list(self._languages) if not messages.has_key(key): if self._v_howmuch == 'clear' or self._v_howmuch == 'all': messages[key] = PersistentMapping() else: return # Don't add unknown messages self._v_num_translations = self._v_num_translations + 1 for lang in unit.keys(): # Since the messagecatalog's default language overrides our # source language anyway, we handle "*all*" correctly already. # In the test below "*all*" should not be allowed. # Languages that start with '_' are other properties if lang == '_note': messages[key]['note'] = unit[lang] self._v_num_notes = self._v_num_notes + 1 continue if lang[0] == '_': # Unknown special property continue if lang == '*all*' or lang == '*none*': lang = self._v_srclang (target_lang, core_lang) = self._normalize_lang(lang) # If the core language is not seen before then add it if core_lang != src_lang and core_lang not in languages: languages.append(core_lang) # If the language+locality is not seen before then add it if target_lang != src_lang and target_lang not in languages: languages.append(target_lang) # Add message for language+locality if target_lang != src_lang: messages[key][target_lang] = unit[target_lang] # Add message for core language if not (unit.has_key(core_lang) or core_lang == src_lang): messages[key][core_lang] = unit[target_lang] self._languages = tuple(languages) self._messages = messages security.declareProtected('Manage messages', 'tmx_import') def tmx_import(self, howmuch, file, REQUEST=None, RESPONSE=None): """ Imports a TMX level 1 file. We use the SAX parser. It has the benefit that it internally converts everything to python unicode strings. """ if howmuch == 'clear': # Clear the message catalogue prior to import self._messages = {} self._languages = () self._v_howmuch = howmuch self._v_srclang = self._default_language self._v_num_translations = 0 self._v_num_notes = 0 # Create a parser parser = make_parser() chandler = HandleTMXParsing(self._tmx_tu, self._tmx_header) # Tell the parser to use our handler parser.setContentHandler(chandler) # Don't load the DTD from the Internet parser.setFeature(handler.feature_external_ges, 0) inputsrc = InputSource() if type(file) is StringType: inputsrc.setByteStream(StringIO(file)) else: content = file.read() inputsrc.setByteStream(StringIO(content)) parser.parse(inputsrc) num_translations = self._v_num_translations num_notes = self._v_num_notes del self._v_srclang del self._v_howmuch del self._v_num_translations del self._v_num_notes if REQUEST is not None: return MessageDialog( title = _('Messages imported'), message = _('Imported %d messages and %d notes') % (num_translations, num_notes), action = 'manage_messages') ####################################################################### # Backwards compatibility (XXX) ####################################################################### hasmsg = message_exists hasLS = message_exists # CMFLocalizer uses it security.declareProtected('Manage messages', 'xliff_export') def xliff_export(self, x, export_all=1, REQUEST=None, RESPONSE=None): """ Exports the content of the message catalog to an XLIFF file """ orglang = self._default_language from DateTime import DateTime r = [] # alias for append function. For optimization purposes r_append = r.append # Generate the XLIFF file header RESPONSE.setHeader('Content-Type', 'text/xml; charset=UTF-8') RESPONSE.setHeader('Content-Disposition', 'attachment; filename="%s_%s_%s.xlf"' % (self.id, orglang, x)) r_append('<?xml version="1.0" encoding="UTF-8"?>') # Version 1.1 of the DTD is not yet available - use version 1.0 r_append('<!DOCTYPE xliff SYSTEM "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd">') # Force a UTF-8 char in the start r_append(u'<!-- XLIFF Format Copyright \xa9 OASIS Open 2001-2003 -->') r_append('<xliff version="1.0">') r_append('<file') r_append('original="/%s"' % self.absolute_url(1)) r_append('product-name="Localizer"') r_append('product-version="1.1.x"') r_append('datatype="plaintext"') r_append('source-language="%s"' % orglang) r_append('target-language="%s"' % x) r_append('date="%s"' % DateTime().HTML4()) r_append('>') r_append('<header>') # r_append('<phase-group>') # r_append('<phase ') # r_append('phase-name="%s"' % REQUEST.get('phase_name', '')) # r_append('process-name="Export"') # r_append('tool="Localizer"') # r_append('date="%s"' % DateTime().HTML4()) # r_append('company-name="%s"' % REQUEST.get('company_name', '')) # r_append('job-id="%s"' % REQUEST.get('job_id', '')) # r_append('contact-name="%s"' % REQUEST.get('contact_name', '')) # r_append('contact-email="%s"' % REQUEST.get('contact_email', '')) # r_append('/>') # r_append('</phase-group>') r_append('</header>') r_append('<body>') # Get the messages, and perhaps its translations. d = {} for msgkey, transunit in self._messages.items(): try: # if export_all=1 export all messages otherwise export # only untranslated messages if int(export_all) == 1 \ or (int(export_all) == 0 and transunit[x] == ''): d[msgkey] = transunit[x] except KeyError: d[msgkey] = "" if d[msgkey] == "": d[msgkey] = msgkey # Generate sorted msgids to simplify diffs dkeys = d.keys() dkeys.sort() for msgkey in dkeys: transunit = self._messages[msgkey] r_append('<trans-unit id="%s">' % md5text(msgkey)) r_append(' <source>%s</source>' % escape(msgkey)) r_append(' <target>%s</target>' % escape(d[msgkey])) if transunit.has_key('note') and transunit['note']: r_append(' <note>%s</note>' % escape(transunit['note'])) r_append('</trans-unit>') r_append('</body>') r_append('</file>') r_append('</xliff>') r2 = [] for x in r: if type(x) is UnicodeType: r2.append(x.encode('utf-8')) else: r2.append(x) return '\r\n'.join(r2) security.declareProtected('Manage messages', 'xliff_import') def xliff_import(self, file, REQUEST=None): """ XLIFF is the XML Localization Interchange File Format designed by a group of software providers. It is specified by www.oasis-open.org """ messages = self._messages # Build a table of messages hashed on the md5 sum of the message # This is because sometimes the xliff file has the sources translated, # not the targets md5hash = {} for mes in messages.keys(): hash = md5text(mes) md5hash[hash] = mes parser = HandleXliffParsing() # parse the xliff information chandler = parser.parseXLIFFFile(file) if chandler is None: return MessageDialog(title = 'Parse error', message = 'Unable to parse XLIFF file' , action = 'manage_main',) header_info = chandler.getFileTag() #get the target language lang = [x for x in header_info if x[0]=='target-language'][0][1] (targetlang, core_lang) = self._normalize_lang(lang) # return a dictionary {id: (source, target)} body_info = chandler.getBody() num_notes = 0 num_translations = 0 # load the data for msgkey, transunit in body_info.items(): # If message is not in catalog, then it is new in xliff file # -- not legal if md5hash.has_key(msgkey): # Normal add srcmsg = md5hash[msgkey] if transunit['note'] != messages[srcmsg].get('note',u''): messages[srcmsg]['note'] = transunit['note'] num_notes = num_notes + 1 if srcmsg == transunit['target']: # No translation was done continue num_translations = num_translations + 1 if transunit['target'] == u'' and transunit['source'] != srcmsg: # The source was translated. Happens sometimes messages[srcmsg][targetlang] = transunit['source'] else: messages[srcmsg][targetlang] = transunit['target'] if REQUEST is not None: return MessageDialog(title = _('Messages imported'), message = _('Imported %d messages and %d notes to %s') % \ (num_translations, num_notes, targetlang) , action = 'manage_messages',)