appy.gen: added default validation for wrong dates; for Action fields, added value 'filetmp' for param 'result' for removing temp files that are returned as the result of an action; values entered by the user in the search screens are not stripped; wrapper method 'export' can now export an object in a CSV file; appy.pod: bullets for default list styles in any ODT file generated through pod are now smaller.
This commit is contained in:
parent
9f418439aa
commit
39d68f6490
|
@ -1244,18 +1244,18 @@ class String(Type):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def validateValue(self, obj, value):
|
def validateValue(self, obj, value):
|
||||||
if self.isSelect:
|
if not self.isSelect: return
|
||||||
possibleValues = self.getPossibleValues(obj)
|
# Check that the value is among possible values
|
||||||
if isinstance(value, basestring):
|
possibleValues = self.getPossibleValues(obj)
|
||||||
error = value not in possibleValues
|
if isinstance(value, basestring):
|
||||||
else:
|
error = value not in possibleValues
|
||||||
error = False
|
else:
|
||||||
for v in value:
|
error = False
|
||||||
if v not in possibleValues:
|
for v in value:
|
||||||
error = True
|
if v not in possibleValues:
|
||||||
break
|
error = True
|
||||||
# Check that the value is among possible values
|
break
|
||||||
if error: obj.translate('bad_select_value')
|
if error: return obj.translate('bad_select_value')
|
||||||
|
|
||||||
accents = {'é':'e','è':'e','ê':'e','ë':'e','à':'a','â':'a','ä':'a',
|
accents = {'é':'e','è':'e','ê':'e','ë':'e','à':'a','â':'a','ä':'a',
|
||||||
'ù':'u','û':'u','ü':'u','î':'i','ï':'i','ô':'o','ö':'o',
|
'ù':'u','û':'u','ü':'u','î':'i','ï':'i','ô':'o','ö':'o',
|
||||||
|
@ -1369,6 +1369,13 @@ class Date(Type):
|
||||||
return ('jscalendar/calendar_stripped.js',
|
return ('jscalendar/calendar_stripped.js',
|
||||||
'jscalendar/calendar-en.js')
|
'jscalendar/calendar-en.js')
|
||||||
|
|
||||||
|
def validateValue(self, obj, value):
|
||||||
|
DateTime = obj.getProductConfig().DateTime
|
||||||
|
try:
|
||||||
|
value = DateTime(value)
|
||||||
|
except DateTime.DateError, ValueError:
|
||||||
|
return obj.translate('bad_date')
|
||||||
|
|
||||||
def getFormattedValue(self, obj, value):
|
def getFormattedValue(self, obj, value):
|
||||||
if self.isEmptyValue(value): return ''
|
if self.isEmptyValue(value): return ''
|
||||||
res = value.strftime('%d/%m/') + str(value.year())
|
res = value.strftime('%d/%m/') + str(value.year())
|
||||||
|
@ -1828,12 +1835,14 @@ class Action(Type):
|
||||||
master=None, masterValue=None, focus=False, historized=False):
|
master=None, masterValue=None, focus=False, historized=False):
|
||||||
# Can be a single method or a list/tuple of methods
|
# Can be a single method or a list/tuple of methods
|
||||||
self.action = action
|
self.action = action
|
||||||
# For the following field:
|
# For the 'result' param:
|
||||||
# * value 'computation' means that the action will simply compute
|
# * value 'computation' means that the action will simply compute
|
||||||
# things and redirect the user to the same page, with some status
|
# things and redirect the user to the same page, with some status
|
||||||
# message about execution of the action;
|
# message about execution of the action;
|
||||||
# * 'file' means that the result is the binary content of a file that
|
# * 'file' means that the result is the binary content of a file that
|
||||||
# the user will download.
|
# the user will download.
|
||||||
|
# * 'filetmp' is similar to file, but the file is a temp file and Appy
|
||||||
|
# will delete it as soon as it will be served to the browser.
|
||||||
# * 'redirect' means that the action will lead to the user being
|
# * 'redirect' means that the action will lead to the user being
|
||||||
# redirected to some other page.
|
# redirected to some other page.
|
||||||
self.result = result
|
self.result = result
|
||||||
|
@ -1858,7 +1867,7 @@ class Action(Type):
|
||||||
actRes = act(obj)
|
actRes = act(obj)
|
||||||
if type(actRes) in sequenceTypes:
|
if type(actRes) in sequenceTypes:
|
||||||
res[0] = res[0] and actRes[0]
|
res[0] = res[0] and actRes[0]
|
||||||
if self.result == 'file':
|
if self.result.startswith('file'):
|
||||||
res[1] = res[1] + actRes[1]
|
res[1] = res[1] + actRes[1]
|
||||||
else:
|
else:
|
||||||
res[1] = res[1] + '\n' + actRes[1]
|
res[1] = res[1] + '\n' + actRes[1]
|
||||||
|
|
|
@ -126,6 +126,7 @@ class Generator(AbstractGenerator):
|
||||||
msg('ref_invalid_index', '', msg.REF_INVALID_INDEX),
|
msg('ref_invalid_index', '', msg.REF_INVALID_INDEX),
|
||||||
msg('bad_long', '', msg.BAD_LONG),
|
msg('bad_long', '', msg.BAD_LONG),
|
||||||
msg('bad_float', '', msg.BAD_FLOAT),
|
msg('bad_float', '', msg.BAD_FLOAT),
|
||||||
|
msg('bad_date', '', msg.BAD_DATE),
|
||||||
msg('bad_email', '', msg.BAD_EMAIL),
|
msg('bad_email', '', msg.BAD_EMAIL),
|
||||||
msg('bad_url', '', msg.BAD_URL),
|
msg('bad_url', '', msg.BAD_URL),
|
||||||
msg('bad_alphanumeric', '', msg.BAD_ALPHANUMERIC),
|
msg('bad_alphanumeric', '', msg.BAD_ALPHANUMERIC),
|
||||||
|
|
|
@ -509,7 +509,7 @@ class ToolMixin(BaseMixin):
|
||||||
if not importPath: continue
|
if not importPath: continue
|
||||||
objectId = os.path.basename(importPath)
|
objectId = os.path.basename(importPath)
|
||||||
self.appy().create(appyClass, id=objectId, _data=importPath)
|
self.appy().create(appyClass, id=objectId, _data=importPath)
|
||||||
self.plone_utils.addPortalMessage(self.translate('import_done'))
|
self.say(self.translate('import_done'))
|
||||||
return self.goto(rq['HTTP_REFERER'])
|
return self.goto(rq['HTTP_REFERER'])
|
||||||
|
|
||||||
def isAlreadyImported(self, contentType, importPath):
|
def isAlreadyImported(self, contentType, importPath):
|
||||||
|
@ -584,13 +584,14 @@ class ToolMixin(BaseMixin):
|
||||||
# given field.
|
# given field.
|
||||||
attrValue = rq.form[attrName]
|
attrValue = rq.form[attrName]
|
||||||
if attrName.find('*') != -1:
|
if attrName.find('*') != -1:
|
||||||
|
attrValue = attrValue.strip()
|
||||||
# The type of the value is encoded after char "*".
|
# The type of the value is encoded after char "*".
|
||||||
attrName, attrType = attrName.split('*')
|
attrName, attrType = attrName.split('*')
|
||||||
if attrType == 'bool':
|
if attrType == 'bool':
|
||||||
exec 'attrValue = %s' % attrValue
|
exec 'attrValue = %s' % attrValue
|
||||||
elif attrType in ('int', 'float'):
|
elif attrType in ('int', 'float'):
|
||||||
# Get the "from" value
|
# Get the "from" value
|
||||||
if not attrValue.strip(): attrValue = None
|
if not attrValue: attrValue = None
|
||||||
else:
|
else:
|
||||||
exec 'attrValue = %s(attrValue)' % attrType
|
exec 'attrValue = %s(attrValue)' % attrType
|
||||||
# Get the "to" value
|
# Get the "to" value
|
||||||
|
|
|
@ -94,7 +94,7 @@ class BaseMixin:
|
||||||
urlBack = self.getTool().getSiteUrl()
|
urlBack = self.getTool().getSiteUrl()
|
||||||
else:
|
else:
|
||||||
urlBack = self.getUrl(rq['HTTP_REFERER'])
|
urlBack = self.getUrl(rq['HTTP_REFERER'])
|
||||||
self.plone_utils.addPortalMessage(self.translate('delete_done'))
|
self.say(self.translate('delete_done'))
|
||||||
self.goto(urlBack)
|
self.goto(urlBack)
|
||||||
|
|
||||||
def onCreate(self):
|
def onCreate(self):
|
||||||
|
@ -189,8 +189,7 @@ class BaseMixin:
|
||||||
urlBack = tool.getSiteUrl()
|
urlBack = tool.getSiteUrl()
|
||||||
else:
|
else:
|
||||||
urlBack = self.getUrl()
|
urlBack = self.getUrl()
|
||||||
self.plone_utils.addPortalMessage(
|
self.say(self.translate('Changes canceled.', domain='plone'))
|
||||||
self.translate('Changes canceled.', domain='plone'))
|
|
||||||
return self.goto(urlBack)
|
return self.goto(urlBack)
|
||||||
|
|
||||||
# Object for storing validation errors
|
# Object for storing validation errors
|
||||||
|
@ -202,7 +201,7 @@ class BaseMixin:
|
||||||
self.intraFieldValidation(errors, values)
|
self.intraFieldValidation(errors, values)
|
||||||
if errors.__dict__:
|
if errors.__dict__:
|
||||||
rq.set('errors', errors.__dict__)
|
rq.set('errors', errors.__dict__)
|
||||||
self.plone_utils.addPortalMessage(errorMessage)
|
self.say(errorMessage)
|
||||||
return self.skyn.edit(self)
|
return self.skyn.edit(self)
|
||||||
|
|
||||||
# Trigger inter-field validation
|
# Trigger inter-field validation
|
||||||
|
@ -210,7 +209,7 @@ class BaseMixin:
|
||||||
if not msg: msg = errorMessage
|
if not msg: msg = errorMessage
|
||||||
if errors.__dict__:
|
if errors.__dict__:
|
||||||
rq.set('errors', errors.__dict__)
|
rq.set('errors', errors.__dict__)
|
||||||
self.plone_utils.addPortalMessage(msg)
|
self.say(msg)
|
||||||
return self.skyn.edit(self)
|
return self.skyn.edit(self)
|
||||||
|
|
||||||
# Before saving data, must we ask a confirmation by the user ?
|
# Before saving data, must we ask a confirmation by the user ?
|
||||||
|
@ -240,7 +239,7 @@ class BaseMixin:
|
||||||
return self.goto(tool.getSiteUrl(), msg)
|
return self.goto(tool.getSiteUrl(), msg)
|
||||||
if rq.get('buttonOk.x', None) or saveConfirmed:
|
if rq.get('buttonOk.x', None) or saveConfirmed:
|
||||||
# Go to the consult view for this object
|
# Go to the consult view for this object
|
||||||
obj.plone_utils.addPortalMessage(msg)
|
obj.say(msg)
|
||||||
return self.goto(obj.getUrl())
|
return self.goto(obj.getUrl())
|
||||||
if rq.get('buttonPrevious.x', None):
|
if rq.get('buttonPrevious.x', None):
|
||||||
# Go to the previous page for this object.
|
# Go to the previous page for this object.
|
||||||
|
@ -260,7 +259,7 @@ class BaseMixin:
|
||||||
else:
|
else:
|
||||||
return self.goto(obj.getUrl(page=pageName))
|
return self.goto(obj.getUrl(page=pageName))
|
||||||
else:
|
else:
|
||||||
obj.plone_utils.addPortalMessage(msg)
|
obj.say(msg)
|
||||||
return self.goto(obj.getUrl())
|
return self.goto(obj.getUrl())
|
||||||
if rq.get('buttonNext.x', None):
|
if rq.get('buttonNext.x', None):
|
||||||
# Go to the next page for this object
|
# Go to the next page for this object
|
||||||
|
@ -277,10 +276,30 @@ class BaseMixin:
|
||||||
else:
|
else:
|
||||||
return self.goto(obj.getUrl(page=pageName))
|
return self.goto(obj.getUrl(page=pageName))
|
||||||
else:
|
else:
|
||||||
obj.plone_utils.addPortalMessage(msg)
|
obj.say(msg)
|
||||||
return self.goto(obj.getUrl())
|
return self.goto(obj.getUrl())
|
||||||
return obj.skyn.edit(obj)
|
return obj.skyn.edit(obj)
|
||||||
|
|
||||||
|
def say(self, msg, type='info'):
|
||||||
|
'''Prints a p_msg in the user interface. p_logLevel may be "info",
|
||||||
|
"warning" or "error".'''
|
||||||
|
mType = type
|
||||||
|
if mType == 'warning': mType = 'warn'
|
||||||
|
elif mType == 'error': mType = 'stop'
|
||||||
|
try:
|
||||||
|
self.plone_utils.addPortalMessage(msg, type=mType)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
self.plone_utils.addPortalMessage(msg.decode('utf-8'), type=mType)
|
||||||
|
|
||||||
|
def log(self, msg, type='info'):
|
||||||
|
'''Logs a p_msg in the log file. p_logLevel may be "info", "warning"
|
||||||
|
or "error".'''
|
||||||
|
logger = self.getProductConfig().logger
|
||||||
|
if type == 'warning': logMethod = logger.warn
|
||||||
|
elif type == 'error': logMethod = logger.error
|
||||||
|
else: logMethod = logger.info
|
||||||
|
logMethod(msg)
|
||||||
|
|
||||||
def rememberPreviousData(self):
|
def rememberPreviousData(self):
|
||||||
'''This method is called before updating an object and remembers, for
|
'''This method is called before updating an object and remembers, for
|
||||||
every historized field, the previous value. Result is a dict
|
every historized field, the previous value. Result is a dict
|
||||||
|
@ -880,16 +899,27 @@ class BaseMixin:
|
||||||
label = '%s_action_%s' % (appyType.labelId, suffix)
|
label = '%s_action_%s' % (appyType.labelId, suffix)
|
||||||
msg = self.translate(label)
|
msg = self.translate(label)
|
||||||
if (resultType == 'computation') or not successfull:
|
if (resultType == 'computation') or not successfull:
|
||||||
self.plone_utils.addPortalMessage(msg)
|
self.say(msg)
|
||||||
return self.goto(self.getUrl(rq['HTTP_REFERER']))
|
return self.goto(self.getUrl(rq['HTTP_REFERER']))
|
||||||
elif resultType == 'file':
|
elif resultType.startswith('file'):
|
||||||
# msg does not contain a message, but a file instance.
|
# msg does not contain a message, but a file instance.
|
||||||
response = self.REQUEST.RESPONSE
|
response = self.REQUEST.RESPONSE
|
||||||
response.setHeader('Content-Type',mimetypes.guess_type(msg.name)[0])
|
response.setHeader('Content-Type',mimetypes.guess_type(msg.name)[0])
|
||||||
response.setHeader('Content-Disposition', 'inline;filename="%s"' %\
|
response.setHeader('Content-Disposition', 'inline;filename="%s"' %\
|
||||||
msg.name)
|
os.path.basename(msg.name))
|
||||||
response.write(msg.read())
|
response.write(msg.read())
|
||||||
msg.close()
|
msg.close()
|
||||||
|
if resultType == 'filetmp':
|
||||||
|
# p_msg is a temp file. We need to delete it.
|
||||||
|
try:
|
||||||
|
os.remove(msg.name)
|
||||||
|
self.log('Temp file "%s" was deleted.' % msg.name)
|
||||||
|
except IOError, err:
|
||||||
|
self.log('Could not remove temp "%s" (%s).' % \
|
||||||
|
(msg.name, str(err)), type='warning')
|
||||||
|
except OSError, err:
|
||||||
|
self.log('Could not remove temp "%s" (%s).' % \
|
||||||
|
(msg.name, str(err)), type='warning')
|
||||||
elif resultType == 'redirect':
|
elif resultType == 'redirect':
|
||||||
# msg does not contain a message, but the URL where to redirect
|
# msg does not contain a message, but the URL where to redirect
|
||||||
# the user.
|
# the user.
|
||||||
|
|
|
@ -113,7 +113,7 @@ class Translation(ModelClass):
|
||||||
# All methods defined below are fake. Real versions are in the wrapper.
|
# All methods defined below are fake. Real versions are in the wrapper.
|
||||||
def getPoFile(self): pass
|
def getPoFile(self): pass
|
||||||
po = Action(action=getPoFile, page=Page('actions', show='view'),
|
po = Action(action=getPoFile, page=Page('actions', show='view'),
|
||||||
result='file')
|
result='filetmp')
|
||||||
title = String(show=False, indexed=True)
|
title = String(show=False, indexed=True)
|
||||||
def computeLabel(self): pass
|
def computeLabel(self): pass
|
||||||
def showField(self, name): pass
|
def showField(self, name): pass
|
||||||
|
|
|
@ -729,10 +729,10 @@
|
||||||
<tal:comment replace="nothing">
|
<tal:comment replace="nothing">
|
||||||
This macro displays the global message on the page.
|
This macro displays the global message on the page.
|
||||||
</tal:comment>
|
</tal:comment>
|
||||||
<metal:message define-macro="message" i18n:domain="plone" >
|
<metal:message define-macro="message">
|
||||||
<tal:comment replace="nothing">Single message from portal_status_message request key</tal:comment>
|
<tal:comment replace="nothing">Single message from portal_status_message request key</tal:comment>
|
||||||
<div tal:define="msg request/portal_status_message | nothing"
|
<div tal:define="msg request/portal_status_message | nothing"
|
||||||
tal:condition="msg" class="portalMessage" tal:content="structure msg" i18n:translate=""></div>
|
tal:condition="msg" class="portalMessage" tal:content="structure msg"></div>
|
||||||
|
|
||||||
<tal:comment replace="nothing">Messages added via plone_utils</tal:comment>
|
<tal:comment replace="nothing">Messages added via plone_utils</tal:comment>
|
||||||
<tal:messages define="messages putils/showPortalMessages" condition="messages">
|
<tal:messages define="messages putils/showPortalMessages" condition="messages">
|
||||||
|
@ -741,7 +741,7 @@
|
||||||
repeat="msg messages">
|
repeat="msg messages">
|
||||||
<div tal:define="mtype msg/type | nothing;"
|
<div tal:define="mtype msg/type | nothing;"
|
||||||
tal:attributes="class python:mtype and type_css_map[mtype] or 'info';"
|
tal:attributes="class python:mtype and type_css_map[mtype] or 'info';"
|
||||||
tal:content="structure msg/message | nothing" i18n:translate=""></div>
|
tal:content="structure msg/message | nothing"></div>
|
||||||
</tal:msgs>
|
</tal:msgs>
|
||||||
</tal:messages>
|
</tal:messages>
|
||||||
</metal:message>
|
</metal:message>
|
||||||
|
|
|
@ -182,5 +182,5 @@ def do(transitionName, stateChange, logger):
|
||||||
if not msg:
|
if not msg:
|
||||||
msg = ploneObj.translate(u'Your content\'s status has been modified.',
|
msg = ploneObj.translate(u'Your content\'s status has been modified.',
|
||||||
domain='plone')
|
domain='plone')
|
||||||
ploneObj.plone_utils.addPortalMessage(msg)
|
ploneObj.say(msg)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -8,6 +8,7 @@ from appy.gen import Search
|
||||||
from appy.gen.utils import sequenceTypes
|
from appy.gen.utils import sequenceTypes
|
||||||
from appy.shared.utils import getOsTempFolder, executeCommand, normalizeString
|
from appy.shared.utils import getOsTempFolder, executeCommand, normalizeString
|
||||||
from appy.shared.xml_parser import XmlMarshaller
|
from appy.shared.xml_parser import XmlMarshaller
|
||||||
|
from appy.shared.csv_parser import CsvMarshaller
|
||||||
|
|
||||||
# Some error messages ----------------------------------------------------------
|
# Some error messages ----------------------------------------------------------
|
||||||
WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
|
WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
|
||||||
|
@ -19,6 +20,7 @@ class AbstractWrapper:
|
||||||
'''Any real Zope object has a companion object that is an instance of this
|
'''Any real Zope object has a companion object that is an instance of this
|
||||||
class.'''
|
class.'''
|
||||||
def __init__(self, o): self.__dict__['o'] = o
|
def __init__(self, o): self.__dict__['o'] = o
|
||||||
|
def appy(self): return self
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
appyType = self.o.getAppyType(name)
|
appyType = self.o.getAppyType(name)
|
||||||
|
@ -248,22 +250,8 @@ class AbstractWrapper:
|
||||||
wfTool.doActionFor(self.o, transitionName, comment=comment)
|
wfTool.doActionFor(self.o, transitionName, comment=comment)
|
||||||
del self.o._v_appy_do
|
del self.o._v_appy_do
|
||||||
|
|
||||||
def log(self, message, type='info'):
|
def log(self, message, type='info'): return self.o.log(message, type)
|
||||||
'''Logs a message in the log file. p_logLevel may be "info", "warning"
|
def say(self, message, type='info'): return self.o.say(message, type)
|
||||||
or "error".'''
|
|
||||||
logger = self.o.getProductConfig().logger
|
|
||||||
if type == 'warning': logMethod = logger.warn
|
|
||||||
elif type == 'error': logMethod = logger.error
|
|
||||||
else: logMethod = logger.info
|
|
||||||
logMethod(message)
|
|
||||||
|
|
||||||
def say(self, message, type='info'):
|
|
||||||
'''Prints a message in the user interface. p_logLevel may be "info",
|
|
||||||
"warning" or "error".'''
|
|
||||||
mType = type
|
|
||||||
if mType == 'warning': mType = 'warn'
|
|
||||||
elif mType == 'error': mType = 'stop'
|
|
||||||
self.o.plone_utils.addPortalMessage(message, type=mType)
|
|
||||||
|
|
||||||
def normalize(self, s, usage='fileName'):
|
def normalize(self, s, usage='fileName'):
|
||||||
'''Returns a version of string p_s whose special chars have been
|
'''Returns a version of string p_s whose special chars have been
|
||||||
|
@ -346,29 +334,46 @@ class AbstractWrapper:
|
||||||
method in those cases.'''
|
method in those cases.'''
|
||||||
self.o.reindexObject()
|
self.o.reindexObject()
|
||||||
|
|
||||||
def export(self, at='string'):
|
def export(self, at='string', format='xml', include=None, exclude=None):
|
||||||
'''Creates an "exportable", XML version of this object. If p_at is
|
'''Creates an "exportable" version of this object. p_format is XML by
|
||||||
"string", this method returns the XML version, without the XML
|
default, but can also be "csv". If p_format is:
|
||||||
prologue. Else, (a) if not p_at, the XML will be exported on disk,
|
* "xml", if p_at is "string", this method returns the XML version,
|
||||||
in the OS temp folder, with an ugly name; (b) else, it will be
|
without the XML prologue. Else, (a) if not p_at, the XML
|
||||||
exported at path p_at.'''
|
will be exported on disk, in the OS temp folder, with an
|
||||||
# Determine where to put the result
|
ugly name; (b) else, it will be exported at path p_at.
|
||||||
toDisk = (at != 'string')
|
* "csv", if p_at is "string", this method returns the CSV data as a
|
||||||
if toDisk and not at:
|
string. If p_at is an opened file handler, the CSV line will
|
||||||
at = getOsTempFolder() + '/' + self.o.UID() + '.xml'
|
be appended in it.
|
||||||
# Create the XML version of the object
|
If p_include is given, only fields whose names are in it will be
|
||||||
marshaller = XmlMarshaller(cdata=True, dumpUnicode=True,
|
included. p_exclude, if given, contains names of fields that will
|
||||||
dumpXmlPrologue=toDisk,
|
not be included in the result.
|
||||||
rootTag=self.klass.__name__)
|
'''
|
||||||
xml = marshaller.marshall(self.o, objectType='appy')
|
if format == 'xml':
|
||||||
# Produce the desired result
|
# Todo: take p_include and p_exclude into account.
|
||||||
if toDisk:
|
# Determine where to put the result
|
||||||
f = file(at, 'w')
|
toDisk = (at != 'string')
|
||||||
f.write(xml.encode('utf-8'))
|
if toDisk and not at:
|
||||||
f.close()
|
at = getOsTempFolder() + '/' + self.o.UID() + '.xml'
|
||||||
return at
|
# Create the XML version of the object
|
||||||
else:
|
marshaller = XmlMarshaller(cdata=True, dumpUnicode=True,
|
||||||
return xml
|
dumpXmlPrologue=toDisk,
|
||||||
|
rootTag=self.klass.__name__)
|
||||||
|
xml = marshaller.marshall(self.o, objectType='appy')
|
||||||
|
# Produce the desired result
|
||||||
|
if toDisk:
|
||||||
|
f = file(at, 'w')
|
||||||
|
f.write(xml.encode('utf-8'))
|
||||||
|
f.close()
|
||||||
|
return at
|
||||||
|
else:
|
||||||
|
return xml
|
||||||
|
elif format == 'csv':
|
||||||
|
if isinstance(at, basestring):
|
||||||
|
marshaller = CsvMarshaller(include=include, exclude=exclude)
|
||||||
|
return marshaller.marshall(self)
|
||||||
|
else:
|
||||||
|
marshaller = CsvMarshaller(at, include=include, exclude=exclude)
|
||||||
|
marshaller.marshall(self)
|
||||||
|
|
||||||
def historize(self, data):
|
def historize(self, data):
|
||||||
'''This method allows to add "manually" a "data-change" event into the
|
'''This method allows to add "manually" a "data-change" event into the
|
||||||
|
|
|
@ -82,6 +82,7 @@ class PoMessage:
|
||||||
BAD_LONG = 'An integer value is expected; do not enter any space.'
|
BAD_LONG = 'An integer value is expected; do not enter any space.'
|
||||||
BAD_FLOAT = 'A floating-point number is expected; use the dot as decimal ' \
|
BAD_FLOAT = 'A floating-point number is expected; use the dot as decimal ' \
|
||||||
'separator, not a comma; do not enter any space.'
|
'separator, not a comma; do not enter any space.'
|
||||||
|
BAD_DATE = 'Please specify a valid date.'
|
||||||
BAD_EMAIL = 'Please enter a valid email.'
|
BAD_EMAIL = 'Please enter a valid email.'
|
||||||
BAD_URL = 'Please enter a valid URL.'
|
BAD_URL = 'Please enter a valid URL.'
|
||||||
BAD_ALPHANUMERIC = 'Please enter a valid alphanumeric value.'
|
BAD_ALPHANUMERIC = 'Please enter a valid alphanumeric value.'
|
||||||
|
@ -142,6 +143,7 @@ class PoMessage:
|
||||||
oldDefault = self.default
|
oldDefault = self.default
|
||||||
if self.default != newMsg.default:
|
if self.default != newMsg.default:
|
||||||
# The default value has changed in the pot file
|
# The default value has changed in the pot file
|
||||||
|
oldDefault = self.default
|
||||||
self.default = newMsg.default
|
self.default = newMsg.default
|
||||||
if self.msg.strip():
|
if self.msg.strip():
|
||||||
self.fuzzy = True
|
self.fuzzy = True
|
||||||
|
@ -149,6 +151,12 @@ class PoMessage:
|
||||||
# because the default value has changed) only if the user
|
# because the default value has changed) only if the user
|
||||||
# has already entered a message. Else, this has no sense to
|
# has already entered a message. Else, this has no sense to
|
||||||
# rewrite the empty message.
|
# rewrite the empty message.
|
||||||
|
if not oldDefault.strip():
|
||||||
|
# This is a strange case: the old default value did not
|
||||||
|
# exist. Maybe was this PO file generated from some
|
||||||
|
# tool, but simply without any default value. So in
|
||||||
|
# this case, we do not consider the label as fuzzy.
|
||||||
|
self.fuzzy = False
|
||||||
else:
|
else:
|
||||||
self.fuzzy = False
|
self.fuzzy = False
|
||||||
if (language == 'en'):
|
if (language == 'en'):
|
||||||
|
|
|
@ -40,43 +40,43 @@
|
||||||
<@style@:paragraph-properties @fo@:keep-with-next="always"/>
|
<@style@:paragraph-properties @fo@:keep-with-next="always"/>
|
||||||
</@style@:style>
|
</@style@:style>
|
||||||
<@text@:list-style @style@:name="podBulletedList">
|
<@text@:list-style @style@:name="podBulletedList">
|
||||||
<@text@:list-level-style-bullet @text@:level="1" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="●">
|
<@text@:list-level-style-bullet @text@:level="1" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="•">
|
||||||
<@style@:list-level-properties @text@:space-before="0.25in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="0.25in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
<@text@:list-level-style-bullet @text@:level="2" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="○">
|
<@text@:list-level-style-bullet @text@:level="2" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="◦">
|
||||||
<@style@:list-level-properties @text@:space-before="0.5in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="0.5in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
<@text@:list-level-style-bullet @text@:level="3" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="■">
|
<@text@:list-level-style-bullet @text@:level="3" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="▪">
|
||||||
<@style@:list-level-properties @text@:space-before="0.75in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="0.75in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
<@text@:list-level-style-bullet @text@:level="4" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="●">
|
<@text@:list-level-style-bullet @text@:level="4" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="•">
|
||||||
<@style@:list-level-properties @text@:space-before="1in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="1in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
<@text@:list-level-style-bullet @text@:level="5" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="○">
|
<@text@:list-level-style-bullet @text@:level="5" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="◦">
|
||||||
<@style@:list-level-properties @text@:space-before="1.25in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="1.25in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
<@text@:list-level-style-bullet @text@:level="6" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="■">
|
<@text@:list-level-style-bullet @text@:level="6" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="▪">
|
||||||
<@style@:list-level-properties @text@:space-before="1.5in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="1.5in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
<@text@:list-level-style-bullet @text@:level="7" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="●">
|
<@text@:list-level-style-bullet @text@:level="7" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="•">
|
||||||
<@style@:list-level-properties @text@:space-before="1.75in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="1.75in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
<@text@:list-level-style-bullet @text@:level="8" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="○">
|
<@text@:list-level-style-bullet @text@:level="8" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="◦">
|
||||||
<@style@:list-level-properties @text@:space-before="2in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="2in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
<@text@:list-level-style-bullet @text@:level="9" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="■">
|
<@text@:list-level-style-bullet @text@:level="9" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="▪">
|
||||||
<@style@:list-level-properties @text@:space-before="2.25in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="2.25in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
<@text@:list-level-style-bullet @text@:level="10" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="●">
|
<@text@:list-level-style-bullet @text@:level="10" @text@:style-name="podBulletStyle" @style@:num-suffix="." @text@:bullet-char="•">
|
||||||
<@style@:list-level-properties @text@:space-before="2.5in" @text@:min-label-width="0.25in"/>
|
<@style@:list-level-properties @text@:space-before="2.5in" @text@:min-label-width="0.25in"/>
|
||||||
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
<@style@:text-properties @style@:font-name="PodStarSymbol"/>
|
||||||
</@text@:list-level-style-bullet>
|
</@text@:list-level-style-bullet>
|
||||||
|
|
|
@ -177,7 +177,8 @@ class Test(appy.shared.test.Test):
|
||||||
areXml=True, xmlTagsToIgnore=(
|
areXml=True, xmlTagsToIgnore=(
|
||||||
(OdfEnvironment.NS_DC, 'date'),
|
(OdfEnvironment.NS_DC, 'date'),
|
||||||
(OdfEnvironment.NS_STYLE, 'style')),
|
(OdfEnvironment.NS_STYLE, 'style')),
|
||||||
xmlAttrsToIgnore=('draw:name','text:name'), encoding='utf-8')
|
xmlAttrsToIgnore=('draw:name','text:name','text:bullet-char'),
|
||||||
|
encoding='utf-8')
|
||||||
if diffOccurred:
|
if diffOccurred:
|
||||||
res = True
|
res = True
|
||||||
break
|
break
|
||||||
|
|
1102
pod/test/Tests.rtf
1102
pod/test/Tests.rtf
File diff suppressed because it is too large
Load diff
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
from appy import Object
|
from appy import Object
|
||||||
|
from appy.gen.utils import sequenceTypes
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
WRONG_LINE = 'Line number %d in file %s does not have the right number of ' \
|
WRONG_LINE = 'Line number %d in file %s does not have the right number of ' \
|
||||||
|
@ -198,4 +199,97 @@ class CsvParser:
|
||||||
newValue = self.resolveReference(attrName, attrValue)
|
newValue = self.resolveReference(attrName, attrValue)
|
||||||
setattr(obj, attrName, newValue)
|
setattr(obj, attrName, newValue)
|
||||||
return self.res
|
return self.res
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
class CsvMarshaller:
|
||||||
|
'''This class is responsible for producing a string, CSV-ready, line of data
|
||||||
|
from a Appy object.'''
|
||||||
|
undumpable = ('File', 'Action', 'Info', 'Pod')
|
||||||
|
def __init__(self, at=None, sep=';', subSep=',', wrap='"',
|
||||||
|
includeHeaders=True, include=None, exclude=None):
|
||||||
|
# If specified, p_at is an opened file handler to the CSV file to fill
|
||||||
|
self.at = at
|
||||||
|
# The CSV field separator
|
||||||
|
self.sep = sep
|
||||||
|
# The sub-separator for multi-valued fields
|
||||||
|
self.subSep = subSep
|
||||||
|
# The "wrap" char will wrap any value that contains self.sep.
|
||||||
|
self.wrap = wrap
|
||||||
|
# Must we put field names as first line in the CSV?
|
||||||
|
self.includeHeaders = includeHeaders
|
||||||
|
# If p_include is given, it lists names of fields that will be included
|
||||||
|
self.include = include
|
||||||
|
# If p_exclude is given, it lists names of fields that will be excluded
|
||||||
|
self.exclude = exclude
|
||||||
|
|
||||||
|
def marshallString(self, value):
|
||||||
|
'''Produces a version of p_value that can be put in the CSV file.'''
|
||||||
|
return value.replace('\r\n', ' ').replace('\n', ' ')
|
||||||
|
|
||||||
|
def marshallValue(self, field, value):
|
||||||
|
'''Produces a version of p_value that can be dumped in a CSV file.'''
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
# Format the string as a one-line CSV-ready value
|
||||||
|
res = self.marshallString(value)
|
||||||
|
elif type(value) in sequenceTypes:
|
||||||
|
# Create a list of values, separated by a sub-separator.
|
||||||
|
res = []
|
||||||
|
for v in value:
|
||||||
|
res.append(self.marshallValue(field, v))
|
||||||
|
res = self.subSep.join(res)
|
||||||
|
elif hasattr(value, 'klass') and hasattr(value, 'title'):
|
||||||
|
# This is a reference to another object. Dump only its title.
|
||||||
|
res = value.title
|
||||||
|
elif value == None:
|
||||||
|
# Empty string is more beautiful than 'None'
|
||||||
|
res = ''
|
||||||
|
else:
|
||||||
|
res = str(value)
|
||||||
|
# If self.sep is found among this value, we must wrap it with self.wrap
|
||||||
|
if self.sep in res:
|
||||||
|
# Double any wrapper char if present
|
||||||
|
res = res.replace(self.wrap, '%s%s' % (self.wrap, self.wrap))
|
||||||
|
# Wrap the value
|
||||||
|
res = '%s%s%s' % (self.wrap, res, self.wrap)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def includeField(self, field):
|
||||||
|
'''Must p_field be included in the result ?'''
|
||||||
|
# Check self.include and self.exclude
|
||||||
|
if self.include and field.name not in self.include: return False
|
||||||
|
if self.exclude and field.name in self.exclude: return False
|
||||||
|
# Check field type
|
||||||
|
if field.type in self.undumpable: return False
|
||||||
|
# Don't dump password fields
|
||||||
|
if (field.type == 'String') and (field.format == 3): return False
|
||||||
|
if (field.type == 'Ref') and field.isBack: return False
|
||||||
|
if (field.type == 'Computed') and not field.plainText: return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def marshall(self, obj):
|
||||||
|
'''Creates the CSV line representing p_obj and dumps it in self.at if
|
||||||
|
specified, or return it else.'''
|
||||||
|
obj = obj.appy()
|
||||||
|
res = []
|
||||||
|
# Dump the header line if required, and if there is still no line
|
||||||
|
# dumped in self.at.
|
||||||
|
headers = []
|
||||||
|
if self.includeHeaders and self.at and (self.at.tell() == 0):
|
||||||
|
for field in obj.fields:
|
||||||
|
if not self.includeField(field): continue
|
||||||
|
headers.append(field.name)
|
||||||
|
self.at.write(self.sep.join(headers))
|
||||||
|
self.at.write('\n')
|
||||||
|
# Dump the data line.
|
||||||
|
for field in obj.fields:
|
||||||
|
if not self.includeField(field): continue
|
||||||
|
# Get the field value
|
||||||
|
value = field.getValue(obj.o)
|
||||||
|
value = self.marshallValue(field, value)
|
||||||
|
res.append(value)
|
||||||
|
res = self.sep.join(res)
|
||||||
|
if self.at:
|
||||||
|
self.at.write(res)
|
||||||
|
self.at.write('\n')
|
||||||
|
else: return res
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
Loading…
Reference in a new issue