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:
parent
4c4b2d0f87
commit
605c42d94e
|
@ -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,6 +292,9 @@ class Action(Type):
|
|||
actRes = act(obj)
|
||||
if type(actRes) in sequenceTypes:
|
||||
res[0] = res[0] and actRes[0]
|
||||
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
|
||||
|
@ -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:
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
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):
|
||||
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,11 +144,23 @@ 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 forward:
|
||||
if fieldName == 'id': return res
|
||||
if self.wrapperClass:
|
||||
baseClass = self.wrapperClass.__bases__[-1]
|
||||
|
@ -162,46 +179,73 @@ class AbstractMixin:
|
|||
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:
|
||||
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)
|
||||
if (resultType == 'computation') or not successfull:
|
||||
self.plone_utils.addPortalMessage(msg)
|
||||
return rq.RESPONSE.redirect(rq['HTTP_REFERER'])
|
||||
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
25
gen/plone25/skin/ajax.pt
Normal 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>
|
BIN
gen/plone25/skin/arrowLeftDouble.png
Normal file
BIN
gen/plone25/skin/arrowLeftDouble.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 212 B |
BIN
gen/plone25/skin/arrowLeftSimple.png
Normal file
BIN
gen/plone25/skin/arrowLeftSimple.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 218 B |
BIN
gen/plone25/skin/arrowRightDouble.png
Normal file
BIN
gen/plone25/skin/arrowRightDouble.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 212 B |
BIN
gen/plone25/skin/arrowRightSimple.png
Normal file
BIN
gen/plone25/skin/arrowRightSimple.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 216 B |
|
@ -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"/>
|
||||
|
|
|
@ -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 > 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">
|
||||
<span tal:replace="python: startNumber+1"/>
|
||||
<img tal:attributes="src string: $portal_url/skyn/to.png"/>
|
||||
<span tal:replace="python: startNumber+len(objs)"/>
|
||||
</td>
|
||||
|
||||
<tal:comment replace="nothing">Go to the next page</tal:comment>
|
||||
<td><img style="cursor:pointer" tal:condition="python: sNumber < 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>
|
||||
|
|
|
@ -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)>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 > 0">
|
||||
<input type="image" name="moveUp" class="imageInput"
|
||||
<td class="noPadding" tal:condition="python: (len(objs)>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 > 0"
|
||||
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 < (len(objs)-1)">
|
||||
<input type="image" name="moveDown" class="imageInput"
|
||||
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 < (totalNumber-1)"
|
||||
tal:attributes="src string: $portal_url/skyn/arrowDown.png;
|
||||
title python: tool.translate('move_down')"/>
|
||||
</span>
|
||||
</form>
|
||||
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)<=1)">
|
||||
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)<=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,8 +209,7 @@
|
|||
</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());
|
||||
fieldName python: test(isBack, field.relationship, field.getName());
|
||||
innerRef python:True">
|
||||
<metal:showField use-macro="here/skyn/ref/macros/showReference" />
|
||||
</tal:ref>
|
||||
|
@ -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
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
BIN
gen/plone25/skin/waiting.gif
Executable file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
|
@ -10,6 +10,14 @@
|
|||
float:right;
|
||||
}
|
||||
|
||||
.appyNav {
|
||||
padding: 0.4em 0 0.4em 0;
|
||||
}
|
||||
|
||||
.appyFocus {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#importedElem {
|
||||
color: grey;
|
||||
font-style: italic;
|
||||
|
|
|
@ -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,11 +262,20 @@ 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')
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
11
gen/utils.py
11
gen/utils.py
|
@ -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.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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:
|
||||
value = e.currentContent.strip()
|
||||
if not value: value = None
|
||||
else:
|
||||
# If we have a custom converter for values of this type, use it.
|
||||
if self.conversionFunctions.has_key(e.currentBasicType):
|
||||
try:
|
||||
exec 'value = %s' % e.currentContent.strip()
|
||||
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:
|
||||
value = None
|
||||
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(e.currentContent.strip())
|
||||
value = DateTime(value)
|
||||
elif e.currentBasicType == 'base64':
|
||||
value = e.currentContent.decode('base64')
|
||||
else:
|
||||
value = e.currentContent.strip()
|
||||
# Store the value on the last container
|
||||
self.storeValue(elem, value)
|
||||
# Clean the environment
|
||||
|
|
Loading…
Reference in a new issue