Eradicated Flavour and PodTemplate classes (for the latter, use Pod fields instead); Added a code analyser; Groups can now be slaves in master/slaves relationships; Refs have more params (show a confirmation popup before adding an object, add an object without creation form); Code for Refs has been refactored to comply with the new way to organize Types; Added a WebDAV client library.

This commit is contained in:
Gaetan Delannay 2010-10-14 14:43:56 +02:00
parent 9f4db88bdf
commit 990e16c6e7
47 changed files with 1006 additions and 1297 deletions

View file

@ -1,9 +1,6 @@
'''This package contains mixin classes that are mixed in with generated classes:
- mixins/ClassMixin is mixed in with Standard Archetypes classes;
- mixins/ToolMixin is mixed in with the generated application Tool class;
- mixins/FlavourMixin is mixed in with the generated application Flavour
class.
The AbstractMixin defined hereafter is the base class of any mixin.'''
- mixins/BaseMixin is mixed in with Standard Archetypes classes;
- mixins/ToolMixin is mixed in with the generated application Tool class.'''
# ------------------------------------------------------------------------------
import os, os.path, types, mimetypes
@ -15,10 +12,10 @@ from appy.gen.plone25.descriptors import ClassDescriptor
from appy.gen.plone25.utils import updateRolesForPermission
# ------------------------------------------------------------------------------
class AbstractMixin:
'''Every Archetype class generated by appy.gen inherits from a mixin that
inherits from this class. It contains basic functions allowing to
minimize the amount of generated code.'''
class BaseMixin:
'''Every Archetype class generated by appy.gen inherits from this class or
a subclass of it.'''
_appy_meta_type = 'Class'
def createOrUpdate(self, created, values):
'''This method creates (if p_created is True) or updates an object.
@ -52,17 +49,21 @@ class AbstractMixin:
# Keep in history potential changes on historized fields
self.historizeData(previousData)
# Manage references
obj._appy_manageRefs(created)
# Manage potential link with an initiator object
if created and rq.get('nav', None):
# Get the initiator
splitted = rq['nav'].split('.')
if splitted[0] == 'search': return # Not an initiator but a search.
initiator = self.uid_catalog(UID=splitted[1])[0].getObject()
fieldName = splitted[2].split(':')[0]
initiator.appy().link(fieldName, obj)
# Call the custom "onEdit" if available
if obj.wrapperClass:
# Get the wrapper first
appyObject = obj.appy()
# Call the custom "onEdit" if available
if hasattr(appyObject, 'onEdit'):
appyObject.onEdit(created)
# Manage "add" permissions
if hasattr(appyObject, 'onEdit'): appyObject.onEdit(created)
# Manage "add" permissions and reindex the object
obj._appy_managePermissions()
# Reindex object
obj.reindexObject()
return obj
@ -95,6 +96,12 @@ class AbstractMixin:
(baseUrl, typeName, objId)
return self.goto(self.getUrl(editUrl, **urlParams))
def onCreateWithoutForm(self):
'''This method is called when a user wants to create a object from a
reference field, automatically (without displaying a form).'''
rq = self.REQUEST
self.appy().create(rq['fieldName'])
def intraFieldValidation(self, errors, values):
'''This method performs field-specific validation for every field from
the page that is being created or edited. For every field whose
@ -120,7 +127,7 @@ class AbstractMixin:
obj = self.appy()
if not hasattr(obj, 'validate'): return
obj.validate(values, errors)
# This custom "validate" method may have added fields in the given
# Those custom validation methods may have added fields in the given
# p_errors object. Within this object, for every error message that is
# not a string, we replace it with the standard validation error for the
# corresponding field.
@ -241,21 +248,19 @@ class AbstractMixin:
res = {}
for appyType in self.getAllAppyTypes():
if appyType.historized:
res[appyType.name] = (getattr(self, appyType.name),
appyType.labelId)
res[appyType.name] = appyType.getValue(self)
return res
def addDataChange(self, changes, labels=False):
def addDataChange(self, changes):
'''This method allows to add "manually" a data change into the objet's
history. Indeed, data changes are "automatically" recorded only when
a HTTP form is uploaded, not if, in the code, a setter is called on
a field. The method is also called by the method historizeData below,
that performs "automatic" recording when a HTTP form is uploaded.'''
# Add to the p_changes dict the field labels if they are not present
if not labels:
for fieldName in changes.iterkeys():
appyType = self.getAppyType(fieldName)
changes[fieldName] = (changes[fieldName], appyType.labelId)
# Add to the p_changes dict the field labels
for fieldName in changes.iterkeys():
appyType = self.getAppyType(fieldName)
changes[fieldName] = (changes[fieldName], appyType.labelId)
# Create the event to record in the history
DateTime = self.getProductConfig().DateTime
state = self.portal_workflow.getInfoFor(self, 'review_state')
@ -273,14 +278,18 @@ class AbstractMixin:
historized fields, while p_self already contains the (potentially)
modified values.'''
# Remove from previousData all values that were not changed
for fieldName in previousData.keys():
prev = previousData[fieldName][0]
curr = getattr(self, fieldName)
for field in previousData.keys():
prev = previousData[field]
appyType = self.getAppyType(field)
curr = appyType.getValue(self)
if (prev == curr) or ((prev == None) and (curr == '')) or \
((prev == '') and (curr == None)):
del previousData[fieldName]
del previousData[field]
if (appyType.type == 'Ref') and (field in previousData):
titles = [r.title for r in previousData[field]]
previousData[field] = ','.join(titles)
if previousData:
self.addDataChange(previousData, labels=True)
self.addDataChange(previousData)
def goto(self, url, addParams=False):
'''Brings the user to some p_url after an action has been executed.'''
@ -308,80 +317,14 @@ class AbstractMixin:
field named p_name.'''
return self.getAppyType(name).getFormattedValue(self, value)
def _appy_getRefs(self, fieldName, ploneObjects=False,
noListIfSingleObj=False, startNumber=None):
'''p_fieldName is the name of a Ref field. This method returns an
ordered list containing the objects linked to p_self through this
field. If p_ploneObjects is True, the method returns the "true"
Plone objects instead of the Appy wrappers.
If p_startNumber is None, this method returns all referred objects.
If p_startNumber is a number, this method will return x objects,
starting at p_startNumber, x being appyType.maxPerPage.'''
appyType = self.getAppyType(fieldName)
sortedUids = self._appy_getSortedField(fieldName)
batchNeeded = startNumber != None
exec 'refUids= self.getRaw%s%s()' % (fieldName[0].upper(),fieldName[1:])
# There may be too much UIDs in sortedUids because these fields
# are not updated when objects are deleted. So we do it now. TODO: do
# such cleaning on object deletion?
toDelete = []
for uid in sortedUids:
if uid not in refUids:
toDelete.append(uid)
for uid in toDelete:
sortedUids.remove(uid)
# Prepare the result
res = SomeObjects()
res.totalNumber = res.batchSize = len(sortedUids)
if batchNeeded:
res.batchSize = appyType.maxPerPage
if startNumber != None:
res.startNumber = startNumber
# Get the needed referred objects
i = res.startNumber
# Is it possible and more efficient to perform a single query in
# uid_catalog and get the result in the order of specified uids?
toUnlink = []
while i < (res.startNumber + res.batchSize):
if i >= res.totalNumber: break
refUid = sortedUids[i]
refObject = self.uid_catalog(UID=refUid)[0].getObject()
i += 1
tool = self.getTool()
if refObject.meta_type != tool.getPortalType(appyType.klass):
toUnlink.append(refObject)
continue
if not ploneObjects:
refObject = refObject.appy()
res.objects.append(refObject)
# Unlink dummy linked objects
if toUnlink:
suffix = '%s%s' % (fieldName[0].upper(), fieldName[1:])
exec 'linkedObjects = self.get%s()' % suffix
for dummyObject in toUnlink:
linkedObjects.remove(dummyObject)
self.getProductConfig().logger.warn('DB error: Ref %s.%s ' \
'contains a %s instance "%s". It was removed.' % \
(self.meta_type, fieldName, dummyObject.meta_type,
dummyObject.getId()))
exec 'self.set%s(linkedObjects)' % suffix
if res.objects and noListIfSingleObj:
if appyType.multiplicity[1] == 1:
res.objects = res.objects[0]
return res
def getAppyRefs(self, name, startNumber=None):
'''Gets the objects linked to me through Ref field named p_name.
If p_startNumber is None, this method returns all referred objects.
If p_startNumber is a number, this method will return x objects,
starting at p_startNumber, x being appyType.maxPerPage.'''
If p_startNumber is a number, this method will return
appyType.maxPerPage objects, starting at p_startNumber.'''
appyType = self.getAppyType(name)
if not appyType.isBack:
return self._appy_getRefs(name, ploneObjects=True,
startNumber=startNumber).__dict__
else:
# Note that pagination is not yet implemented for backward refs.
return SomeObjects(self.getBRefs(appyType.relationship)).__dict__
return appyType.getValue(self, type='zobjects', someObjects=True,
startNumber=startNumber).__dict__
def getSelectableAppyRefs(self, name):
'''p_name is the name of a Ref field. This method returns the list of
@ -780,10 +723,6 @@ class AbstractMixin:
self.reindexObject()
return self.goto(urlBack)
def getFlavour(self):
'''Returns the flavour corresponding to this object.'''
return self.getTool().getFlavour(self.portal_type)
def fieldValueSelected(self, fieldName, vocabValue, dbValue):
'''When displaying a selection box (ie a String with a validator being a
list), must the _vocabValue appear as selected?'''
@ -853,32 +792,6 @@ class AbstractMixin:
rq.appyWrappers[uid] = wrapper
return wrapper
def _appy_getRefsBack(self, fieldName, relName, ploneObjects=False,
noListIfSingleObj=False):
'''This method returns the list of objects linked to this one
through the BackRef corresponding to the Archetypes
relationship named p_relName.'''
# Preamble: must I return a list or a single element?
maxOne = False
if noListIfSingleObj:
# I must get the referred appyType to know its maximum multiplicity.
appyType = self.getAppyType(fieldName)
if appyType.multiplicity[1] == 1:
maxOne = True
# Get the referred objects through the Archetypes relationship.
objs = self.getBRefs(relName)
if maxOne:
res = None
if objs:
res = objs[0]
if res and not ploneObjects:
res = res.appy()
else:
res = objs
if not ploneObjects:
res = [o.appy() for o in objs]
return res
def _appy_showState(self, workflow, stateShow):
'''Must I show a state whose "show value" is p_stateShow?'''
if callable(stateShow):
@ -955,57 +868,6 @@ class AbstractMixin:
exec 'self.%s = pList()' % sortedFieldName
return getattr(self, sortedFieldName)
def _appy_manageRefs(self, created):
'''Every time an object is created or updated, this method updates
the Reference fields accordingly.'''
self._appy_manageRefsFromRequest()
rq = self.REQUEST
# If the creation was initiated by another object, update the ref.
if created and rq.get('nav', None):
# Get the initiator
splitted = rq['nav'].split('.')
if splitted[0] == 'search': return # Not an initiator but a search.
initiator = self.uid_catalog.searchResults(
UID=splitted[1])[0].getObject()
fieldName = splitted[2].split(':')[1]
initiator.appy().link(fieldName, self)
def _appy_manageRefsFromRequest(self):
'''Appy manages itself some Ref fields (with link=True). So here we must
update the Ref fields.'''
fieldsInRequest = [] # Fields present in the request
for requestKey in self.REQUEST.keys():
if requestKey.startswith('appy_ref_'):
fieldName = requestKey[9:]
# Security check
if not self.getAppyType(fieldName).isShowable(self, 'edit'):
continue
fieldsInRequest.append(fieldName)
fieldValue = self.REQUEST[requestKey]
sortedRefField = self._appy_getSortedField(fieldName)
del sortedRefField[:]
if not fieldValue: fieldValue = []
if isinstance(fieldValue, basestring):
fieldValue = [fieldValue]
refObjects = []
for uid in fieldValue:
obj = self.uid_catalog(UID=uid)[0].getObject()
refObjects.append(obj)
sortedRefField.append(uid)
exec 'self.set%s%s(refObjects)' % (fieldName[0].upper(),
fieldName[1:])
# Manage Ref fields that are not present in the request
currentPage = self.REQUEST.get('page', 'main')
for appyType in self.getAllAppyTypes():
if (appyType.type == 'Ref') and not appyType.isBack and \
(appyType.page == currentPage) and \
(appyType.name not in fieldsInRequest):
# If this field is visible, it was not present in the request:
# it means that we must remove any Ref from it.
if appyType.isShowable(self, 'edit'):
exec 'self.set%s%s([])' % (appyType.name[0].upper(),
appyType.name[1:])
getUrlDefaults = {'page':True, 'nav':True}
def getUrl(self, base=None, mode='view', **kwargs):
'''Returns a Appy URL.
@ -1039,12 +901,14 @@ class AbstractMixin:
params = ''
return '%s%s' % (base, params)
def translate(self, label, mapping={}, domain=None, default=None):
def translate(self, label, mapping={}, domain=None, default=None,
language=None):
'''Translates a given p_label into p_domain with p_mapping.'''
cfg = self.getProductConfig()
if not domain: domain = cfg.PROJECTNAME
return self.translation_service.utranslate(
domain, label, mapping, self, default=default)
return self.Control_Panel.TranslationService.utranslate(
domain, label, mapping, self, default=default,
target_language=language)
def getPageLayout(self, layoutType):
'''Returns the layout corresponding to p_layoutType for p_self.'''