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.

This commit is contained in:
Gaetan Delannay 2010-12-17 14:46:55 +01:00
parent 2124cffa5e
commit a30949a621
15 changed files with 213 additions and 132 deletions

View file

@ -688,7 +688,13 @@ class Type:
if not self.editDefault: if not self.editDefault:
# Return self.default, of self.default() if it is a method # Return self.default, of self.default() if it is a method
if type(self.default) == types.FunctionType: 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: else:
return self.default return self.default
# If value is editable, get the default value from the tool # If value is editable, get the default value from the tool
@ -704,6 +710,11 @@ class Type:
if self.isEmptyValue(value): return '' if self.isEmptyValue(value): return ''
return value 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): def getIndexValue(self, obj, forSearch=False):
'''This method returns a version for this field value on p_obj that is '''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 ready for indexing purposes. Needs to be overridden by some child
@ -1130,6 +1141,13 @@ class String(Type):
res = str(value) res = str(value)
return res 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): def getPossibleValues(self,obj,withTranslations=False,withBlankValue=False):
'''Returns the list of possible values for this field if it is a '''Returns the list of possible values for this field if it is a
selection field. If p_withTranslations is True, selection field. If p_withTranslations is True,
@ -1236,6 +1254,14 @@ class String(Type):
value = [value] value = [value]
exec 'obj.%s = value' % self.name 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): class Boolean(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None, def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True, default=None, optional=False, editDefault=False, show=True,
@ -1291,10 +1317,11 @@ class Date(Type):
masterValue, focus, historized, True) masterValue, focus, historized, True)
def getCss(self, layoutType): 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): def getJs(self, layoutType):
if layoutType == 'edit': if (layoutType == 'edit') and self.calendar:
return ('jscalendar/calendar_stripped.js', return ('jscalendar/calendar_stripped.js',
'jscalendar/calendar-en.js') 'jscalendar/calendar-en.js')

View file

@ -247,6 +247,8 @@ class PloneInstaller:
appyType.template) appyType.template)
if os.path.exists(fileName): if os.path.exists(fileName):
setattr(self.appyTool, attrName, fileName) setattr(self.appyTool, attrName, fileName)
self.appyTool.log('Imported "%s" in the tool in ' \
'attribute "%s"'% (fileName,attrName))
else: else:
self.appyTool.log('Template "%s" was not found!' % \ self.appyTool.log('Template "%s" was not found!' % \
fileName, type='error') fileName, type='error')
@ -353,7 +355,7 @@ class PloneInstaller:
workflow = wfMethod(self, workflowName) workflow = wfMethod(self, workflowName)
wfTool._setObject(workflowName, workflow) wfTool._setObject(workflowName, workflow)
else: else:
self.log('%s already in workflows.' % workflowName) self.appyTool.log('%s already in workflows.' % workflowName)
# Link the workflow to the current content type # Link the workflow to the current content type
wfTool.setChainForPortalTypes([contentType], workflowName) wfTool.setChainForPortalTypes([contentType], workflowName)
return wfTool return wfTool
@ -404,14 +406,10 @@ class PloneInstaller:
indexInfo = {} indexInfo = {}
for className, appyTypes in self.attributes.iteritems(): for className, appyTypes in self.attributes.iteritems():
for appyType in appyTypes: for appyType in appyTypes:
if appyType.indexed: if not appyType.indexed or (appyType.name == 'title'): continue
n = appyType.name n = appyType.name
indexName = 'get%s%s' % (n[0].upper(), n[1:]) indexName = 'get%s%s' % (n[0].upper(), n[1:])
indexType = 'FieldIndex' indexInfo[indexName] = appyType.getIndexType()
if (appyType.type == 'String') and appyType.isSelect and \
appyType.isMultiValued():
indexType = 'ZCTextIndex'
indexInfo[indexName] = indexType
if indexInfo: if indexInfo:
PloneInstaller.updateIndexes(self.ploneSite, indexInfo, self) PloneInstaller.updateIndexes(self.ploneSite, indexInfo, self)
@ -454,11 +452,9 @@ class PloneInstaller:
frontPageName = self.productName + 'FrontPage' frontPageName = self.productName + 'FrontPage'
site.manage_changeProperties(default_page=frontPageName) site.manage_changeProperties(default_page=frontPageName)
def log(self, msg): print msg def info(self, msg): return self.appyTool.log(msg)
def info(self, msg): return self.log(msg)
def install(self): def install(self):
self.log("Installation of %s:" % self.productName)
if self.minimalistPlone: self.customizePlone() if self.minimalistPlone: self.customizePlone()
self.installRootFolder() self.installRootFolder()
self.installTypes() self.installTypes()
@ -470,7 +466,7 @@ class PloneInstaller:
self.manageIndexes() self.manageIndexes()
self.manageLanguages() self.manageLanguages()
self.finalizeInstallation() self.finalizeInstallation()
self.log("Installation of %s done." % self.productName) self.appyTool.log("Installation of %s done." % self.productName)
def uninstallTool(self): def uninstallTool(self):
site = self.ploneSite site = self.ploneSite
@ -496,10 +492,8 @@ class PloneInstaller:
nvProps.manage_changeProperties(**{'idsNotToList': current}) nvProps.manage_changeProperties(**{'idsNotToList': current})
def uninstall(self): def uninstall(self):
self.log("Uninstallation of %s:" % self.productName)
self.uninstallTool() self.uninstallTool()
self.log("Uninstallation of %s done." % self.productName) return 'Done.'
return self.toLog.getvalue()
# Stuff for tracking user activity --------------------------------------------- # Stuff for tracking user activity ---------------------------------------------
loggedUsers = {} loggedUsers = {}

View file

@ -165,10 +165,12 @@ class ToolMixin(BaseMixin):
return res return res
def getSearchInfo(self, contentType, refInfo=None): def getSearchInfo(self, contentType, refInfo=None):
'''Returns a tuple: '''Returns, as a dict:
1) The list of searchable fields (ie fields among all indexed fields) - the list of searchable fields (= some fields among all indexed
2) The number of columns for layouting those fields.''' fields);
- the number of columns for layouting those fields.'''
fields = [] fields = []
fieldDicts = []
if refInfo: if refInfo:
# The search is triggered from a Ref field. # The search is triggered from a Ref field.
refField = self.getRefInfo(refInfo)[1] refField = self.getRefInfo(refInfo)[1]
@ -180,9 +182,12 @@ class ToolMixin(BaseMixin):
fieldNames = getattr(at, 'searchFieldsFor%s' % contentType,()) fieldNames = getattr(at, 'searchFieldsFor%s' % contentType,())
nbOfColumns = getattr(at, 'numberOfSearchColumnsFor%s' %contentType) nbOfColumns = getattr(at, 'numberOfSearchColumnsFor%s' %contentType)
for name in fieldNames: 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) fields.append(appyType)
return fields, nbOfColumns fieldDicts.append(appyDict)
return {'fields': fields, 'nbOfColumns': nbOfColumns,
'fieldDicts': fieldDicts}
def getImportElements(self, contentType): def getImportElements(self, contentType):
'''Returns the list of elements that can be imported from p_path for '''Returns the list of elements that can be imported from p_path for
@ -336,13 +341,12 @@ class ToolMixin(BaseMixin):
if not searchName: if not searchName:
# It is the global search for all objects pf p_contentType # It is the global search for all objects pf p_contentType
searchName = contentType searchName = contentType
s = self.REQUEST.SESSION
uids = {} uids = {}
i = -1 i = -1
for obj in res.objects: for obj in res.objects:
i += 1 i += 1
uids[startNumber+i] = obj.UID() uids[startNumber+i] = obj.UID()
s['search_%s' % searchName] = uids self.REQUEST.SESSION['search_%s' % searchName] = uids
return res.__dict__ return res.__dict__
def getResultColumnsNames(self, contentType, refField): def getResultColumnsNames(self, contentType, refField):
@ -452,15 +456,6 @@ class ToolMixin(BaseMixin):
return pythonClass.maySearch(self.appy()) return pythonClass.maySearch(self.appy())
return True 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): def onImportObjects(self):
'''This method is called when the user wants to create objects from '''This method is called when the user wants to create objects from
external data.''' external data.'''
@ -614,7 +609,7 @@ class ToolMixin(BaseMixin):
field, this method returns information about this reference: the field, this method returns information about this reference: the
source content type and the Ref field (Appy type). If p_refInfo is source content type and the Ref field (Appy type). If p_refInfo is
not given, we search it among search criteria in the session.''' 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) criteria = self.REQUEST.SESSION.get('searchCriteria', None)
if criteria and criteria.has_key('_ref'): refInfo = criteria['_ref'] if criteria and criteria.has_key('_ref'): refInfo = criteria['_ref']
if not refInfo: return ('', None) if not refInfo: return ('', None)
@ -847,6 +842,8 @@ class ToolMixin(BaseMixin):
session.invalidate() session.invalidate()
from Products.CMFPlone import transaction_note from Products.CMFPlone import transaction_note
transaction_note('Logged out') transaction_note('Logged out')
self.getProductConfig().logger.info('User "%s" has been logged out.' % \
userId)
# Remove user from variable "loggedUsers" # Remove user from variable "loggedUsers"
from appy.gen.plone25.installer import loggedUsers from appy.gen.plone25.installer import loggedUsers
if loggedUsers.has_key(userId): del loggedUsers[userId] if loggedUsers.has_key(userId): del loggedUsers[userId]

View file

@ -276,16 +276,22 @@ class BaseMixin:
res[appyType.name] = appyType.getValue(self) res[appyType.name] = appyType.getValue(self)
return res 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 '''This method allows to add "manually" a data change into the objet's
history. Indeed, data changes are "automatically" recorded only when 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 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, a field. The method is also called by m_historizeData below, that
that performs "automatic" recording when a HTTP form is uploaded.''' 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 # Add to the p_changes dict the field labels
for fieldName in changes.iterkeys(): for fieldName in changes.keys():
appyType = self.getAppyType(fieldName) 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 # Create the event to record in the history
DateTime = self.getProductConfig().DateTime DateTime = self.getProductConfig().DateTime
state = self.portal_workflow.getInfoFor(self, 'review_state') state = self.portal_workflow.getInfoFor(self, 'review_state')
@ -474,21 +480,23 @@ class BaseMixin:
res.append(appyType) res.append(appyType)
return res return res
def getCssAndJs(self, layoutType, page): def getCssAndJs(self, fields, layoutType):
'''Returns the CSS and Javascript files that need to be loaded by the '''Gets the list of Javascript and CSS files required by Appy types
p_page for the given p_layoutType.''' 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 = [] css = []
js = [] js = []
for appyType in self.getAppyTypes(layoutType, page): for field in fields:
typeCss = appyType.getCss(layoutType) fieldCss = field.getCss(layoutType)
if typeCss: if fieldCss:
for tcss in typeCss: for fcss in fieldCss:
if tcss not in css: css.append(tcss) if fcss not in css: css.append(fcss)
typeJs = appyType.getJs(layoutType) fieldJs = field.getJs(layoutType)
if typeJs: if fieldJs:
for tjs in typeJs: for fjs in fieldJs:
if tjs not in js: js.append(tjs) if fjs not in js: js.append(fjs)
return css, js return {'css':css, 'js':js}
def getAppyTypesFromNames(self, fieldNames, asDict=True, addTitle=True): def getAppyTypesFromNames(self, fieldNames, asDict=True, addTitle=True):
'''Gets the Appy types named p_fieldNames. If 'title' is not among '''Gets the Appy types named p_fieldNames. If 'title' is not among
@ -766,12 +774,27 @@ class BaseMixin:
res = True res = True
return res 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): def executeAppyAction(self, actionName, reindex=True):
'''Executes action with p_fieldName on this object.''' '''Executes action with p_fieldName on this object.'''
appyType = self.getAppyType(actionName) appyType = self.getAppyType(actionName)
actionRes = appyType(self.appy()) actionRes = appyType(self.appy())
if self.getParentNode().get(self.id): 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() self.reindexObject()
return appyType.result, actionRes return appyType.result, actionRes
@ -869,7 +892,17 @@ class BaseMixin:
blank value is prepended to the list. If no p_className is defined, blank value is prepended to the list. If no p_className is defined,
the field is supposed to belong to self's class''' the field is supposed to belong to self's class'''
appyType = self.getAppyType(name, className=className) 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): def appy(self):
'''Returns a wrapper object allowing to manipulate p_self the Appy '''Returns a wrapper object allowing to manipulate p_self the Appy

View file

@ -84,7 +84,7 @@ class User(ModelClass):
validator=validatePassword, **gm) validator=validatePassword, **gm)
password2 = String(format=String.PASSWORD, show=showPassword, **gm) password2 = String(format=String.PASSWORD, show=showPassword, **gm)
gm['multiplicity'] = (0, None) gm['multiplicity'] = (0, None)
roles = String(validator=Selection('getGrantableRoles'), **gm) roles = String(validator=Selection('getGrantableRoles'), indexed=True, **gm)
# The Tool class --------------------------------------------------------------- # The Tool class ---------------------------------------------------------------

View file

@ -6,12 +6,10 @@
tool contextObj/getTool; tool contextObj/getTool;
appFolder tool/getAppFolder; appFolder tool/getAppFolder;
appName appFolder/getId; appName appFolder/getId;
phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType='edit'); phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType=layoutType);
phase phaseInfo/name; phase phaseInfo/name;
page request/page|python:'main'; page request/page|python:'main';
cssAndJs python: contextObj.getCssAndJs(layoutType, page); cssJs python: contextObj.getCssAndJs(contextObj.getAppyTypes(layoutType, page), layoutType);
css python: cssAndJs[0];
js python: cssAndJs[1];
confirmMsg request/confirmMsg | nothing;"> confirmMsg request/confirmMsg | nothing;">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
@ -28,7 +26,7 @@
<tal:comment replace="nothing">Include type-specific CSS and JS.</tal:comment> <tal:comment replace="nothing">Include type-specific CSS and JS.</tal:comment>
<metal:js fill-slot="javascript_head_slot"> <metal:js fill-slot="javascript_head_slot">
<tal:js condition="js" repeat="jsFile js"> <tal:js condition="cssJs/js" repeat="jsFile cssJs/js">
<script type="text/javascript" charset="iso-8859-1" <script type="text/javascript" charset="iso-8859-1"
tal:condition="python:exists('portal/%s' % jsFile)" tal:condition="python:exists('portal/%s' % jsFile)"
tal:attributes="src string:$portal_url/$jsFile"> tal:attributes="src string:$portal_url/$jsFile">
@ -36,7 +34,7 @@
</tal:js> </tal:js>
</metal:js> </metal:js>
<metal:css fill-slot="css_slot"> <metal:css fill-slot="css_slot">
<tal:css condition="css" repeat="cssFile css"> <tal:css condition="cssJs/css" repeat="cssFile cssJs/css">
<style type="text/css" media="all" <style type="text/css" media="all"
tal:condition="python:exists('portal/%s' % cssFile)" tal:condition="python:exists('portal/%s' % cssFile)"
tal:content="structure string:<!-- @import url($portal_url/$cssFile); -->"> tal:content="structure string:<!-- @import url($portal_url/$cssFile); -->">

View file

@ -114,7 +114,7 @@
</a></td> </a></td>
<tal:comment replace="nothing">Delete the element</tal:comment> <tal:comment replace="nothing">Delete the element</tal:comment>
<td class="noPadding"> <td class="noPadding">
<img tal:condition="python: member.has_permission('Delete objects', obj)" <img tal:condition="python: member.has_permission('Delete objects', obj) and obj.mayDelete()"
title="Delete" i18n:domain="plone" i18n:attributes="title" style="cursor:pointer" title="Delete" i18n:domain="plone" i18n:attributes="title" style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/delete.png; tal:attributes="src string: $portal_url/skyn/delete.png;
onClick python:'onDeleteObject(\'%s\')' % obj.UID()"/> onClick python:'onDeleteObject(\'%s\')' % obj.UID()"/>

View file

@ -28,7 +28,7 @@
</td> </td>
</dt> </dt>
<tal:publishedObject condition="python: contextObj and tool.userMayNavigate(contextObj)"> <tal:publishedObject condition="python: contextObj and contextObj.mayNavigate()">
<dt class="portletAppyItem portletCurrent"><b tal:content="contextObj/Title"></b></dt> <dt class="portletAppyItem portletCurrent"><b tal:content="contextObj/Title"></b></dt>
<dt class="portletAppyItem"><metal:phases use-macro="here/skyn/portlet/macros/phases"/></dt> <dt class="portletAppyItem"><metal:phases use-macro="here/skyn/portlet/macros/phases"/></dt>
</tal:publishedObject> </tal:publishedObject>

View file

@ -16,7 +16,7 @@
appName appFolder/id; appName appFolder/id;
tool python: portal.get('portal_%s' % appName.lower()); tool python: portal.get('portal_%s' % appName.lower());
contentType request/type_name; contentType request/type_name;
searchName request/search|python:'';"> searchName request/search|python:''">
<div metal:use-macro="here/skyn/page/macros/prologue"/> <div metal:use-macro="here/skyn/page/macros/prologue"/>
<tal:comment replace="nothing">Query result</tal:comment> <tal:comment replace="nothing">Query result</tal:comment>

View file

@ -1,25 +1,43 @@
<tal:search metal:define-macro="master"
define="appFolder context/getParentNode;
contentType request/type_name;
refInfo request/ref|nothing;
tool python: here.portal_url.getPortalObject().get('portal_%s' % appFolder.id.lower());
searchInfo python: tool.getSearchInfo(contentType, refInfo);
cssJs python: tool.getCssAndJs(searchInfo['fields'], 'edit')">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal" lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal" xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n" xmlns:i18n="http://xml.zope.org/namespaces/i18n"
metal:use-macro="here/main_template/macros/master"> metal:use-macro="here/main_template/macros/master">
<tal:comment replace="nothing">Disable standard Plone green tabs</tal:comment> <tal:comment replace="nothing">Disable standard Plone green tabs</tal:comment>
<div metal:fill-slot="top_slot"> <div metal:fill-slot="top_slot">
<metal:block metal:use-macro="here/global_defines/macros/defines" /> <metal:block metal:use-macro="here/global_defines/macros/defines" />
<div tal:define="dummy python:request.set('disable_border', 1)" /> <div tal:define="dummy python:request.set('disable_border', 1)" />
</div> </div>
<tal:comment replace="nothing">Fill main slot of Plone main_template</tal:comment> <tal:comment replace="nothing">Include type-specific CSS and JS.</tal:comment>
<body> <metal:js fill-slot="javascript_head_slot">
<metal:fill fill-slot="main" <tal:js condition="cssJs/js" repeat="jsFile cssJs/js">
tal:define="appFolder context/getParentNode; <script type="text/javascript" charset="iso-8859-1"
contentType request/type_name; tal:condition="python:exists('portal/%s' % jsFile)"
refInfo request/ref|nothing; tal:attributes="src string:$portal_url/$jsFile">
tool python: portal.get('portal_%s' % appFolder.id.lower()); </script>
searchInfo python: tool.getSearchInfo(contentType, refInfo); </tal:js>
searchableFields python: searchInfo[0]; </metal:js>
numberOfColumns python: searchInfo[1]"> <metal:css fill-slot="css_slot">
<tal:css condition="cssJs/css" repeat="cssFile cssJs/css">
<style type="text/css" media="all"
tal:condition="python:exists('portal/%s' % cssFile)"
tal:content="structure string:<!-- @import url($portal_url/$cssFile); -->">
</style>
</tal:css>
</metal:css>
<body>
<metal:fill fill-slot="main">
<tal:comment replace="nothing">Search title</tal:comment> <tal:comment replace="nothing">Search title</tal:comment>
<h1><span tal:replace="python: tool.translate('%s_plural' % contentType)"/> — <h1><span tal:replace="python: tool.translate('%s_plural' % contentType)"/> —
@ -32,8 +50,9 @@
<input tal:condition="refInfo" type="hidden" name="ref" tal:attributes="value refInfo"/> <input tal:condition="refInfo" type="hidden" name="ref" tal:attributes="value refInfo"/>
<table class="no-style-table" cellpadding="0" cellspacing="0" width="100%"> <table class="no-style-table" cellpadding="0" cellspacing="0" width="100%">
<tr tal:repeat="searchRow python: tool.tabularize(searchableFields, numberOfColumns)" valign="top"> <tr tal:repeat="searchRow python: tool.tabularize(searchInfo['fieldDicts'], searchInfo['nbOfColumns'])"
<td tal:repeat="widget searchRow" tal:attributes="width python:'%d%%' % (100/numberOfColumns)"> valign="top">
<td tal:repeat="widget searchRow" tal:attributes="width python:'%d%%' % (100/searchInfo['nbOfColumns'])">
<tal:field condition="widget"> <tal:field condition="widget">
<tal:show define="name widget/name; <tal:show define="name widget/name;
widgetName python: 'w_%s' % name; widgetName python: 'w_%s' % name;
@ -53,3 +72,4 @@
</metal:fill> </metal:fill>
</body> </body>
</html> </html>
</tal:search>

View file

@ -78,55 +78,71 @@
</metal:cell> </metal:cell>
<tal:comment replace="nothing">Search macro for an Date.</tal:comment> <tal:comment replace="nothing">Search macro for an Date.</tal:comment>
<metal:search define-macro="search"> <metal:search define-macro="search"
tal:define="years python:range(widget['startYear'], widget['endYear']+1)">
<label tal:content="python: tool.translate(widget['labelId'])"></label> <label tal:content="python: tool.translate(widget['labelId'])"></label>
<table cellpadding="0" cellspacing="0"> <table cellpadding="0" cellspacing="0">
<tal:comment replace="nothing">From</tal:comment> <tal:comment replace="nothing">From</tal:comment>
<tr tal:define="fromName python: '%s*date' % widgetName"> <tr tal:define="yearFromName python: '%s*date' % widgetName;
monthFromName python: '%s_from_month' % name;
dayFromName python: '%s_from_day' % name;
dummyFromName python: '_d_ummy_from_%s' % name;">
<td width="10px">&nbsp;</td> <td width="10px">&nbsp;</td>
<td> <td>
<label tal:content="python: tool.translate('search_from')"></label> <label tal:content="python: tool.translate('search_from')"></label>
</td> </td>
<td> <td>
<select tal:attributes="name fromName"> <input type="hidden" tal:attributes="id dummyFromName; name dummyFromName" originalvalue=""/>
<select tal:attributes="id dayFromName; name dayFromName">
<option value="">--</option> <option value="">--</option>
<option tal:repeat="value python:range(widget['startYear'], widget['endYear']+1)" <option tal:repeat="value python: [str(v).zfill(2) for v in range(1, 32)]"
tal:content="value" tal:attributes="value value"></option> tal:content="value" tal:attributes="value value"></option>
</select> / </select> /
<select tal:attributes="name python: '%s_from_month' % name"> <select tal:attributes="id monthFromName; name monthFromName">
<option value="">--</option> <option value="">--</option>
<option tal:repeat="value python: [str(v).zfill(2) for v in range(1, 13)]" <option tal:repeat="value python: [str(v).zfill(2) for v in range(1, 13)]"
tal:content="python:tool.getMonthName(value)" tal:attributes="value value"></option> tal:content="python:tool.getMonthName(value)" tal:attributes="value value"></option>
</select> / </select> /
<select tal:attributes="name python: '%s_from_day' % name"> <select tal:attributes="id yearFromName; name yearFromName">
<option value="">--</option> <option value="">--</option>
<option tal:repeat="value python: [str(v).zfill(2) for v in range(1, 32)]" <option tal:repeat="value python:range(widget['startYear'], widget['endYear']+1)"
tal:content="value" tal:attributes="value value"></option> tal:content="value" tal:attributes="value value"></option>
</select> </select>
<tal:comment replace="nothing">The icon for displaying the calendar (=date chooser)</tal:comment>
<a tal:condition="widget/calendar"
tal:attributes="onclick python: 'return showJsCalendar(\'%s\', \'%s\', \'%s\', \'%s\', \'%s\', null, null, %d, %d)' % (monthFromName, dummyFromName, yearFromName, monthFromName, dayFromName, years[0], years[-1])">
<img tal:attributes="src string: $portal_url/popup_calendar.gif"/></a>
</td> </td>
</tr> </tr>
<tal:comment replace="nothing">To</tal:comment> <tal:comment replace="nothing">To</tal:comment>
<tr> <tr tal:define="dummyToName python: '_d_ummy_to_%s' % name;
yearToName python: '%s_to_year' % name;
monthToName python: '%s_to_month' % name;
dayToName python: '%s_to_day' % name">
<td></td> <td></td>
<td> <td>
<label tal:content="python: tool.translate('search_to')"></label> <label tal:content="python: tool.translate('search_to')"></label>&nbsp;&nbsp;&nbsp;&nbsp;
</td> </td>
<td> <td height="20px;">
<select tal:attributes="name python: '%s_to_year' % name"> <input type="hidden" tal:attributes="id dummyToName; name dummyToName" originalvalue=""/>
<select tal:attributes="id dayToName; name dayToName">
<option value="">--</option> <option value="">--</option>
<option tal:repeat="value python:range(widget['startYear'], widget['endYear']+1)" <option tal:repeat="value python: [str(v).zfill(2) for v in range(1, 32)]"
tal:content="value" tal:attributes="value value"></option> tal:content="value" tal:attributes="value value"></option>
</select> / </select> /
<select tal:attributes="name python: '%s_to_month' % name"> <select tal:attributes="id monthToName; name monthToName">
<option value="">--</option> <option value="">--</option>
<option tal:repeat="value python: [str(v).zfill(2) for v in range(1, 13)]" <option tal:repeat="value python: [str(v).zfill(2) for v in range(1, 13)]"
tal:content="python:tool.getMonthName(value)" tal:attributes="value value"></option> tal:content="python:tool.getMonthName(value)" tal:attributes="value value"></option>
</select> / </select> /
<select tal:attributes="name python: '%s_to_day' % name"> <select tal:attributes="id yearToName; name yearToName">
<option value="">--</option> <option value="">--</option>
<option tal:repeat="value python: [str(v).zfill(2) for v in range(1, 32)]" <option tal:repeat="value python:range(widget['startYear'], widget['endYear']+1)"
tal:content="value" tal:attributes="value value"></option> tal:content="value" tal:attributes="value value"></option>
</select> </select>
<a tal:condition="widget/calendar"
tal:attributes="onclick python: 'return showJsCalendar(\'%s\', \'%s\', \'%s\', \'%s\', \'%s\', null, null, %d, %d)' % (monthToName, dummyToName, yearToName, monthToName, dayToName, years[0], years[-1])">
<img tal:attributes="src string: $portal_url/popup_calendar.gif"/></a>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -22,7 +22,7 @@
<table class="no-style-table" cellpadding="0" cellspacing="0"> <table class="no-style-table" cellpadding="0" cellspacing="0">
<tr> <tr>
<tal:comment replace="nothing">Arrows for moving objects up or down</tal:comment> <tal:comment replace="nothing">Arrows for moving objects up or down</tal:comment>
<td class="noPadding" tal:condition="python: (len(objs)&gt;1) and member.has_permission('Modify portal content', contextObj)"> <td class="noPadding" tal:condition="python: not appyType['isBack'] and (len(objs)&gt;1) and member.has_permission('Modify portal content', contextObj)">
<tal:moveRef define="objectIndex python: contextObj.getAppyRefIndex(fieldName, obj); <tal:moveRef define="objectIndex python: contextObj.getAppyRefIndex(fieldName, obj);
ajaxBaseCall python: navBaseCall.replace('**v**', '\'%s\',\'ChangeRefOrder\', {\'refObjectUid\':\'%s\', \'move\':\'**v**\'}' % (startNumber, obj.UID()))"> ajaxBaseCall python: navBaseCall.replace('**v**', '\'%s\',\'ChangeRefOrder\', {\'refObjectUid\':\'%s\', \'move\':\'**v**\'}' % (startNumber, obj.UID()))">
<tal:comment replace="nothing">Move up</tal:comment> <tal:comment replace="nothing">Move up</tal:comment>
@ -49,7 +49,7 @@
</td> </td>
<tal:comment replace="nothing">Delete the element</tal:comment> <tal:comment replace="nothing">Delete the element</tal:comment>
<td class="noPadding"> <td class="noPadding">
<img tal:condition="python: member.has_permission('Delete objects', obj)" <img tal:condition="python: not appyType['isBack'] and member.has_permission('Delete objects', obj) and obj.mayDelete()"
title="Delete" i18n:domain="plone" i18n:attributes="title" style="cursor:pointer" title="Delete" i18n:domain="plone" i18n:attributes="title" style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/delete.png; tal:attributes="src string: $portal_url/skyn/delete.png;
onClick python:'onDeleteObject(\'%s\')' % obj.UID()"/> onClick python:'onDeleteObject(\'%s\')' % obj.UID()"/>
@ -179,19 +179,10 @@
tal:attributes="class python:test(innerRef, 'innerAppyTable', '')"> tal:attributes="class python:test(innerRef, 'innerAppyTable', '')">
<tr valign="bottom"><td> <tr valign="bottom"><td>
<tal:comment replace="nothing">Show backward reference(s)</tal:comment> <tal:comment replace="nothing">Show forward or backward reference(s)</tal:comment>
<table class="no-style-table" cellspacing="0" cellpadding="0"
tal:condition="python: appyType['isBack'] and objs">
<tr tal:repeat="obj objs">
<td><metal:showObjectTitle use-macro="portal/skyn/widgets/ref/macros/objectTitle" />
</td>
</tr>
</table>
<tal:comment replace="nothing">Show forward reference(s)</tal:comment>
<table tal:attributes="class python:test(innerRef, '', 'listing nosort'); <table tal:attributes="class python:test(innerRef, '', 'listing nosort');
width python:test(innerRef, '100%', appyType['layouts']['view']['width']);" width python:test(innerRef, '100%', appyType['layouts']['view']['width']);"
align="right" tal:condition="python: not appyType['isBack'] and objs" cellpadding="0" cellspacing="0"> align="right" tal:condition="python: objs" cellpadding="0" cellspacing="0">
<tal:widgets define="widgets python: objs[0].getAppyTypesFromNames(appyType['shownInfo'])"> <tal:widgets define="widgets python: objs[0].getAppyTypesFromNames(appyType['shownInfo'])">
<tr tal:condition="appyType/showHeaders"> <tr tal:condition="appyType/showHeaders">
<th tal:repeat="widget widgets"> <th tal:repeat="widget widgets">

View file

@ -101,7 +101,7 @@
<label tal:attributes="for andName" tal:content="python: tool.translate('search_and')"></label><br/> <label tal:attributes="for andName" tal:content="python: tool.translate('search_and')"></label><br/>
</tal:operator> </tal:operator>
<tal:comment replace="nothing">The list of values</tal:comment> <tal:comment replace="nothing">The list of values</tal:comment>
<select tal:attributes="name widgetName" multiple="multiple" size="5"> <select tal:attributes="name widgetName; size widget/height" multiple="multiple">
<option tal:repeat="v python:tool.getPossibleValues(name, withTranslations=True, withBlankValue=False, className=contentType)" <option tal:repeat="v python:tool.getPossibleValues(name, withTranslations=True, withBlankValue=False, className=contentType)"
tal:attributes="value python:v[0]; title python: v[1]" tal:attributes="value python:v[0]; title python: v[1]"
tal:content="python: tool.truncateValue(v[1], widget)"> tal:content="python: tool.truncateValue(v[1], widget)">

View file

@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, re, httplib, sys, stat, urlparse, time import os, re, httplib, sys, stat, urlparse, time, socket
from urllib import quote from urllib import quote
from StringIO import StringIO from StringIO import StringIO
from mimetypes import guess_type from mimetypes import guess_type
@ -9,6 +9,9 @@ from appy.shared.utils import copyData
from appy.gen.utils import sequenceTypes from appy.gen.utils import sequenceTypes
from appy.shared.xml_parser import XmlUnmarshaller, XmlMarshaller from appy.shared.xml_parser import XmlUnmarshaller, XmlMarshaller
# ------------------------------------------------------------------------------
class ResourceError(Exception): pass
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class FormDataEncoder: class FormDataEncoder:
'''Allows to encode form data for sending it through a HTTP request.''' '''Allows to encode form data for sending it through a HTTP request.'''
@ -84,6 +87,7 @@ class HttpResponse:
if self.duration: duration = ', got in %.4f seconds' % self.duration if self.duration: duration = ', got in %.4f seconds' % self.duration
return '<HttpResponse %s (%s)%s>' % (self.code, self.text, duration) return '<HttpResponse %s (%s)%s>' % (self.code, self.text, duration)
xmlHeaders = ('text/xml', 'application/xml')
def extractData(self): def extractData(self):
'''This method extracts, from the various parts of the HTTP response, '''This method extracts, from the various parts of the HTTP response,
some useful information. For example, it will find the URI where to some useful information. For example, it will find the URI where to
@ -91,11 +95,13 @@ class HttpResponse:
data into Python objects.''' data into Python objects.'''
if self.code == 302: if self.code == 302:
return urlparse.urlparse(self.headers['location'])[2] return urlparse.urlparse(self.headers['location'])[2]
elif self.headers.has_key('content-type') and \ elif self.headers.has_key('content-type'):
self.headers['content-type'].startswith('text/xml'): contentType = self.headers['content-type']
# Return an unmarshalled version of the XML content, for easy use for xmlHeader in self.xmlHeaders:
# in Python. if contentType.startswith(xmlHeader):
return XmlUnmarshaller().parse(self.body) # Return an unmarshalled version of the XML content, for
# easy use in Python.
return XmlUnmarshaller().parse(self.body)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
urlRex = re.compile(r'http://([^:/]+)(:[0-9]+)?(/.+)?', re.I) urlRex = re.compile(r'http://([^:/]+)(:[0-9]+)?(/.+)?', re.I)
@ -109,9 +115,6 @@ class Resource:
self.username = username self.username = username
self.password = password self.password = password
self.url = url self.url = url
# If some headers must be sent with any request sent through this
# resource (like a cookie), you can store them in the following dict.
self.headers = {}
# If p_measure is True, we will measure, for every request sent, the # If p_measure is True, we will measure, for every request sent, the
# time we wait until we receive the response. # time we wait until we receive the response.
self.measure = measure self.measure = measure
@ -127,6 +130,9 @@ class Resource:
self.port = port and int(port[1:]) or 80 self.port = port and int(port[1:]) or 80
self.uri = uri or '/' self.uri = uri or '/'
else: raise 'Wrong URL: %s' % str(url) else: raise 'Wrong URL: %s' % str(url)
# If some headers must be sent with any request sent through this
# resource (like a cookie), you can store them in the following dict.
self.headers = {'Host': self.host}
def __repr__(self): def __repr__(self):
return '<Dav resource at %s>' % self.url return '<Dav resource at %s>' % self.url
@ -147,7 +153,12 @@ class Resource:
def send(self, method, uri, body=None, headers={}, bodyType=None): def send(self, method, uri, body=None, headers={}, bodyType=None):
'''Sends a HTTP request with p_method, for p_uri.''' '''Sends a HTTP request with p_method, for p_uri.'''
conn = httplib.HTTP() conn = httplib.HTTP()
conn.connect(self.host, self.port) try:
conn.connect(self.host, self.port)
except socket.gaierror, sge:
raise ResourceError('Check your Internet connection (%s)'% str(sge))
except socket.error, se:
raise ResourceError('Connection error (%s)'% str(sge))
# Tell what kind of HTTP request it will be. # Tell what kind of HTTP request it will be.
conn.putrequest(method, uri) conn.putrequest(method, uri)
# Add HTTP headers # Add HTTP headers
@ -222,6 +233,7 @@ class Resource:
'''Perform a HTTP GET on the server.''' '''Perform a HTTP GET on the server.'''
if not uri: uri = self.uri if not uri: uri = self.uri
return self.send('GET', uri, headers=headers) return self.send('GET', uri, headers=headers)
rss = get
def post(self, data=None, uri=None, headers={}, encode='form'): def post(self, data=None, uri=None, headers={}, encode='form'):
'''Perform a HTTP POST on the server. If p_encode is "form", p_data is '''Perform a HTTP POST on the server. If p_encode is "form", p_data is
@ -230,7 +242,6 @@ class Resource:
body of the HTTP request.''' body of the HTTP request.'''
if not uri: uri = self.uri if not uri: uri = self.uri
# Prepare the data to send # Prepare the data to send
headers['Host'] = self.host
if encode == 'form': if encode == 'form':
# Format the form data and prepare headers # Format the form data and prepare headers
body = FormDataEncoder(data).encode() body = FormDataEncoder(data).encode()

View file

@ -342,12 +342,6 @@ class XmlUnmarshaller(XmlParser):
# Alias: "unmarshall" -> "parse" # Alias: "unmarshall" -> "parse"
unmarshall = XmlParser.parse unmarshall = XmlParser.parse
class CssParser(XmlUnmarshaller):
cssTags = {'rss': 'object', 'channel': 'object', 'item': 'object'}
def startDocument(self):
XmlUnmarshaller.startDocument(self)
self.tagTypes.update(self.cssTags)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class XmlMarshaller: class XmlMarshaller:
'''This class allows to produce a XML version of a Python object, which '''This class allows to produce a XML version of a Python object, which