Added an AJAX framework within appy.gen, and its first use: a pagination mechanism for producing paginated references in the reference widget.

This commit is contained in:
Gaetan Delannay 2009-10-25 21:42:08 +01:00
parent 4c4b2d0f87
commit 605c42d94e
20 changed files with 546 additions and 187 deletions

View file

@ -79,13 +79,16 @@ class ToolMixin(AbstractMixin):
res.append({'title': flavour.title, 'number':flavour.number})
return res
def getAppName(self):
'''Returns the name of this application.'''
return self.getProductConfig().PROJECTNAME
def getAppFolder(self):
'''Returns the folder at the root of the Plone site that is dedicated
to this application.'''
portal = self.getProductConfig().getToolByName(
self, 'portal_url').getPortalObject()
appName = self.getProductConfig().PROJECTNAME
return getattr(portal, appName)
cfg = self.getProductConfig()
portal = cfg.getToolByName(self, 'portal_url').getPortalObject()
return getattr(portal, self.getAppName())
def getRootClasses(self):
'''Returns the list of root classes for this application.'''
@ -275,9 +278,9 @@ class ToolMixin(AbstractMixin):
for importPath in importPaths:
if not importPath: continue
objectId = os.path.basename(importPath)
self.appy().create(appyClass, id=objectId)
self.appy().create(appyClass, id=objectId, _data=importPath)
self.plone_utils.addPortalMessage(self.translate('import_done'))
return rq.RESPONSE.redirect(rq['HTTP_REFERER'])
return self.goto(rq['HTTP_REFERER'])
def isAlreadyImported(self, contentType, importPath):
appFolder = self.getAppFolder()

View file

@ -6,11 +6,11 @@
The AbstractMixin defined hereafter is the base class of any mixin.'''
# ------------------------------------------------------------------------------
import os, os.path, sys, types
import os, os.path, sys, types, mimetypes
import appy.gen
from appy.gen import String
from appy.gen.utils import FieldDescr, GroupDescr, PhaseDescr, StateDescr, \
ValidationErrors, sequenceTypes
ValidationErrors, sequenceTypes, RefObjects
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
from appy.gen.plone25.utils import updateRolesForPermission, getAppyRequest
@ -20,10 +20,6 @@ class AbstractMixin:
inherits from this class. It contains basic functions allowing to
minimize the amount of generated code.'''
def getAppyAttribute(self, name):
'''Returns method or attribute value corresponding to p_name.'''
return eval('self.%s' % name)
def createOrUpdate(self, created):
'''This method creates (if p_created is True) or updates an object.
In the case of an object creation, p_self is a temporary object
@ -35,11 +31,16 @@ class AbstractMixin:
if created:
obj = self.portal_factory.doCreate(self, self.id) # portal_factory
# creates the final object from the temp object.
obj.processForm()
if created and (obj._appy_meta_type == 'tool'):
# We are in the special case where the tool itself is being created.
# In this case, we do not process form data.
pass
else:
obj.processForm()
# Get the current language and put it in the request
if rq.form.has_key('current_lang'):
rq.form['language'] = rq.form.get('current_lang')
#if rq.form.has_key('current_lang'):
# rq.form['language'] = rq.form.get('current_lang')
# Manage references
obj._appy_manageRefs(created)
@ -80,7 +81,7 @@ class AbstractMixin:
objId = self.generateUniqueId(rq.get('type_name'))
urlBack = '%s/portal_factory/%s/%s/skyn/edit' % \
(baseUrl, rq.get('type_name'), objId)
return rq.RESPONSE.redirect(urlBack)
return self.goto(urlBack)
def onUpdate(self):
'''This method is executed when a user wants to update an object.
@ -95,11 +96,15 @@ class AbstractMixin:
# Go back to the consult view if the user clicked on 'Cancel'
if rq.get('buttonCancel', None):
urlBack = '%s/skyn/view?phase=%s&pageName=%s' % (
self.absolute_url(), rq.get('phase'), rq.get('pageName'))
if '/portal_factory/' in self.absolute_url():
# Go back to the Plone site (no better solution at present).
urlBack = self.portal_url.getPortalObject().absolute_url()
else:
urlBack = '%s/skyn/view?phase=%s&pageName=%s' % (
self.absolute_url(), rq.get('phase'), rq.get('pageName'))
self.plone_utils.addPortalMessage(
self.translate('Changes canceled.', domain='plone'))
return rq.RESPONSE.redirect(urlBack)
return self.goto(urlBack)
# Trigger field-specific validation
self.validate(REQUEST=rq, errors=errors, data=1, metadata=0)
@ -124,7 +129,7 @@ class AbstractMixin:
obj.translate('Changes saved.', domain='plone'))
urlBack = '%s/skyn/view?phase=%s&pageName=%s' % (
obj.absolute_url(), rq.get('phase'), rq.get('pageName'))
return rq.RESPONSE.redirect(urlBack)
return self.goto(urlBack)
elif rq.get('buttonPrevious', None):
# Go to the edit view (previous page) for this object
rq.set('fieldset', rq.get('previousPage'))
@ -139,69 +144,108 @@ class AbstractMixin:
msg = self.translate('delete_done')
self.delete()
self.plone_utils.addPortalMessage(msg)
rq.RESPONSE.redirect(rq['HTTP_REFERER'])
self.goto(rq['HTTP_REFERER'])
def getAppyType(self, fieldName):
'''Returns the Appy type corresponding to p_fieldName.'''
def goto(self, url):
'''Brings the user to some p_url after an action has been executed.'''
return self.REQUEST.RESPONSE.redirect(url)
def getAppyAttribute(self, name):
'''Returns method or attribute value corresponding to p_name.'''
return eval('self.%s' % name)
def getAppyType(self, fieldName, forward=True):
'''Returns the Appy type corresponding to p_fieldName. If you want to
get the Appy type corresponding to a backward field, set p_forward
to False and specify the corresponding Archetypes relationship in
p_fieldName.'''
res = None
if fieldName == 'id': return res
if self.wrapperClass:
baseClass = self.wrapperClass.__bases__[-1]
try:
# If I get the attr on self instead of baseClass, I get the
# property field that is redefined at the wrapper level.
appyType = getattr(baseClass, fieldName)
res = self._appy_getTypeAsDict(fieldName, appyType, baseClass)
except AttributeError:
# Check for another parent
if self.wrapperClass.__bases__[0].__bases__:
baseClass = self.wrapperClass.__bases__[0].__bases__[-1]
try:
appyType = getattr(baseClass, fieldName)
res = self._appy_getTypeAsDict(fieldName, appyType,
baseClass)
except AttributeError:
pass
if forward:
if fieldName == 'id': return res
if self.wrapperClass:
baseClass = self.wrapperClass.__bases__[-1]
try:
# If I get the attr on self instead of baseClass, I get the
# property field that is redefined at the wrapper level.
appyType = getattr(baseClass, fieldName)
res = self._appy_getTypeAsDict(fieldName, appyType, baseClass)
except AttributeError:
# Check for another parent
if self.wrapperClass.__bases__[0].__bases__:
baseClass = self.wrapperClass.__bases__[0].__bases__[-1]
try:
appyType = getattr(baseClass, fieldName)
res = self._appy_getTypeAsDict(fieldName, appyType,
baseClass)
except AttributeError:
pass
else:
referers = self.getProductConfig().referers
for appyType, rel in referers[self.__class__.__name__]:
if rel == fieldName:
res = appyType.__dict__
res['backd'] = appyType.back.__dict__
return res
def _appy_getRefs(self, fieldName, ploneObjects=False,
noListIfSingleObj=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.'''
res = []
sortedFieldName = '_appy_%s' % fieldName
exec 'objs = self.get%s%s()' % (fieldName[0].upper(), fieldName[1:])
if objs:
if type(objs) != list:
objs = [objs]
objectsUids = [o.UID() for o in objs]
sortedObjectsUids = getattr(self, sortedFieldName)
# The list of UIDs may contain too much UIDs; indeed, when deleting
# objects, the list of UIDs are not updated.
uidsToDelete = []
for uid in sortedObjectsUids:
try:
uidIndex = objectsUids.index(uid)
obj = objs[uidIndex]
if not ploneObjects:
obj = obj._appy_getWrapper(force=True)
res.append(obj)
except ValueError:
uidsToDelete.append(uid)
# Delete unused UIDs
for uid in uidsToDelete:
sortedObjectsUids.remove(uid)
if res and noListIfSingleObj:
appyType = self.getAppyType(fieldName)
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 = getattr(self, '_appy_%s' % 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 = RefObjects()
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 is possible and more efficient to perform a single query in
# uid_catalog and get the result in the order of specified uids?
while i < (res.startNumber + res.batchSize):
if i >= res.totalNumber: break
refUid = sortedUids[i]
refObject = self.uid_catalog(UID=refUid)[0].getObject()
if not ploneObjects:
refObject = refObject.appy()
res.objects.append(refObject)
i += 1
if res.objects and noListIfSingleObj:
if appyType['multiplicity'][1] == 1:
res = res[0]
res.objects = res.objects[0]
return res
def getAppyRefs(self, fieldName):
'''Gets the objects linked to me through p_fieldName.'''
return self._appy_getRefs(fieldName, ploneObjects=True)
def getAppyRefs(self, fieldName, forward=True, startNumber=None):
'''Gets the objects linked to me through p_fieldName. If you need to
get a backward reference, set p_forward to False and specify the
corresponding Archetypes relationship in p_fieldName.
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 forward:
return self._appy_getRefs(fieldName, ploneObjects=True,
startNumber=startNumber).__dict__
else:
# Note Pagination is not yet implemented for backward ref.
return RefObjects(self.getBRefs(fieldName)).__dict__
def getAppyRefIndex(self, fieldName, obj):
'''Gets the position of p_obj within Ref field named p_fieldName.'''
@ -211,8 +255,8 @@ class AbstractMixin:
return res
def getAppyBackRefs(self):
'''Returns the list of back references (=types) that are defined for
this class.'''
'''Returns the list of back references (=types, not objects) that are
defined for this class.'''
className = self.__class__.__name__
referers = self.getProductConfig().referers
res = []
@ -303,6 +347,11 @@ class AbstractMixin:
res = fieldDescr['show'](obj)
else:
res = fieldDescr['show']
# Take into account possible values 'view' and 'edit' for 'show' param.
if (res == 'view' and isEdit) or (res == 'edit' and not isEdit):
res = False
else:
res = True
return res
def getAppyFields(self, isEdit, page):
@ -460,22 +509,12 @@ class AbstractMixin:
'''This method is called when the user wants to change order of an
item in a reference field.'''
rq = self.REQUEST
# Move the item up (-1), down (+1) or at a given position ?
# Move the item up (-1), down (+1) ?
move = -1 # Move up
if rq['move'] == 'down':
move = 1 # Down
isDelta = True
if rq.get('moveDown.x', None) != None:
move = 1 # Move down
elif rq.get('moveSeveral.x', None) != None:
try:
move = int(rq.get('moveValue'))
# In this case, it is not a delta value; it is the new position
# where the item must be moved.
isDelta = False
except ValueError:
self.plone_utils.addPortalMessage(
self.translate('ref_invalid_index'))
self.changeRefOrder(rq['fieldName'], rq['refObjectUid'], move, isDelta)
return rq.RESPONSE.redirect(rq['HTTP_REFERER'])
def getWorkflow(self, appy=True):
'''Returns the Appy workflow instance that is relevant for this
@ -563,24 +602,34 @@ class AbstractMixin:
def executeAppyAction(self, actionName, reindex=True):
'''Executes action with p_fieldName on this object.'''
appyClass = self.wrapperClass.__bases__[1]
res = getattr(appyClass, actionName)(self._appy_getWrapper(force=True))
appyType = getattr(appyClass, actionName)
actionRes = appyType(self._appy_getWrapper(force=True))
self.reindexObject()
return res
return appyType.result, actionRes
def onExecuteAppyAction(self):
'''This method is called every time a user wants to execute an Appy
action on an object.'''
rq = self.REQUEST
res, msg = self.executeAppyAction(rq['fieldName'])
resultType, actionResult = self.executeAppyAction(rq['fieldName'])
successfull, msg = actionResult
if not msg:
# Use the default i18n messages
suffix = 'ko'
if res:
if successfull:
suffix = 'ok'
label='%s_action_%s' % (self.getLabelPrefix(rq['fieldName']),suffix)
msg = self.translate(label)
self.plone_utils.addPortalMessage(msg)
return rq.RESPONSE.redirect(rq['HTTP_REFERER'])
if (resultType == 'computation') or not successfull:
self.plone_utils.addPortalMessage(msg)
return self.goto(rq['HTTP_REFERER'])
else:
# msg does not contain a message, but a complete file to show as is.
# (or, if your prefer, the message must be shown directly to the
# user, not encapsulated in a Plone page).
res = self.getProductConfig().File(msg.name, msg.name, msg,
content_type=mimetypes.guess_type(msg.name)[0])
return res.index_html(rq, rq.RESPONSE)
def onTriggerTransition(self):
'''This method is called whenever a user wants to trigger a workflow
@ -597,7 +646,7 @@ class AbstractMixin:
msg = self.translate(u'Your content\'s status has been modified.',
domain='plone')
self.plone_utils.addPortalMessage(msg)
return rq.RESPONSE.redirect(urlBack)
return self.goto(urlBack)
def callAppySelect(self, selectMethod, brains):
'''Selects objects from a Reference field.'''
@ -616,11 +665,14 @@ class AbstractMixin:
return res
def getCssClasses(self, appyType, asSlave=True):
'''Gets the CSS classes (used for master/slave relationships) for this
object, either as slave (p_asSlave=True) either as master. The HTML
element on which to define the CSS class for a slave or a master is
'''Gets the CSS classes (used for master/slave relationships, or if the
field corresponding to p_appyType is focus) for this object,
either as slave (p_asSlave=True) or as master. The HTML element on
which to define the CSS class for a slave or a master is
different. So this method is called either for getting CSS classes
as slave or as master.'''
as slave or as master. We set the focus-specific CSS class only when
p_asSlave is True, because we this place as being the "standard" one
for specifying CSS classes for a field.'''
res = ''
if not asSlave and appyType['slaves']:
res = 'appyMaster master_%s' % appyType['id']
@ -628,6 +680,11 @@ class AbstractMixin:
res = 'slave_%s' % appyType['master'].id
res += ' slaveValue_%s_%s' % (appyType['master'].id,
appyType['masterValue'])
# Add the focus-specific class if needed
if appyType['focus']:
prefix = ''
if res: prefix = ' '
res += prefix + 'appyFocus'
return res
def fieldValueSelected(self, fieldName, value, vocabValue):
@ -999,9 +1056,18 @@ class AbstractMixin:
exec 'self.set%s%s([])' % (fieldName[0].upper(),
fieldName[1:])
def getUrl(self):
'''This method returns the URL of the consult view for this object.'''
return self.absolute_url() + '/skyn/view'
def getUrl(self, t='view', **kwargs):
'''This method returns various URLs about this object.'''
baseUrl = self.absolute_url()
params = ''
for k, v in kwargs.iteritems(): params += '&%s=%s' % (k, v)
params = params[1:]
if t == 'showRef':
chunk = '/skyn/ajax?objectUid=%s&page=ref&' \
'macro=showReferenceContent&' % self.UID()
return baseUrl + chunk + params
else: # We consider t=='view'
return baseUrl + '/skyn/view' + params
def translate(self, label, mapping={}, domain=None, default=None):
'''Translates a given p_label into p_domain with p_mapping.'''