[gen] More work on string multilingual fields.

This commit is contained in:
Gaetan Delannay 2014-08-13 17:17:25 +02:00
parent 6770d23a50
commit 4131ba899e
5 changed files with 210 additions and 87 deletions

View file

@ -63,7 +63,7 @@ class Field:
value=not isSearch and \
field.getFormattedValue(zobj, rawValue, showChanges);
requestValue=not isSearch and zobj.getRequestFieldValue(name);
inRequest=req.has_key(name);
inRequest=field.valueIsInRequest(req, name);
error=req.get('%s_error' % name);
isMultiple=(field.multiplicity[1] == None) or \
(field.multiplicity[1] > 1);
@ -544,6 +544,14 @@ class Field:
return res
return value
def valueIsInRequest(self, request, name):
'''Is there a value corresponding to this field in the request? p_name
can be different from self.name (ie, if it is a field within another
(List) field). In most cases, checking that this p_name is in the
request is sufficient. But in some cases it may be more complex, ie
for string multilingual fields.'''
return request.has_key(name)
def getRequestValue(self, request, requestName=None):
'''Gets a value for this field as carried in the request object. In the
simplest cases, the request value is a single value whose name in the
@ -601,6 +609,18 @@ class Field:
'''Returns True if the p_value must be considered as an empty value.'''
return value in self.nullValues
def isCompleteValue(self, value, obj=None):
'''Returns True if the p_value must be considered as "complete". While,
in most cases, a "complete" value simply means a "non empty" value
(see m_isEmptyValue above), in some special cases it is more subtle.
For example, a multilingual string value is not empty as soon as a
value is given for some language but will not be considered as
complete while a value is missing for some language. Another example:
a Date with the "hour" part required will not be considered as empty
if the "day, month, year" part is present but will not be considered
as complete without the "hour, minute" part.'''
return not self.isEmptyValue(value, obj)
def validateValue(self, obj, value):
'''This method may be overridden by child classes and will be called at
the right moment by m_validate defined below for triggering
@ -623,8 +643,8 @@ class Field:
m_getRequestValue defined above, is valid according to this type
definition. If it is the case, None is returned. Else, a translated
error message is returned.'''
# Check that a value is given if required.
if self.isEmptyValue(value, obj):
# If the value is required, check that a (complete) value is present.
if not self.isCompleteValue(value, obj):
if self.required and self.isClientVisible(obj):
# If the field is required, but not visible according to
# master/slave relationships, we consider it not to be required.

View file

@ -35,6 +35,7 @@ class Boolean(Field):
cLayouts = {'view': 'lf|', 'edit': 'flrv|'}
# Layout for radio buttons (render = "radios")
rLayouts = {'edit': 'f', 'view': 'f', 'search': 'l-f'}
rlLayouts = {'edit': 'l-f', 'view': 'lf', 'search': 'l-f'}
pxView = pxCell = Px('''<x>:value</x>
<input type="hidden" if="masterCss"

View file

@ -77,6 +77,13 @@ class String(Field):
URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \
'(([0-9]{1,5})?\/.*)?')
# Possible values for "format"
LINE = 0
TEXT = 1
XHTML = 2
PASSWORD = 3
CAPTCHA = 4
pxView = Px('''
<x var="fmt=field.format; isUrl=field.isUrl;
mayAjaxEdit=not showChanges and field.inlineEdit and \
@ -101,16 +108,45 @@ class String(Field):
<div if="not mayAjaxEdit" class="xhtml">::value or '-'</div>
<div if="mayAjaxEdit" class="xhtml" contenteditable="true"
id=":'%s_%s_ck' % (zobj.id, name)">::value or '-'</div>
<script if="mayAjaxEdit">::field.getJsInlineInit(zobj)</script>
<script if="mayAjaxEdit">::field.getJsInlineInit(zobj,None)</script>
</x>
<span if="not value and (fmt != 2)" class="smaller">-</span>
<input type="hidden" if="masterCss" class=":masterCss" value=":rawValue"
name=":name" id=":name"/>
</x>''')
# pxEdit part for formats String.LINE (but that are not selections),
# String.PASSWORD and String.CAPTCHA.
pxEditText = Px('''
<input var="inputId=not lg and name or '%s_%s' % (name, lg);
placeholder=field.getAttribute(obj, 'placeholder') or ''"
id=":inputId" name=":inputId" size=":field.width"
maxlength=":field.maxChars" placeholder=":placeholder"
value=":inRequest and requestValue or value"
style=":'text-transform:%s' % field.transform"
type=":(fmt == 3) and 'password' or 'text'"/>
<!-- Display a captcha if required -->
<span if="fmt == 4">:_('captcha_text', \
mapping=field.getCaptchaChallenge(req.SESSION))
</span>''')
# pxEdit part for formats String.TEXT and String.XHTML.
pxEditTextArea = Px('''
<textarea var="inputId=not lg and name or '%s_%s' % (name, lg)"
id=":inputId" name=":inputId" cols=":field.width"
class=":(fmt == 2) and ('rich_%s' % name) or ''"
style=":'text-transform:%s' % field.transform"
rows=":field.height">:inRequest and requestValue or value
</textarea>
<script if="fmt == 2"
type="text/javascript">::field.getJsInit(zobj, lg)</script>''')
# Mapping formats -> PXs defining the edit widgets.
editPx = {LINE:pxEditText, TEXT:pxEditTextArea, XHTML:pxEditTextArea,
PASSWORD:pxEditText, CAPTCHA:pxEditText}
pxEdit = Px('''
<x var="fmt=field.format;
isOneLine=fmt in (0,3,4)">
multilingual=field.isMultilingual()">
<select if="field.isSelect"
var2="possibleValues=field.getPossibleValues(zobj, \
withTranslations=True, withBlankValue=True)"
@ -122,28 +158,22 @@ class String(Field):
selected=":field.isSelected(zobj, name, val[0], rawValue)"
title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
</select>
<x if="isOneLine and not field.isSelect"
var2="placeholder=field.getAttribute(obj, 'placeholder') or ''">
<input id=":name" name=":name" size=":field.width"
maxlength=":field.maxChars" placeholder=":placeholder"
value=":inRequest and requestValue or value"
style=":'text-transform:%s' % field.transform"
type=":(fmt == 3) and 'password' or 'text'"/>
<!-- Display a captcha if required -->
<span if="fmt == 4">:_('captcha_text', \
mapping=field.getCaptchaChallenge(req.SESSION))
</span>
<!-- Any other unilingual field. -->
<x if="not field.isSelect and not multilingual"
var2="lg=None">:field.editPx[fmt]</x>
<!-- Any other multilingual field. -->
<table if="not field.isSelect and multilingual" width="100%">
<tr valign="top">
<x for="lg in field.languages">
<td style=":(fmt==2) and 'padding-top:1px' or 'padding-top:3px'"
width="25px">
<span class="language help"
title=":ztool.getLanguageName(lg)">:lg.upper()</span></td>
<td var="requestValue=requestValue[lg];
value=value[lg]|''">:field.editPx[fmt]</td>
</x>
<x if="fmt in (1,2)">
<textarea id=":name" name=":name" cols=":field.width"
class=":(fmt == 2) and ('rich_%s' % name) or ''"
style=":'text-transform:%s' % field.transform"
rows=":field.height">:inRequest and requestValue or value
</textarea>
<script if="fmt == 2"
type="text/javascript">::field.getJsInit(zobj)</script>
</x>
</x>''')
</tr>
</table></x>''')
pxCell = Px('''
<x var="multipleValues=value and isMultiple">
@ -285,12 +315,6 @@ class String(Field):
if not alpha.match(c): return False
return True
# Possible values for "format"
LINE = 0
TEXT = 1
XHTML = 2
PASSWORD = 3
CAPTCHA = 4
def __init__(self, validator=None, multiplicity=(0,1), default=None,
format=LINE, show=True, page='main', group=None, layouts=None,
move=0, indexed=False, searchable=False,
@ -300,7 +324,7 @@ class String(Field):
label=None, sdefault='', scolspan=1, swidth=None, sheight=None,
persist=True, transform='none', placeholder=None,
styles=('p','h1','h2','h3','h4'), allowImageUpload=True,
spellcheck=False, contentLanguage=None, inlineEdit=False):
spellcheck=False, languages=('en',), inlineEdit=False):
# According to format, the widget will be different: input field,
# textarea, inline editor... Note that there can be only one String
# field of format CAPTCHA by page, because the captcha challenge is
@ -314,8 +338,11 @@ class String(Field):
self.allowImageUpload = allowImageUpload
# When format is XHTML, do we run the CK spellchecker ?
self.spellcheck = spellcheck
# What is the language of field content?
self.contentLanguage = contentLanguage
# If "languages" holds more than one language, the field will be
# multi-lingual and several widgets will allow to edit/visualize the
# field content in all the supported languages. The field is also used
# by the CK spell checker.
self.languages = languages
# When format in XHTML, can the field be inline-edited (ckeditor)?
self.inlineEdit = inlineEdit
# The following field has a direct impact on the text entered by the
@ -361,6 +388,12 @@ class String(Field):
not self.isSelect
self.swidth = self.swidth or self.width
self.sheight = self.sheight or self.height
self.checkParameters()
def checkParameters(self):
'''Ensures this String is correctly defined.'''
if self.isSelect and self.isMultilingual():
raise Exception("A selection field can't be multilingual.")
def isSelection(self):
'''Does the validator of this type definition define a list of values
@ -376,6 +409,8 @@ class String(Field):
res = False
return res
def isMultilingual(self): return len(self.languages) > 1
def getDefaultLayouts(self):
'''Returns the default layouts for this type. Default layouts can vary
acccording to format, multiplicity or history.'''
@ -394,7 +429,7 @@ class String(Field):
return {'view': 'l-f', 'edit': 'lrv-f'}
def getValue(self, obj):
# Cheat if this field represents p_obj's state
# Cheat if this field represents p_obj's state.
if self.name == 'state': return obj.State()
value = Field.getValue(self, obj)
if not value:
@ -406,27 +441,46 @@ class String(Field):
value = list(value)
return value
def store(self, obj, value):
'''When the value is XHTML, we perform some cleanup.'''
if not self.persist: return
if (self.format == String.XHTML) and value:
# When image upload is allowed, ckeditor inserts some "style" attrs
# (ie for image size when images are resized). So in this case we
# can't remove style-related information.
try:
value = XhtmlCleaner(keepStyles=False).clean(value)
except XhtmlCleaner.Error, e:
# Errors while parsing p_value can't prevent the user from
# storing it.
obj.log('Unparsable XHTML content in field "%s".' % self.name,
type='warning')
Field.store(self, obj, value)
def valueIsInRequest(self, request, name):
if not self.isMultilingual():
return Field.valueIsInRequest(self, request, name)
return request.has_key('%s_%s' % (name, self.languages[0]))
def storeFromAjax(self, obj):
'''Stores the new field value from an Ajax request, or do nothing if
the action was canceled.'''
rq = obj.REQUEST
if rq.get('cancel') != 'True': self.store(obj, rq['fieldContent'])
def getRequestValue(self, request, requestName=None):
'''The request value may be multilingual.'''
name = requestName or self.name
# A unilingual field.
if not self.isMultilingual(): return request.get(name, None)
# A multilingual field.
res = {}
for language in self.languages:
res[language] = request.get('%s_%s' % (name, language), None)
return res
def isEmptyValue(self, value, obj=None):
'''Returns True if the p_value must be considered as an empty value.'''
if not self.isMultilingual():
return Field.isEmptyValue(self, value, obj)
# For a multilingual value, as soon as a value is not empty for a given
# language, the whole value is considered as not being empty.
if not value: return True
for v in value.itervalues():
if not Field.isEmptyValue(self, v, obj): return
return True
def isCompleteValue(self, value, obj=None):
'''Returns True if the p_value must be considered as complete. For a
unilingual field, being complete simply means not being empty. For a
multilingual field, being complete means that a value is present for
every language'''
if not self.isMultilingual():
return Field.isCompleteValue(self, value, obj)
# As soon as a given language value is empty, the global value is not
# complete.
if not value: return True
for v in value.itervalues():
if Field.isEmptyValue(self, v, obj): return
return True
def getDiffValue(self, obj, value):
'''Returns a version of p_value that includes the cumulative diffs
@ -452,8 +506,8 @@ class String(Field):
comparator = HtmlDiff(res, value or '', iMsg, dMsg)
return comparator.get()
def getFormattedValue(self, obj, value, showChanges=False):
if self.isEmptyValue(value): return ''
def getUnilingualFormattedValue(self, obj, value, showChanges=False):
if Field.isEmptyValue(self, value): return ''
res = value
if self.isSelect:
if isinstance(self.validator, Selection):
@ -480,6 +534,17 @@ class String(Field):
(res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res
return res
def getFormattedValue(self, obj, value, showChanges=False):
if not self.isMultilingual():
return self.getUnilingualFormattedValue(obj, value, showChanges)
# Return the dict of values whose individual, language-specific values
# have been formatted via m_getUnilingualFormattedValue.
if not value: return value
res = {}
for lg in self.languages:
res[lg]=self.getUnilingualFormattedValue(obj,value[lg],showChanges)
return res
emptyStringTuple = ('',)
emptyValuesCatalogIgnored = (None, '')
def getIndexValue(self, obj, forSearch=False):
@ -623,12 +688,23 @@ class String(Field):
elif self.transform == 'capitalize': return value.capitalize()
return value
def getStorableValue(self, value):
def getUnilingualStorableValue(self, value):
isString = isinstance(value, basestring)
isEmpty = Field.isEmptyValue(self, value)
# Apply transform if required
if isString and not self.isEmptyValue(value) and \
(self.transform != 'none'):
if isString and not isEmpty and (self.transform != 'none'):
value = self.applyTransform(value)
# Clean XHTML if format is XHTML
if (self.format == String.XHTML) and not isEmpty:
# When image upload is allowed, ckeditor inserts some "style" attrs
# (ie for image size when images are resized). So in this case we
# can't remove style-related information.
try:
value = XhtmlCleaner(keepStyles=False).clean(value)
except XhtmlCleaner.Error, e:
# Errors while parsing p_value can't prevent the user from
# storing it.
pass
# Truncate the result if longer than self.maxChars
if isString and self.maxChars and (len(value) > self.maxChars):
value = value[:self.maxChars]
@ -638,6 +714,33 @@ class String(Field):
value = [value]
return value
def getStorableValue(self, value):
if not self.isMultilingual():
return self.getUnilingualStorableValue(value)
# A multilingual value is stored as a dict whose keys are ISO 2-letters
# language codes and whose values are strings storing content in the
# language ~{s_language: s_content}~.
if not value: return
for lg in self.languages:
value[lg] = self.getUnilingualStorableValue(value[lg])
return value
def store(self, obj, value):
'''Stores p_value on p_obj for this field.'''
if self.isMultilingual() and value and \
(not isinstance(value, dict) or (len(value) != len(self.languages))):
raise Exception('Multilingual field "%s" accepts a dict whose '\
'keys are in field.languages and whose ' \
'values are strings.' % self.name)
Field.store(self, obj, value)
def storeFromAjax(self, obj):
'''Stores the new field value from an Ajax request, or do nothing if
the action was canceled.'''
rq = obj.REQUEST
if rq.get('cancel') != 'True':
self.store(obj, self.getStorableValue(rq['fieldContent']))
def getIndexType(self):
'''Index type varies depending on String parameters.'''
# If String.isSelect, be it multivalued or not, we define a ListIndex:
@ -679,19 +782,19 @@ class String(Field):
'fi': 'fi_FI', 'fr': 'fr_FR', 'de': 'de_DE', 'el': 'el_GR',
'it': 'it_IT', 'nb': 'nb_NO', 'pt': 'pt_PT', 'es': 'es_ES',
'sv': 'sv_SE'}
def getCkLanguage(self):
'''Gets the language for CK editor SCAYT. We will use
self.contentLanguage. If it is not supported by CK, we use
english.'''
lang = self.contentLanguage
if lang and (lang in self.ckLanguages): return self.ckLanguages[lang]
def getCkLanguage(self, language):
'''Gets the language for CK editor SCAYT. p_language is one of
self.languages if the field is multilingual, None else. If p_language
is not supported by CK, we use english.'''
if not language: language = self.languages[0]
if language in self.ckLanguages: return self.ckLanguages[language]
return 'en_US'
def getCkParams(self, obj):
def getCkParams(self, obj, language):
'''Gets the base params to set on a rich text field.'''
ckAttrs = {'toolbar': 'Appy',
'format_tags': ';'.join(self.styles),
'scayt_sLang': self.getCkLanguage()}
'scayt_sLang': self.getCkLanguage(language)}
if self.width: ckAttrs['width'] = self.width
if self.spellcheck: ckAttrs['scayt_autoStartup'] = True
if self.allowImageUpload:
@ -704,31 +807,28 @@ class String(Field):
ck.append('%s: %s' % (k, sv))
return ', '.join(ck)
def getJsInit(self, obj):
def getJsInit(self, obj, language):
'''Gets the Javascript init code for displaying a rich editor for this
field (rich field only).'''
field (rich field only). If the field is multilingual, we must init
the rich text editor for a given p_language (among self.languages).
Else, p_languages is None.'''
name = not language and self.name or ('%s_%s' % (self.name, language))
return 'CKEDITOR.replace("%s", {%s})' % \
(self.name, self.getCkParams(obj))
(name, self.getCkParams(obj, language))
def getJsInlineInit(self, obj):
def getJsInlineInit(self, obj, language):
'''Gets the Javascript init code for enabling inline edition of this
field (rich text only).'''
field (rich text only). If the field is multilingual, the current
p_language is given. Else, p_language is None.'''
uid = obj.id
name = not language and self.name or ('%s_%s' % (self.name, language))
return "CKEDITOR.disableAutoInline = true;\n" \
"CKEDITOR.inline('%s_%s_ck', {%s, on: {blur: " \
"function( event ) { var content = event.editor.getData(); " \
"doInlineSave('%s', '%s', '%s', content)}}})" % \
(uid, self.name, self.getCkParams(obj), uid, self.name,
(uid, name, self.getCkParams(obj, language), uid, name,
obj.absolute_url())
return "CKEDITOR.disableAutoInline = true;\n" \
"CKEDITOR.inline('%s_%s_ck', {on: {blur: " \
"function( event ) { var data = event.editor.getData(); " \
"askAjaxChunk('%s_%s','POST','%s','%s:pxSave', " \
"{'fieldContent': encodeURIComponent(data)}, " \
"null, evalInnerScripts);}}});"% \
(uid, self.name, uid, self.name, obj.absolute_url(), self.name)
def isSelected(self, obj, fieldName, vocabValue, dbValue):
'''When displaying a selection box (only for fields with a validator
being a list), must the _vocabValue appear as selected? p_fieldName

View file

@ -29,13 +29,13 @@ input[type=submit] { border: 1px solid #d0d0d0; background-color: #f8f8f8;
cursor: pointer }
input[type=password] { border: 1px solid #d0d0d0; background-color: #f8f8f8;
font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif }
input[type=text] { border: 1px solid #d0d0d0; background-color: #f8f8f8;
input[type=text] { border: 1px solid #d0d0d0;
font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif;
margin-bottom: 1px }
select { border: 1px solid #d0d0d0; background-color: #f8f8f8 }
textarea { width: 99%; font: 100% "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif;
border: 1px solid #d0d0d0; background-color: #f8f8f8 }
border: 1px solid #d0d0d0; background-color: white }
label { color: #888888; font-size: 11px; margin: 3px 0;
text-transform: uppercase }
legend { padding-bottom: 2px; padding-right: 3px; color: black }
@ -167,3 +167,5 @@ td.search { padding-top: 8px }
.tabs { position:relative; bottom:-2px }
.tab { padding: 0 10px 0 10px; text-align: center; font-size: 90%;
font-weight: bold}
.language {color: grey; font-size: 7pt; border: grey 1px solid; padding: 2px;
margin: 0 2px 0 4px}