From a30949a621d6a3457bedda97845863ae096b5296 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Fri, 17 Dec 2010 14:46:55 +0100 Subject: [PATCH] Improved management of indexes; always provide str and not unicode strings as index values; search screen may now include javascripts and css like those required by the date chooser; removed CSS parser, basic XmlUnmarshaller can do it by itself. --- gen/__init__.py | 33 ++++++++++++-- gen/plone25/installer.py | 26 ++++------- gen/plone25/mixins/ToolMixin.py | 31 ++++++------- gen/plone25/mixins/__init__.py | 73 ++++++++++++++++++++++-------- gen/plone25/model.py | 2 +- gen/plone25/skin/edit.pt | 10 ++-- gen/plone25/skin/macros.pt | 2 +- gen/plone25/skin/portlet.pt | 2 +- gen/plone25/skin/query.pt | 2 +- gen/plone25/skin/search.pt | 54 +++++++++++++++------- gen/plone25/skin/widgets/date.pt | 52 +++++++++++++-------- gen/plone25/skin/widgets/ref.pt | 17 ++----- gen/plone25/skin/widgets/string.pt | 2 +- shared/dav.py | 33 +++++++++----- shared/xml_parser.py | 6 --- 15 files changed, 213 insertions(+), 132 deletions(-) diff --git a/gen/__init__.py b/gen/__init__.py index 1e781e6..a1f9450 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -688,7 +688,13 @@ class Type: if not self.editDefault: # Return self.default, of self.default() if it is a method if type(self.default) == types.FunctionType: - return self.default(obj.appy()) + appyObj = obj.appy() + try: + return self.default(appyObj) + except Exception, e: + appyObj.log('Exception while getting default value ' \ + 'of field "%s": %s.' % (self.name, str(e))) + return None else: return self.default # If value is editable, get the default value from the tool @@ -704,6 +710,11 @@ class Type: if self.isEmptyValue(value): return '' return value + def getIndexType(self): + '''Returns the name of the technical, Zope-level index type for this + field.''' + return 'FieldIndex' + def getIndexValue(self, obj, forSearch=False): '''This method returns a version for this field value on p_obj that is ready for indexing purposes. Needs to be overridden by some child @@ -1130,6 +1141,13 @@ class String(Type): res = str(value) return res + def getIndexValue(self, obj, forSearch=False): + '''For indexing purposes, we return only strings, not unicodes.''' + res = Type.getIndexValue(self, obj, forSearch) + if isinstance(res, unicode): + res = res.encode('utf-8') + return res + def getPossibleValues(self,obj,withTranslations=False,withBlankValue=False): '''Returns the list of possible values for this field if it is a selection field. If p_withTranslations is True, @@ -1236,6 +1254,14 @@ class String(Type): value = [value] exec 'obj.%s = value' % self.name + def getIndexType(self): + '''Index type varies depending on String parameters.''' + # If String.isSelect, be it multivalued or not, we define a ZCTextIndex: + # this way we can use AND/OR operator. + if self.isSelect or (self.format in (String.TEXT, String.XHTML)): + return 'ZCTextIndex' + return Type.getIndexType(self) + class Boolean(Type): def __init__(self, validator=None, multiplicity=(0,1), index=None, default=None, optional=False, editDefault=False, show=True, @@ -1291,10 +1317,11 @@ class Date(Type): masterValue, focus, historized, True) def getCss(self, layoutType): - if layoutType == 'edit': return ('jscalendar/calendar-system.css',) + if (layoutType == 'edit') and self.calendar: + return ('jscalendar/calendar-system.css',) def getJs(self, layoutType): - if layoutType == 'edit': + if (layoutType == 'edit') and self.calendar: return ('jscalendar/calendar_stripped.js', 'jscalendar/calendar-en.js') diff --git a/gen/plone25/installer.py b/gen/plone25/installer.py index 4cbc36c..2096349 100644 --- a/gen/plone25/installer.py +++ b/gen/plone25/installer.py @@ -247,6 +247,8 @@ class PloneInstaller: appyType.template) if os.path.exists(fileName): setattr(self.appyTool, attrName, fileName) + self.appyTool.log('Imported "%s" in the tool in ' \ + 'attribute "%s"'% (fileName,attrName)) else: self.appyTool.log('Template "%s" was not found!' % \ fileName, type='error') @@ -353,7 +355,7 @@ class PloneInstaller: workflow = wfMethod(self, workflowName) wfTool._setObject(workflowName, workflow) else: - self.log('%s already in workflows.' % workflowName) + self.appyTool.log('%s already in workflows.' % workflowName) # Link the workflow to the current content type wfTool.setChainForPortalTypes([contentType], workflowName) return wfTool @@ -404,14 +406,10 @@ class PloneInstaller: indexInfo = {} for className, appyTypes in self.attributes.iteritems(): for appyType in appyTypes: - if appyType.indexed: - n = appyType.name - indexName = 'get%s%s' % (n[0].upper(), n[1:]) - indexType = 'FieldIndex' - if (appyType.type == 'String') and appyType.isSelect and \ - appyType.isMultiValued(): - indexType = 'ZCTextIndex' - indexInfo[indexName] = indexType + if not appyType.indexed or (appyType.name == 'title'): continue + n = appyType.name + indexName = 'get%s%s' % (n[0].upper(), n[1:]) + indexInfo[indexName] = appyType.getIndexType() if indexInfo: PloneInstaller.updateIndexes(self.ploneSite, indexInfo, self) @@ -454,11 +452,9 @@ class PloneInstaller: frontPageName = self.productName + 'FrontPage' site.manage_changeProperties(default_page=frontPageName) - def log(self, msg): print msg - def info(self, msg): return self.log(msg) + def info(self, msg): return self.appyTool.log(msg) def install(self): - self.log("Installation of %s:" % self.productName) if self.minimalistPlone: self.customizePlone() self.installRootFolder() self.installTypes() @@ -470,7 +466,7 @@ class PloneInstaller: self.manageIndexes() self.manageLanguages() self.finalizeInstallation() - self.log("Installation of %s done." % self.productName) + self.appyTool.log("Installation of %s done." % self.productName) def uninstallTool(self): site = self.ploneSite @@ -496,10 +492,8 @@ class PloneInstaller: nvProps.manage_changeProperties(**{'idsNotToList': current}) def uninstall(self): - self.log("Uninstallation of %s:" % self.productName) self.uninstallTool() - self.log("Uninstallation of %s done." % self.productName) - return self.toLog.getvalue() + return 'Done.' # Stuff for tracking user activity --------------------------------------------- loggedUsers = {} diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index 10b98e1..ee0d8ff 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -165,10 +165,12 @@ class ToolMixin(BaseMixin): return res def getSearchInfo(self, contentType, refInfo=None): - '''Returns a tuple: - 1) The list of searchable fields (ie fields among all indexed fields) - 2) The number of columns for layouting those fields.''' + '''Returns, as a dict: + - the list of searchable fields (= some fields among all indexed + fields); + - the number of columns for layouting those fields.''' fields = [] + fieldDicts = [] if refInfo: # The search is triggered from a Ref field. refField = self.getRefInfo(refInfo)[1] @@ -180,9 +182,12 @@ class ToolMixin(BaseMixin): fieldNames = getattr(at, 'searchFieldsFor%s' % contentType,()) nbOfColumns = getattr(at, 'numberOfSearchColumnsFor%s' %contentType) for name in fieldNames: - appyType = self.getAppyType(name, asDict=True,className=contentType) + appyType = self.getAppyType(name,asDict=False,className=contentType) + appyDict = self.getAppyType(name, asDict=True,className=contentType) fields.append(appyType) - return fields, nbOfColumns + fieldDicts.append(appyDict) + return {'fields': fields, 'nbOfColumns': nbOfColumns, + 'fieldDicts': fieldDicts} def getImportElements(self, contentType): '''Returns the list of elements that can be imported from p_path for @@ -336,13 +341,12 @@ class ToolMixin(BaseMixin): if not searchName: # It is the global search for all objects pf p_contentType searchName = contentType - s = self.REQUEST.SESSION uids = {} i = -1 for obj in res.objects: i += 1 uids[startNumber+i] = obj.UID() - s['search_%s' % searchName] = uids + self.REQUEST.SESSION['search_%s' % searchName] = uids return res.__dict__ def getResultColumnsNames(self, contentType, refField): @@ -452,15 +456,6 @@ class ToolMixin(BaseMixin): return pythonClass.maySearch(self.appy()) return True - def userMayNavigate(self, obj): - '''This method checks if the currently logged user can display the - navigation panel within the portlet. This is done by calling method - "mayNavigate" on the currently shown object. If no such method - exists, we return True.''' - appyObj = obj.appy() - if hasattr(appyObj, 'mayNavigate'): return appyObj.mayNavigate() - return True - def onImportObjects(self): '''This method is called when the user wants to create objects from external data.''' @@ -614,7 +609,7 @@ class ToolMixin(BaseMixin): field, this method returns information about this reference: the source content type and the Ref field (Appy type). If p_refInfo is not given, we search it among search criteria in the session.''' - if not refInfo: + if not refInfo and (self.REQUEST.get('search', None) == '_advanced'): criteria = self.REQUEST.SESSION.get('searchCriteria', None) if criteria and criteria.has_key('_ref'): refInfo = criteria['_ref'] if not refInfo: return ('', None) @@ -847,6 +842,8 @@ class ToolMixin(BaseMixin): session.invalidate() from Products.CMFPlone import transaction_note transaction_note('Logged out') + self.getProductConfig().logger.info('User "%s" has been logged out.' % \ + userId) # Remove user from variable "loggedUsers" from appy.gen.plone25.installer import loggedUsers if loggedUsers.has_key(userId): del loggedUsers[userId] diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index f6f6189..d33ccaa 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -276,16 +276,22 @@ class BaseMixin: res[appyType.name] = appyType.getValue(self) return res - def addDataChange(self, changes): + def addDataChange(self, changes, notForPreviouslyEmptyValues=False): '''This method allows to add "manually" a data change into the objet's history. Indeed, data changes are "automatically" recorded only when a HTTP form is uploaded, not if, in the code, a setter is called on - a field. The method is also called by the method historizeData below, - that performs "automatic" recording when a HTTP form is uploaded.''' + a field. The method is also called by m_historizeData below, that + performs "automatic" recording when a HTTP form is uploaded. Field + changes for which the previous value was empty are not recorded into + the history if p_notForPreviouslyEmptyValues is True.''' # Add to the p_changes dict the field labels - for fieldName in changes.iterkeys(): + for fieldName in changes.keys(): appyType = self.getAppyType(fieldName) - changes[fieldName] = (changes[fieldName], appyType.labelId) + if notForPreviouslyEmptyValues and \ + appyType.isEmptyValue(changes[fieldName], self): + del changes[fieldName] + else: + changes[fieldName] = (changes[fieldName], appyType.labelId) # Create the event to record in the history DateTime = self.getProductConfig().DateTime state = self.portal_workflow.getInfoFor(self, 'review_state') @@ -474,21 +480,23 @@ class BaseMixin: res.append(appyType) return res - def getCssAndJs(self, layoutType, page): - '''Returns the CSS and Javascript files that need to be loaded by the - p_page for the given p_layoutType.''' + def getCssAndJs(self, fields, layoutType): + '''Gets the list of Javascript and CSS files required by Appy types + p_fields when shown on p_layoutType.''' + # lists css and js below are not sets, because order of Javascript + # inclusion can be important, and this could be losed by using sets. css = [] js = [] - for appyType in self.getAppyTypes(layoutType, page): - typeCss = appyType.getCss(layoutType) - if typeCss: - for tcss in typeCss: - if tcss not in css: css.append(tcss) - typeJs = appyType.getJs(layoutType) - if typeJs: - for tjs in typeJs: - if tjs not in js: js.append(tjs) - return css, js + for field in fields: + fieldCss = field.getCss(layoutType) + if fieldCss: + for fcss in fieldCss: + if fcss not in css: css.append(fcss) + fieldJs = field.getJs(layoutType) + if fieldJs: + for fjs in fieldJs: + if fjs not in js: js.append(fjs) + return {'css':css, 'js':js} def getAppyTypesFromNames(self, fieldNames, asDict=True, addTitle=True): '''Gets the Appy types named p_fieldNames. If 'title' is not among @@ -766,12 +774,27 @@ class BaseMixin: res = True return res + def mayNavigate(self): + '''May the currently logged user see the navigation panel linked to + this object?''' + appyObj = self.appy() + if hasattr(appyObj, 'mayNavigate'): return appyObj.mayNavigate() + return True + + def mayDelete(self): + '''May the currently logged user delete this object? This condition + comes as an addition/refinement to the corresponding workflow + permission.''' + appyObj = self.appy() + if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete() + return True + def executeAppyAction(self, actionName, reindex=True): '''Executes action with p_fieldName on this object.''' appyType = self.getAppyType(actionName) actionRes = appyType(self.appy()) if self.getParentNode().get(self.id): - # Else, it means that the action has led to self's removal. + # Else, it means that the action has led to self's deletion. self.reindexObject() return appyType.result, actionRes @@ -869,7 +892,17 @@ class BaseMixin: blank value is prepended to the list. If no p_className is defined, the field is supposed to belong to self's class''' appyType = self.getAppyType(name, className=className) - return appyType.getPossibleValues(self,withTranslations,withBlankValue) + if className: + # We need an instance of className, but self can be an instance of + # another class. So here we will search such an instance. + brains = self.executeQuery(className, maxResults=1, brainsOnly=True) + if brains: + obj = brains[0].getObject() + else: + obj = self + else: + obj = self + return appyType.getPossibleValues(obj, withTranslations, withBlankValue) def appy(self): '''Returns a wrapper object allowing to manipulate p_self the Appy diff --git a/gen/plone25/model.py b/gen/plone25/model.py index 7ae7583..6568387 100644 --- a/gen/plone25/model.py +++ b/gen/plone25/model.py @@ -84,7 +84,7 @@ class User(ModelClass): validator=validatePassword, **gm) password2 = String(format=String.PASSWORD, show=showPassword, **gm) gm['multiplicity'] = (0, None) - roles = String(validator=Selection('getGrantableRoles'), **gm) + roles = String(validator=Selection('getGrantableRoles'), indexed=True, **gm) # The Tool class --------------------------------------------------------------- diff --git a/gen/plone25/skin/edit.pt b/gen/plone25/skin/edit.pt index d43178f..bce7b84 100644 --- a/gen/plone25/skin/edit.pt +++ b/gen/plone25/skin/edit.pt @@ -6,12 +6,10 @@ tool contextObj/getTool; appFolder tool/getAppFolder; appName appFolder/getId; - phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType='edit'); + phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType=layoutType); phase phaseInfo/name; page request/page|python:'main'; - cssAndJs python: contextObj.getCssAndJs(layoutType, page); - css python: cssAndJs[0]; - js python: cssAndJs[1]; + cssJs python: contextObj.getCssAndJs(contextObj.getAppyTypes(layoutType, page), layoutType); confirmMsg request/confirmMsg | nothing;"> Include type-specific CSS and JS. - + + + + + + + + + + + Search title

— @@ -32,8 +50,9 @@ - - +
+
Search macro for an Date. - + From - + To - + -
  - + / - / + / - - + The icon for displaying the calendar (=date chooser) + +
- +      - + + / - / - - + + +
diff --git a/gen/plone25/skin/widgets/ref.pt b/gen/plone25/skin/widgets/ref.pt index 2dc4e63..7a7110d 100644 --- a/gen/plone25/skin/widgets/ref.pt +++ b/gen/plone25/skin/widgets/ref.pt @@ -22,7 +22,7 @@ Arrows for moving objects up or down - Delete the element
+ Move up @@ -49,7 +49,7 @@ - @@ -179,19 +179,10 @@ tal:attributes="class python:test(innerRef, 'innerAppyTable', '')">
- Show backward reference(s) - - - - -
-
- - Show forward reference(s) + Show forward or backward reference(s) + align="right" tal:condition="python: objs" cellpadding="0" cellspacing="0">
diff --git a/gen/plone25/skin/widgets/string.pt b/gen/plone25/skin/widgets/string.pt index 673b737..1854aa3 100644 --- a/gen/plone25/skin/widgets/string.pt +++ b/gen/plone25/skin/widgets/string.pt @@ -101,7 +101,7 @@
The list of values -