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

@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
import re
import re, time
from appy.gen.utils import sequenceTypes, PageDescr
# Default Appy permissions -----------------------------------------------------
@ -33,7 +33,7 @@ class Type:
def __init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, searchable,
specificReadPermission, specificWritePermission, width,
height, master, masterValue):
height, master, masterValue, focus):
# The validator restricts which values may be defined. It can be an
# interval (1,None), a list of string values ['choice1', 'choice2'],
# a regular expression, a custom function, a Selection instance, etc.
@ -86,6 +86,9 @@ class Type:
self.master.slaves.append(self)
# When master has some value(s), there is impact on this field.
self.masterValue = masterValue
# If a field must retain attention in a particular way, set focus=True.
# It will be rendered in a special way.
self.focus = focus
self.id = id(self)
self.type = self.__class__.__name__
self.pythonType = None # The True corresponding Python type
@ -106,11 +109,12 @@ class Integer(Type):
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
width=None, height=None, master=None, masterValue=None,
focus=False):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
self.pythonType = long
class Float(Type):
@ -118,11 +122,12 @@ class Float(Type):
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
width=None, height=None, master=None, masterValue=None,
focus=False):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
self.pythonType = float
class String(Type):
@ -141,11 +146,12 @@ class String(Type):
default=None, optional=False, editDefault=False, format=LINE,
show=True, page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
width=None, height=None, master=None, masterValue=None,
focus=False):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, searchable,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
self.format = format
def isSelection(self):
'''Does the validator of this type definition define a list of values
@ -166,11 +172,12 @@ class Boolean(Type):
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
width=None, height=None, master=None, masterValue=None,
focus=False):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, searchable,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
self.pythonType = bool
class Date(Type):
@ -179,15 +186,19 @@ class Date(Type):
WITHOUT_HOUR = 1
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False,
format=WITH_HOUR, show=True, page='main', group=None, move=0,
searchable=False,
format=WITH_HOUR, startYear=time.localtime()[0]-10,
endYear=time.localtime()[0]+10,
show=True, page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
width=None, height=None, master=None, masterValue=None,
focus=False):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, searchable,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
self.format = format
self.startYear = startYear
self.endYear = endYear
class File(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
@ -195,11 +206,11 @@ class File(Type):
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None,
isImage=False):
focus=False, isImage=False):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
self.isImage = isImage
class Ref(Type):
@ -208,13 +219,14 @@ class Ref(Type):
editDefault=False, add=False, link=True, unlink=False,
back=None, isBack=False, show=True, page='main', group=None,
showHeaders=False, shownInfo=(), wide=False, select=None,
move=0, searchable=False,
maxPerPage=30, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
width=None, height=None, master=None, masterValue=None,
focus=False):
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
self.klass = klass
self.attribute = attribute
self.add = add # May the user add new objects through this ref ?
@ -231,6 +243,8 @@ class Ref(Type):
# as possible
self.select = select # If a method is defined here, it will be used to
# filter the list of available tied objects.
self.maxPerPage = maxPerPage # Maximum number of referenced objects
# shown at once.
class Computed(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
@ -238,11 +252,11 @@ class Computed(Type):
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, method=None, plainText=True,
master=None, masterValue=None):
master=None, masterValue=None, focus=False):
Type.__init__(self, None, multiplicity, index, default, optional,
False, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
self.method = method # The method used for computing the field value
self.plainText = plainText # Does field computation produce pain text
# or XHTML?
@ -256,13 +270,17 @@ class Action(Type):
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, action=None, master=None,
masterValue=None):
width=None, height=None, action=None, result='computation',
master=None, masterValue=None, focus=False):
Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
self.action = action # Can be a single method or a list/tuple of methods
self.result = result # 'computation' means that the action will simply
# compute things and redirect the user to the same page, with some
# status message about execution of the action. 'file' means that the
# result is the binary content of a file that the user will download.
def __call__(self, obj):
'''Calls the action on p_obj.'''
@ -274,7 +292,10 @@ class Action(Type):
actRes = act(obj)
if type(actRes) in sequenceTypes:
res[0] = res[0] and actRes[0]
res[1] = res[1] + '\n' + actRes[1]
if self.result == 'file':
res[1] = res[1] + actRes[1]
else:
res[1] = res[1] + '\n' + actRes[1]
else:
res[0] = res[0] and actRes
else:
@ -284,8 +305,8 @@ class Action(Type):
res = list(actRes)
else:
res = [actRes, '']
# If res is None (ie the user-defined action did not return anything)
# we consider the action as successfull.
# If res is None (ie the user-defined action did not return
# anything), we consider the action as successfull.
if res[0] == None: res[0] = True
except Exception, e:
res = (False, str(e))
@ -298,11 +319,12 @@ class Info(Type):
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None):
width=None, height=None, master=None, masterValue=None,
focus=False):
Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, move, False,
specificReadPermission, specificWritePermission, width,
height, master, masterValue)
height, master, masterValue, focus)
# Workflow-specific types ------------------------------------------------------
class State:

View file

@ -82,6 +82,8 @@ class ArchetypeFieldDescriptor:
self.widgetType = 'CalendarWidget'
if self.appyType.format == Date.WITHOUT_HOUR:
self.widgetParams['show_hm'] = False
self.widgetParams['starting_year'] = self.appyType.startYear
self.widgetParams['ending_year'] = self.appyType.endYear
elif self.appyType.type == 'Float':
self.widgetType = 'DecimalWidget'
elif self.appyType.type == 'File':

View file

@ -120,6 +120,10 @@ class Generator(AbstractGenerator):
msg('no_elem_selected', '', msg.NO_SELECTION),
msg('delete_confirm', '', msg.DELETE_CONFIRM),
msg('delete_done', '', msg.DELETE_DONE),
msg('goto_first', '', msg.GOTO_FIRST),
msg('goto_previous', '', msg.GOTO_PREVIOUS),
msg('goto_next', '', msg.GOTO_NEXT),
msg('goto_last', '', msg.GOTO_LAST),
]
# Create basic files (config.py, Install.py, etc)
self.generateTool()
@ -408,7 +412,7 @@ class Generator(AbstractGenerator):
getterName = 'get%s%s' % (attrName[0].upper(), attrName[1:])
if isinstance(appyType, Ref):
res += blanks + 'return self.o._appy_getRefs("%s", ' \
'noListIfSingleObj=True)\n' % attrName
'noListIfSingleObj=True).objects\n' % attrName
elif isinstance(appyType, Computed):
res += blanks + 'appyType = getattr(self.klass, "%s")\n' % attrName
res += blanks + 'return self.o.getComputedValue(' \
@ -417,6 +421,8 @@ class Generator(AbstractGenerator):
res += blanks + 'v = self.o.%s()\n' % getterName
res += blanks + 'if not v: return None\n'
res += blanks + 'else: return FileWrapper(v)\n'
elif isinstance(appyType, String) and appyType.isMultiValued():
res += blanks + 'return list(self.o.%s())\n' % getterName
else:
if attrName in ArchetypeFieldDescriptor.specialParams:
getterName = attrName.capitalize()

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.'''

25
gen/plone25/skin/ajax.pt Normal file
View file

@ -0,0 +1,25 @@
<tal:comment replace="nothing">
This page is called by a XmlHttpRequest object. It requires parameters "page" and "macro":
they are used to call the macro that will render the HTML chunk to be returned to the browser.
It also requires parameters "objectUid", which is the UID of the related object. The object will
be available to the macro as "contextObj".
It can also have a parameter "action", that refers to a method that will be triggered on
contextObj before returning the result of the macro to the browser.
</tal:comment>
<tal:ajax define="page request/page;
macro request/macro;
macroPath python: 'here/%s/macros/%s' % (page, macro);
contextObj python: context.uid_catalog(UID=request['objectUid'])[0].getObject();
action request/action|nothing;
response request/RESPONSE;
member context/portal_membership/getAuthenticatedMember;
portal context/portal_url/getPortalObject;
portal_url context/portal_url/getPortalPath;
dummy python:response.setHeader('Content-Type','text/html;;charset=utf-8');
dummy2 python:response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT');
dummy3 python:response.setHeader('CacheControl', 'no-cache')">
<tal:executeAction condition="action">
<tal:do define="dummy python: contextObj.getAppyAttribute('on'+action)()" omit-tag=""/>
</tal:executeAction>
<metal:callMacro use-macro="python: context.get(page).macros.get(macro)"/>
</tal:ajax>

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

View file

@ -74,7 +74,7 @@
<form name="edit_form" method="post" enctype="multipart/form-data"
class="enableUnloadProtection atBaseEditForm"
tal:attributes="action python: contextObj.absolute_url()+'/skyn/do'">
<div metal:use-macro="here/skyn/macros/macros/listFields" />
<div metal:use-macro="here/skyn/macros/macros/listFields" /><br/>
<input type="hidden" name="action" value="Update"/>
<input type="hidden" name="fieldset" tal:attributes="value fieldset"/>
<input type="hidden" name="pageName" tal:attributes="value pageName"/>

View file

@ -107,7 +107,10 @@
<input type="hidden" name="action" value="ExecuteAppyAction"/>
<input type="hidden" name="objectUid" tal:attributes="value contextObj/UID"/>
<input type="hidden" name="fieldName" tal:attributes="value field/getName"/>
<input type="submit" name="do" tal:attributes="value label"/>
<input type="submit" name="do" tal:attributes="value label" onClick="javascript:;"/>
<tal:comment replace="nothing">The previous onClick is simply used to prevent Plone
from adding a CSS class that displays a popup when the user triggers the form multiple
times.</tal:comment>
</form>
</div>
@ -115,7 +118,8 @@
tal:define="field fieldDescr/atField|widgetDescr/atField;
appyType fieldDescr/appyType|widgetDescr/appyType;
showLabel showLabel|python:True;
label python: tool.translate(field.widget.label_msgid);
labelId field/widget/label_msgid;
label python: tool.translate(labelId);
descrId field/widget/description_msgid|python:'';
description python: tool.translate(descrId)"
tal:attributes="class python: contextObj.getCssClasses(appyType, asSlave=True)">
@ -157,8 +161,7 @@
<tal:comment replace="nothing">For other fields like Refs we use specific view/edit macros.</tal:comment>
<tal:viewRef condition="python: (not isEdit) and (appyType['type'] == 'Ref')">
<tal:ref define="isBack python:False;
fieldRel python:field.relationship;
objs python:contextObj.getAppyRefs(field.getName());
fieldName field/getName;
innerRef innerRef|python:False">
<metal:viewRef use-macro="here/skyn/ref/macros/showReference" />
</tal:ref>
@ -186,10 +189,9 @@
<div metal:define-macro="showBackwardField"
tal:define="isBack python:True;
appyType widgetDescr/appyType;
fieldRel widgetDescr/fieldRel;
objs python:contextObj.getBRefs(fieldRel);
label python:contextObj.translate('%s_%s_back' % (contextObj.meta_type, appyType['backd']['attribute']));
description python:'';
fieldName widgetDescr/fieldRel;
labelId python: '%s_%s_back' % (contextObj.meta_type, appyType['backd']['attribute']);
descrId python: '';
innerRef innerRef|python:False">
<div metal:use-macro="here/skyn/ref/macros/showReference" />
</div>
@ -309,6 +311,63 @@
<tal:comment replace="nothing">"Static" javascripts</tal:comment>
<script language="javascript">
<!--
// AJAX machinery
var xhrObjects = new Array(); // An array of XMLHttpRequest objects
function XhrObject() { // Wraps a XmlHttpRequest object
this.freed = 1; // Is this xhr object already dealing with a request or not?
this.xhr = false;
if (window.XMLHttpRequest) this.xhr = new XMLHttpRequest();
else this.xhr = new ActiveXObject("Microsoft.XMLHTTP");
this.hook = ''; // The ID of the HTML element in the page that will be
// replaced by result of executing the Ajax request.
}
function getAjaxChunk(pos) {
// This function is the callback called by the AJAX machinery (see function
// askAjaxChunk below) when an Ajax response is available.
// First, find back the correct XMLHttpRequest object
if ( (typeof(xhrObjects[pos]) != 'undefined') &&
(xhrObjects[pos].freed == 0)) {
var hook = xhrObjects[pos].hook;
if (xhrObjects[pos].xhr.readyState == 1) {
// The request has been initialized: display the waiting radar
var hookElem = document.getElementById(hook);
if (hookElem) hookElem.innerHTML = "<div align=\"center\"><img src=\"skyn/waiting.gif\"/><\/div>";
}
if (xhrObjects[pos].xhr.readyState == 4) {
// We have received the HTML chunk
var hookElem = document.getElementById(hook);
if (hookElem && (xhrObjects[pos].xhr.status == 200)) {
hookElem.innerHTML = xhrObjects[pos].xhr.responseText;
}
xhrObjects[pos].freed = 1;
}
}
}
function askAjaxChunk(hook, url) {
// This function will ask to get a chunk of HTML on the server by
// triggering a XMLHttpRequest.
// First, get a non-busy XMLHttpRequest object.
var pos = -1;
for (var i=0; i < xhrObjects.length; i++) {
if (xhrObjects[i].freed == 1) { pos = i; break; }
}
if (pos == -1) {
pos = xhrObjects.length;
xhrObjects[pos] = new XhrObject();
}
xhrObjects[pos].hook = hook;
if (xhrObjects[pos].xhr) {
xhrObjects[pos].freed = 0;
// Perform the asynchronous HTTP GET
xhrObjects[pos].xhr.open('GET', url, true);
xhrObjects[pos].xhr.onreadystatechange = function() { getAjaxChunk(pos); }
if (window.XMLHttpRequest) { xhrObjects[pos].xhr.send(null); }
else if (window.ActiveXObject) { xhrObjects[pos].xhr.send(); }
}
}
// Function used by checkbox widgets for having radio-button-like behaviour
function toggleCheckbox(visibleCheckbox, hiddenBoolean) {
vis = document.getElementById(visibleCheckbox);
@ -805,3 +864,47 @@
<metal:phases use-macro="here/skyn/macros/macros/phases"/>
</dt>
</metal:portletContent>
<tal:comment replace="nothing">
Buttons for navigating among a list of elements (next, back, first, last, etc).
</tal:comment>
<metal:appyNavigate define-macro="appyNavigate" tal:condition="python: totalNumber &gt; batchSize">
<table cellpadding="0" cellspacing="0" align="right" class="appyNav"
tal:define="baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId}) + '&%s_startNumber=' % ajaxHookId">
<tr>
<tal:comment replace="nothing">Go to the first page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: (startNumber != 0) and (startNumber != batchSize)"
tal:attributes="src string: $portal_url/skyn/arrowLeftDouble.png;
title python: tool.translate('goto_first');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+'0')"/></td>
<tal:comment replace="nothing">Go to the previous page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: startNumber != 0"
tal:define="sNumber python: startNumber - batchSize"
tal:attributes="src string: $portal_url/skyn/arrowLeftSimple.png;
title python: tool.translate('goto_previous');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td>
<tal:comment replace="nothing">Explain which elements are currently shown</tal:comment>
<td class="discreet">&nbsp;
<span tal:replace="python: startNumber+1"/>
<img tal:attributes="src string: $portal_url/skyn/to.png"/>
<span tal:replace="python: startNumber+len(objs)"/>&nbsp;&nbsp;
</td>
<tal:comment replace="nothing">Go to the next page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: sNumber &lt; totalNumber"
tal:define="sNumber python: startNumber + batchSize"
tal:attributes="src string: $portal_url/skyn/arrowRightSimple.png;
title python: tool.translate('goto_next');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td>
<tal:comment replace="nothing">Go to the last page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: (startNumber != sNumber) and (startNumber != sNumber-batchSize)"
tal:define="lastPageIsIncomplete python: totalNumber % batchSize;
nbOfCompletePages python: totalNumber/batchSize;
nbOfCountedPages python: test(lastPageIsIncomplete, nbOfCompletePages, nbOfCompletePages-1);
sNumber python: (nbOfCountedPages*batchSize)"
tal:attributes="src string: $portal_url/skyn/arrowRightDouble.png;
title python: tool.translate('goto_last');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td>
</tr>
</table>
</metal:appyNavigate>

View file

@ -21,31 +21,28 @@
<img src="edit.gif" title="label_edit" i18n:domain="plone" i18n:attributes="title" />
</a></td>
<tal:comment replace="nothing">Delete the element</tal:comment>
<td class="noPadding"><a tal:attributes="href python: obj.absolute_url() + '/delete_confirmation'"
tal:condition="python: member.has_permission('Delete objects', obj)">
<img src="delete_icon.gif" title="label_remove" i18n:domain="plone" i18n:attributes="title" />
</a></td>
<td class="noPadding">
<img tal:condition="python: member.has_permission('Delete objects', obj)"
src="delete_icon.gif" title="Delete" i18n:domain="plone" i18n:attributes="title" style="cursor:pointer"
tal:attributes="onClick python:'javascript:onDeleteObject(\'%s\')' % obj.UID()"/>
</td>
<tal:comment replace="nothing">Arrows for moving objects up or down</tal:comment>
<td class="noPadding" tal:condition="python: len(objs)&gt;1">
<form tal:condition="python: member.has_permission('Modify portal content', obj)"
tal:attributes="action python: contextObj.absolute_url() + '/skyn/do'"
tal:define="objectIndex python:contextObj.getAppyRefIndex(field.getName(), obj)">
<input type="hidden" name="action" value="ChangeRefOrder"/>
<input type="hidden" name="fieldName" tal:attributes="value field/getName"/>
<input type="hidden" name="refObjectUid" tal:attributes="value obj/UID"/>
<tal:comment replace="nothing">Arrow up</tal:comment>
<span tal:condition="python: objectIndex &gt; 0">
<input type="image" name="moveUp" class="imageInput"
tal:attributes="src string: $portal_url/skyn/arrowUp.png;
title python: tool.translate('move_up')"/>
</span>
<tal:comment replace="nothing">Arrow down</tal:comment>
<span tal:condition="python: objectIndex &lt; (len(objs)-1)">
<input type="image" name="moveDown" class="imageInput"
tal:attributes="src string: $portal_url/skyn/arrowDown.png;
title python: tool.translate('move_down')"/>
</span>
</form>
<td class="noPadding" tal:condition="python: (len(objs)&gt;1) and member.has_permission('Modify portal content', obj)">
<tal:moveRef define="objectIndex python:contextObj.getAppyRefIndex(fieldName, obj);
baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId, '%s_startNumber' % ajaxHookId: startNumber, 'action':'ChangeRefOrder', 'refObjectUid': obj.UID()})">
<tal:comment replace="nothing">Move up</tal:comment>
<img tal:define="ajaxUrl python: baseUrl + '&move=up'" tal:condition="python: objectIndex &gt; 0"
tal:attributes="src string: $portal_url/skyn/arrowUp.png;
title python: tool.translate('move_up');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, ajaxUrl)"
style="cursor:pointer"/>
<tal:comment replace="nothing">Move down</tal:comment>
<img tal:define="ajaxUrl python: baseUrl + '&move=down'" tal:condition="python: objectIndex &lt; (totalNumber-1)"
tal:attributes="src string: $portal_url/skyn/arrowDown.png;
title python: tool.translate('move_down');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, ajaxUrl)"
style="cursor:pointer"/>
</tal:moveRef>
</td>
</tr>
</table>
@ -58,18 +55,59 @@
<img style="cursor:pointer" tal:condition="showPlusIcon"
tal:attributes="src string:$portal_url/skyn/plus.png;
title python: tool.translate('add_ref');
onClick python: 'href: window.location=\'%s/skyn/do?action=Create&initiator=%s&field=%s&type_name=%s\'' % (folder.absolute_url(), contextObj.UID(), field.getName(), linkedPortalType)"/>
onClick python: 'href: window.location=\'%s/skyn/do?action=Create&initiator=%s&field=%s&type_name=%s\'' % (folder.absolute_url(), contextObj.UID(), fieldName, linkedPortalType)"/>
</metal:plusIcon>
<tal:comment replace="nothing">
This macro shows a reference field. More precisely, it shows nothing, but calls
a Javascript function that will asynchonously call (via a XmlHttpRequest object) the
macro 'showReferenceContent' defined below, that will really show content.
It requires:
- isBack (bool) Is the reference a backward or forward reference?
- fieldName (string) The name of the reference field (if it is a forward reference)
or the name of the Archetypes relationship (if it is a backward reference)
- innerRef (bool) Are we rendering a reference within a reference or not?
- contextObj (object) the object from which the reference starts
- labelId (string) the i18n id of the reference field label
- descrId (string) the i18n id of the reference field description
</tal:comment>
<div metal:define-macro="showReference"
tal:define="folder python: test(contextObj.isPrincipiaFolderish, contextObj, contextObj.getParentNode());
tal:define="ajaxHookId python: contextObj.UID()+fieldName;
ajaxUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId})"
tal:attributes="id ajaxHookId">
<script language="javascript"
tal:content="python: 'askAjaxChunk(\'%s\',\'%s\')' % (ajaxHookId, ajaxUrl)">
</script>
</div>
<tal:comment replace="nothing">
This macro is called by a XmlHttpRequest for displaying the paginated referred objects
of a reference field.
</tal:comment>
<div metal:define-macro="showReferenceContent"
tal:define="fieldName request/fieldName;
isBack python: test(request['isBack']=='True', True, False);
innerRef python: test(request['innerRef']=='True', True, False);
labelId request/labelId;
descrId request/descrId;
ajaxHookId python: contextObj.UID()+fieldName;
startNumber python: int(request.get('%s_startNumber' % ajaxHookId, 0));
appyType python: contextObj.getAppyType(fieldName, not isBack);
tool contextObj/getTool;
refObjects python:contextObj.getAppyRefs(fieldName, not isBack, startNumber);
objs refObjects/objects;
totalNumber refObjects/totalNumber;
batchSize refObjects/batchSize;
folder python: test(contextObj.isPrincipiaFolderish, contextObj, contextObj.getParentNode());
flavour python:tool.getFlavour(contextObj);
linkedPortalType python:flavour.getPortalType(appyType['klass']);
addPermission python: '%s: Add %s' % (appName, linkedPortalType);
addPermission python: '%s: Add %s' % (tool.getAppName(), linkedPortalType);
multiplicity python:test(isBack, appyType['backd']['multiplicity'], appyType['multiplicity']);
maxReached python:(multiplicity[1] != None) and (len(objs) >= multiplicity[1]);
showPlusIcon python:not isBack and appyType['add'] and not maxReached and member.has_permission(addPermission, folder);
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1)">
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1);
label python: tool.translate(labelId);
description python: tool.translate(descrId)">
<tal:comment replace="nothing">This macro displays the Reference widget on a "consult" page.
@ -106,6 +144,7 @@
<fieldset tal:attributes="class python:test(innerRef, 'innerAppyFieldset', '')">
<legend tal:condition="python: not innerRef or showPlusIcon">
<span tal:condition="not: innerRef" tal:content="label"/>
<tal:numberOfRefs>(<span tal:replace="totalNumber"/>)</tal:numberOfRefs>
<metal:plusIcon use-macro="here/skyn/ref/macros/plusIcon"/>
</legend>
@ -113,6 +152,9 @@
<p tal:condition="python: not innerRef and description"
tal:content="description" class="discreet" ></p>
<tal:comment replace="nothing">Appy (top) navigation</tal:comment>
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
<tal:comment replace="nothing">No object is present</tal:comment>
<p tal:condition="not:objs" tal:content="python: tool.translate('no_ref')"></p>
@ -167,9 +209,8 @@
</tal:showNormalField>
<tal:showRef condition="python: appyType['type'] == 'Ref'">
<tal:ref tal:define="isBack python:appyType['isBack'];
fieldRel python:field.relationship;
objs python:contextObj.getAppyRefs(field.getName());
innerRef python:True">
fieldName python: test(isBack, field.relationship, field.getName());
innerRef python:True">
<metal:showField use-macro="here/skyn/ref/macros/showReference" />
</tal:ref>
</tal:showRef>
@ -190,6 +231,10 @@
</td></tr>
</table>
<tal:comment replace="nothing">Appy (bottom) navigation</tal:comment>
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
</fieldset>
<tal:comment replace="nothing">A carriage return needed in some cases.</tal:comment>
<br tal:define="widgetDescr widgetDescr|nothing"
@ -202,7 +247,7 @@
appyType python:here.getAppyType(field.getName());
allBrains python:here.uid_catalog(portal_type=refPortalType);
brains python:here.callAppySelect(appyType['select'], allBrains);
refUids python: [o.UID() for o in here.getAppyRefs(field.getName())];
refUids python: [o.UID() for o in here.getAppyRefs(field.getName())['objects']];
isMultiple python:test(appyType['multiplicity'][1]!=1, 'multiple', '');
appyFieldName python: 'appy_ref_%s' % field.getName();
inError python:test(errors.has_key(field.getName()), True, False);

BIN
gen/plone25/skin/to.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

BIN
gen/plone25/skin/waiting.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -10,6 +10,14 @@
float:right;
}
.appyNav {
padding: 0.4em 0 0.4em 0;
}
.appyFocus {
color: red;
}
#importedElem {
color: grey;
font-style: italic;

View file

@ -142,6 +142,11 @@ class AbstractWrapper:
del kwargs['id']
else:
objId = '%s.%f' % (idPrefix, time.time())
# Determine if object must be created from external data
externalData = None
if kwargs.has_key('_data'):
externalData = kwargs['_data']
del kwargs['_data']
# Where must I create the object?
if not isField:
folder = self.o.getTool().getAppFolder()
@ -173,7 +178,9 @@ class AbstractWrapper:
self.o.reindexObject()
# Call custom initialization
try:
appyObj.onEdit(True)
if externalData: param = externalData
else: param = True
appyObj.onEdit(param)
except AttributeError:
pass
ploneObj.reindexObject()
@ -210,6 +217,15 @@ class AbstractWrapper:
wfTool.doActionFor(self.o, transitionName, comment=comment)
del self.o._v_appy_do
def log(self, message, logLevel='info'):
'''Logs a message in the log file. p_logLevel may be "info", "warning"
or "error".'''
logger = self.o.getProductConfig().logger
if logLevel == 'warning': logMethod = logger.warn
elif logLevel == 'error': logMethod = logger.error
else: logMethod = logger.info
logMethod(message)
# ------------------------------------------------------------------------------
class FileWrapper:
'''When you get, from an appy object, the value of a File attribute, you
@ -246,12 +262,21 @@ class FileWrapper:
'''Writes the file on disk. If p_filePath is specified, it is the
path name where the file will be dumped; folders mentioned in it
must exist. If not, the file will be dumped in the OS temp folder.
The absoulte path name of the dumped file is returned.'''
The absolute path name of the dumped file is returned.'''
if not filePath:
filePath = '%s/file%f.%s' % (getOsTempFolder(), time.time(),
self.name)
f = file(filePath, 'w')
f.write(self.content)
if self.content.__class__.__name__ == 'Pdata':
# The file content is splitted in several chunks.
f.write(self.content.data)
nextPart = self.content.next
while nextPart:
f.write(nextPart.data)
nextPart = nextPart.next
else:
# Only one chunk
f.write(self.content)
f.close()
return filePath
# ------------------------------------------------------------------------------

View file

@ -81,6 +81,10 @@ class PoMessage:
NO_SELECTION = 'You must select at least one element.'
DELETE_CONFIRM = 'Are you sure you want to delete this element?'
DELETE_DONE = 'The element has been deleted.'
GOTO_FIRST = 'Go to top'
GOTO_PREVIOUS = 'Go to previous'
GOTO_NEXT = 'Go to next'
GOTO_LAST = 'Go to end'
def __init__(self, id, msg, default, fuzzy=False, comments=[]):
self.id = id

View file

@ -170,4 +170,15 @@ class AppyRequest:
else:
res = self.zopeRequest.get(attr, None)
return res
# ------------------------------------------------------------------------------
class RefObjects:
'''Represents a bunch of objects retrieved from a reference.'''
def __init__(self, objects=None):
self.objects = objects or [] # The objects
self.totalNumber = len(self.objects) # self.objects may only represent a
# part of all available objects.
self.batchSize = self.totalNumber # The max length of self.objects.
self.startNumber = 0 # The index of first object in self.objects in
# the whole list.
# ------------------------------------------------------------------------------

View file

@ -21,6 +21,13 @@ import xml.sax
from xml.sax.handler import ContentHandler, ErrorHandler
from xml.sax.xmlreader import InputSource
from StringIO import StringIO
from appy.shared.errors import AppyError
# Error-related constants ------------------------------------------------------
CONVERSION_ERROR = '"%s" value "%s" could not be converted by the XML ' \
'unmarshaller.'
CUSTOM_CONVERSION_ERROR = 'Custom converter for "%s" values produced an ' \
'error while converting value "%s". %s'
# ------------------------------------------------------------------------------
class XmlElement:
@ -150,7 +157,7 @@ class XmlUnmarshaller(XmlParser):
If "object" is specified, it means that the tag contains sub-tags, each
one corresponding to the value of an attribute for this object.
if "tuple" is specified, it will be converted to a list.'''
def __init__(self, klass=None, tagTypes={}):
def __init__(self, klass=None, tagTypes={}, conversionFunctions={}):
XmlParser.__init__(self)
self.klass = klass # If a klass is given here, instead of creating
# a root UnmarshalledObject instance, we will create an instance of this
@ -167,6 +174,19 @@ class XmlUnmarshaller(XmlParser):
# it is not the case of p_xmlContent, you can provide the missing type
# information in p_tagTypes. Here is an example of p_tagTypes:
# {"information": "list", "days": "list", "person": "object"}.
self.conversionFunctions = conversionFunctions
# The parser assumes that data is represented in some standard way. If
# it is not the case, you may provide, in this dict, custom functions
# allowing to convert values of basic types (long, float, DateTime...).
# Every such function must take a single arg which is the value to
# convert and return the converted value. Dict keys are strings
# representing types ('bool', 'int', 'unicode', etc) and dict values are
# conversion functions. Here is an example:
# {'int': convertInteger, 'DateTime': convertDate}
# NOTE: you can even invent a new basic type, put it in self.tagTypes,
# and create a specific conversionFunction for it. This way, you can
# for example convert strings that have specific values (in this case,
# knowing that the value is a 'string' is not sufficient).
def startDocument(self):
self.res = None # The resulting web of Python objects
@ -246,18 +266,37 @@ class XmlUnmarshaller(XmlParser):
def endElement(self, elem):
e = XmlParser.endElement(self, elem)
if e.currentBasicType:
# Get and convert the value of this field
if e.currentBasicType in self.numericTypes:
try:
exec 'value = %s' % e.currentContent.strip()
except SyntaxError:
value = None
elif e.currentBasicType == 'DateTime':
value = DateTime(e.currentContent.strip())
elif e.currentBasicType == 'base64':
value = e.currentContent.decode('base64')
value = e.currentContent.strip()
if not value: value = None
else:
value = e.currentContent.strip()
# If we have a custom converter for values of this type, use it.
if self.conversionFunctions.has_key(e.currentBasicType):
try:
value = self.conversionFunctions[e.currentBasicType](
value)
except Exception, err:
raise AppyError(CUSTOM_CONVERSION_ERROR % (
e.currentBasicType, value, str(err)))
# If not, try a standard conversion
elif e.currentBasicType in self.numericTypes:
try:
exec 'value = %s' % value
except SyntaxError:
raise AppyError(CONVERSION_ERROR % (
e.currentBasicType, value))
except NameError:
raise AppyError(CONVERSION_ERROR % (
e.currentBasicType, value))
# Check that the value is of the correct type. For instance,
# a float value with a comma in it could have been converted
# to a tuple instead of a float.
if not isinstance(value, eval(e.currentBasicType)):
raise AppyError(CONVERSION_ERROR % (
e.currentBasicType, value))
elif e.currentBasicType == 'DateTime':
value = DateTime(value)
elif e.currentBasicType == 'base64':
value = e.currentContent.decode('base64')
# Store the value on the last container
self.storeValue(elem, value)
# Clean the environment