From 605c42d94ef267ad6ff1d34a8a21d2a8a185ba6a Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Sun, 25 Oct 2009 21:42:08 +0100 Subject: [PATCH] Added an AJAX framework within appy.gen, and its first use: a pagination mechanism for producing paginated references in the reference widget. --- gen/__init__.py | 80 +++++--- gen/plone25/descriptors.py | 2 + gen/plone25/generator.py | 8 +- gen/plone25/mixins/ToolMixin.py | 15 +- gen/plone25/mixins/__init__.py | 256 ++++++++++++++++---------- gen/plone25/skin/ajax.pt | 25 +++ gen/plone25/skin/arrowLeftDouble.png | Bin 0 -> 212 bytes gen/plone25/skin/arrowLeftSimple.png | Bin 0 -> 218 bytes gen/plone25/skin/arrowRightDouble.png | Bin 0 -> 212 bytes gen/plone25/skin/arrowRightSimple.png | Bin 0 -> 216 bytes gen/plone25/skin/edit.pt | 2 +- gen/plone25/skin/macros.pt | 119 +++++++++++- gen/plone25/skin/ref.pt | 109 +++++++---- gen/plone25/skin/to.png | Bin 0 -> 214 bytes gen/plone25/skin/waiting.gif | Bin 0 -> 2975 bytes gen/plone25/templates/Styles.css.dtml | 8 + gen/plone25/wrappers/__init__.py | 31 +++- gen/po.py | 4 + gen/utils.py | 11 ++ shared/xml_parser.py | 63 +++++-- 20 files changed, 546 insertions(+), 187 deletions(-) create mode 100644 gen/plone25/skin/ajax.pt create mode 100644 gen/plone25/skin/arrowLeftDouble.png create mode 100644 gen/plone25/skin/arrowLeftSimple.png create mode 100644 gen/plone25/skin/arrowRightDouble.png create mode 100644 gen/plone25/skin/arrowRightSimple.png create mode 100644 gen/plone25/skin/to.png create mode 100755 gen/plone25/skin/waiting.gif diff --git a/gen/__init__.py b/gen/__init__.py index 6e1b194..e8e28de 100755 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -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: diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py index a3595a0..09555d1 100644 --- a/gen/plone25/descriptors.py +++ b/gen/plone25/descriptors.py @@ -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': diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index 2c5e3ee..1454b40 100644 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -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() diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index 119be79..4fccf11 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -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() diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index 5e1514e..7f5d528 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -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.''' diff --git a/gen/plone25/skin/ajax.pt b/gen/plone25/skin/ajax.pt new file mode 100644 index 0000000..519100c --- /dev/null +++ b/gen/plone25/skin/ajax.pt @@ -0,0 +1,25 @@ + + 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. + + + + + + + diff --git a/gen/plone25/skin/arrowLeftDouble.png b/gen/plone25/skin/arrowLeftDouble.png new file mode 100644 index 0000000000000000000000000000000000000000..9e25e604765d89f960a1bef222d0665b4a653f29 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~k$P6SIpV$=wDYgKg5LX6f2HmJb&vO{wfh0FdgVlSPo*MET8_`iF1B#Zfaf$gL6@8Vo7R>LV0FMhC)b2s)D;{XE)7O>#CW|1qiKtyt zX9G}3w!}4}#5q4VH#M(>!MP|ku_QG`p**uBLm?z1Rl(iUH{gAWY93IbqNj^v2*>s0 ygoL7mgoFnyf~y?P7_fHCWJ=KKkcbdl!N=fmlFdgVlSPo*gjMFVdQ&MBb@09&9o)&Kwi literal 0 HcmV?d00001 diff --git a/gen/plone25/skin/arrowRightSimple.png b/gen/plone25/skin/arrowRightSimple.png new file mode 100644 index 0000000000000000000000000000000000000000..baec038aaa0139417c4933f9dc5098cb3ee30ade GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp@K+MU+3?!$B`27TN0(?STfwXSap@8=H`)V&r0Xd8% zL4Lsu4$p3+fjCLt?k)@+tg;>;{XE)7O>#CW|1qv4NHP zX`l#$Y>8_`iF1B#Zfaf$gL6@8Vo7R>LV0FMhC)b2s)DY)TQ*4J^q_J*sTeS-BV)T=JMXWLdkSK!$p{`njxgN@xNA&)YGn literal 0 HcmV?d00001 diff --git a/gen/plone25/skin/edit.pt b/gen/plone25/skin/edit.pt index f012c7b..92df734 100644 --- a/gen/plone25/skin/edit.pt +++ b/gen/plone25/skin/edit.pt @@ -74,7 +74,7 @@
-
+

diff --git a/gen/plone25/skin/macros.pt b/gen/plone25/skin/macros.pt index 03e5305..a62dd8d 100644 --- a/gen/plone25/skin/macros.pt +++ b/gen/plone25/skin/macros.pt @@ -107,7 +107,10 @@ - + + 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.
@@ -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 @@ For other fields like Refs we use specific view/edit macros. @@ -186,10 +189,9 @@
@@ -309,6 +311,63 @@ "Static" javascripts