[gen] More work on multilingual string fields.

This commit is contained in:
Gaetan Delannay 2014-08-14 16:46:37 +02:00
parent 4131ba899e
commit 7c2d4c1b7a
3 changed files with 146 additions and 70 deletions

View file

@ -529,20 +529,19 @@ class Field:
If p_forSearch is True, it will return a "string" version of the If p_forSearch is True, it will return a "string" version of the
index value suitable for a global search.''' index value suitable for a global search.'''
value = self.getValue(obj) res = self.getValue(obj)
if forSearch and (value != None): # Zope catalog does not like unicode strings.
if isinstance(value, unicode): if isinstance(res, unicode): res = res.encode('utf-8')
res = value.encode('utf-8') if forSearch and (res != None):
elif type(value) in sutils.sequenceTypes: if type(res) in sutils.sequenceTypes:
res = [] vals = []
for v in value: for v in res:
if isinstance(v, unicode): res.append(v.encode('utf-8')) if isinstance(v, unicode): vals.append(v.encode('utf-8'))
else: res.append(str(v)) else: vals.append(str(v))
res = ' '.join(res) res = ' '.join(vals)
else: else:
res = str(value) res = str(res)
return res return res
return value
def valueIsInRequest(self, request, name): def valueIsInRequest(self, request, name):
'''Is there a value corresponding to this field in the request? p_name '''Is there a value corresponding to this field in the request? p_name

View file

@ -84,40 +84,87 @@ class String(Field):
PASSWORD = 3 PASSWORD = 3
CAPTCHA = 4 CAPTCHA = 4
# Default ways to render multingual fields
defaultLanguagesLayouts = {
LINE: {'edit': 'vertical', 'view': 'vertical'},
TEXT: {'edit': 'horizontal', 'view': 'vertical'},
XHTML: {'edit': 'horizontal', 'view': 'horizontal'},
}
# pxView part for formats String.LINE (but that are not selections) and
# String.PASSWORD (a fake view for String.PASSWORD and no view at all for
# String.CAPTCHA).
pxViewLine = Px('''
<span if="not value" class="smaller">-</span>
<x if="value">
<!-- A password -->
<x if="fmt == 3">********</x>
<!-- A URL -->
<a if="(fmt != 3) and isUrl" target="_blank" href=":value">:value</a>
<!-- Any other value -->
<x if="(fmt != 3) and not isUrl">::value</x>
</x>''')
# pxView part for format String.TEXT.
pxViewText = Px('''
<span if="not value" class="smaller">-</span>
<x if="value">::zobj.formatText(value, format='html')</x>''')
# pxView part for format String.XHTML.
pxViewRich = Px('''
<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,None)</script>''')
# PX displaying the language code and name besides the part of the
# multilingual field storing content in this language.
pxLanguage = Px('''
<td style=":'padding-top:%dpx' % lgTop" width="25px">
<span class="language help"
title=":ztool.getLanguageName(lg)">:lg.upper()</span>
</td>''')
pxMultilingual = Px('''
<!-- Horizontally-layouted multilingual field -->
<table if="mLayout == 'horizontal'" width="100%">
<tr valign="top"><x for="lg in field.languages"><x>:field.pxLanguage</x>
<td var="requestValue=requestValue[lg]|None;
value=value[lg]|emptyDefault">:field.subPx[layoutType][fmt]</td>
</x></tr></table>
<!-- Vertically-layouted multilingual field -->
<table if="mLayout == 'vertical'">
<tr valign="top" height="20px" for="lg in field.languages">
<x>:field.pxLanguage</x>
<td var="requestValue=requestValue[lg]|None;
value=value[lg]|emptyDefault">:field.subPx[layoutType][fmt]</td>
</tr></table>''')
pxView = Px(''' pxView = Px('''
<x var="fmt=field.format; isUrl=field.isUrl; <x var="fmt=field.format; isUrl=field.isUrl;
multilingual=field.isMultilingual();
mLayout=multilingual and field.getLanguagesLayout('view');
mayAjaxEdit=not showChanges and field.inlineEdit and \ mayAjaxEdit=not showChanges and field.inlineEdit and \
zobj.mayEdit(field.writePermission)"> zobj.mayEdit(field.writePermission)">
<x if="fmt in (0, 3)"> <x if="field.isSelect">
<ul if="value and isMultiple"> <span if="not value" class="smaller">-</span>
<li for="sv in value"><i>::sv</i></li> <x if="value and not isMultiple">::value</x>
</ul> <ul if="value and isMultiple"><li for="sv in value"><i>::sv</i></li></ul>
<x if="value and not isMultiple">
<!-- A password -->
<x if="fmt == 3">********</x>
<!-- A URL -->
<a if="(fmt != 3) and isUrl" target="_blank" href=":value">:value</a>
<!-- Any other value -->
<x if="(fmt != 3) and not isUrl">::value</x>
</x>
</x> </x>
<!-- Unformatted text --> <!-- Any other unilingual field -->
<x if="value and (fmt == 1)">::zobj.formatText(value, format='html')</x> <x if="not field.isSelect and not multilingual"
<!-- XHTML text --> var2="lg=None">:field.subPx['view'][fmt]</x>
<x if="fmt == 2"> <!-- Any other multilingual field -->
<div if="not mayAjaxEdit" class="xhtml">::value or '-'</div> <x if="not field.isSelect and multilingual"
<div if="mayAjaxEdit" class="xhtml" contenteditable="true" var2="lgTop=1; emptyDefault='-'">:field.pxMultilingual</x>
id=":'%s_%s_ck' % (zobj.id, name)">::value or '-'</div> <!-- If this field is a master field -->
<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" <input type="hidden" if="masterCss" class=":masterCss" value=":rawValue"
name=":name" id=":name"/> name=":name" id=":name"/></x>''')
</x>''')
# pxEdit part for formats String.LINE (but that are not selections), # pxEdit part for formats String.LINE (but that are not selections),
# String.PASSWORD and String.CAPTCHA. # String.PASSWORD and String.CAPTCHA.
pxEditText = Px(''' pxEditLine = Px('''
<input var="inputId=not lg and name or '%s_%s' % (name, lg); <input var="inputId=not lg and name or '%s_%s' % (name, lg);
placeholder=field.getAttribute(obj, 'placeholder') or ''" placeholder=field.getAttribute(obj, 'placeholder') or ''"
id=":inputId" name=":inputId" size=":field.width" id=":inputId" name=":inputId" size=":field.width"
@ -141,12 +188,10 @@ class String(Field):
<script if="fmt == 2" <script if="fmt == 2"
type="text/javascript">::field.getJsInit(zobj, lg)</script>''') 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(''' pxEdit = Px('''
<x var="fmt=field.format; <x var="fmt=field.format;
multilingual=field.isMultilingual()"> multilingual=field.isMultilingual();
mLayout=multilingual and field.getLanguagesLayout('edit')">
<select if="field.isSelect" <select if="field.isSelect"
var2="possibleValues=field.getPossibleValues(zobj, \ var2="possibleValues=field.getPossibleValues(zobj, \
withTranslations=True, withBlankValue=True)" withTranslations=True, withBlankValue=True)"
@ -158,22 +203,14 @@ class String(Field):
selected=":field.isSelected(zobj, name, val[0], rawValue)" selected=":field.isSelected(zobj, name, val[0], rawValue)"
title=":val[1]">:ztool.truncateValue(val[1],field.width)</option> title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
</select> </select>
<!-- Any other unilingual field. --> <!-- Any other unilingual field -->
<x if="not field.isSelect and not multilingual" <x if="not field.isSelect and not multilingual"
var2="lg=None">:field.editPx[fmt]</x> var2="lg=None">:field.subPx['edit'][fmt]</x>
<!-- Any other multilingual field. --> <!-- Any other multilingual field -->
<table if="not field.isSelect and multilingual" width="100%"> <x if="not field.isSelect and multilingual"
<tr valign="top"> var2="lgTop=(fmt!=2) and 3 or 1;
<x for="lg in field.languages"> emptyDefault=''">:field.pxMultilingual</x>
<td style=":(fmt==2) and 'padding-top:1px' or 'padding-top:3px'" </x>''')
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>
</tr>
</table></x>''')
pxCell = Px(''' pxCell = Px('''
<x var="multipleValues=value and isMultiple"> <x var="multipleValues=value and isMultiple">
@ -212,6 +249,14 @@ class String(Field):
</select> </select>
</x><br/>''') </x><br/>''')
# Sub-PX to use according to String format.
subPx = {
'edit': {LINE:pxEditLine, TEXT:pxEditTextArea, XHTML:pxEditTextArea,
PASSWORD:pxEditLine, CAPTCHA:pxEditLine},
'view': {LINE:pxViewLine, TEXT:pxViewText, XHTML:pxViewRich,
PASSWORD:pxViewLine, CAPTCHA:pxViewLine}
}
# Some predefined functions that may also be used as validators # Some predefined functions that may also be used as validators
@staticmethod @staticmethod
def _MODULO_97(obj, value, complement=False): def _MODULO_97(obj, value, complement=False):
@ -324,7 +369,8 @@ class String(Field):
label=None, sdefault='', scolspan=1, swidth=None, sheight=None, label=None, sdefault='', scolspan=1, swidth=None, sheight=None,
persist=True, transform='none', placeholder=None, persist=True, transform='none', placeholder=None,
styles=('p','h1','h2','h3','h4'), allowImageUpload=True, styles=('p','h1','h2','h3','h4'), allowImageUpload=True,
spellcheck=False, languages=('en',), inlineEdit=False): spellcheck=False, languages=('en',), languagesLayouts=None,
inlineEdit=False):
# According to format, the widget will be different: input field, # According to format, the widget will be different: input field,
# textarea, inline editor... Note that there can be only one String # textarea, inline editor... Note that there can be only one String
# field of format CAPTCHA by page, because the captcha challenge is # field of format CAPTCHA by page, because the captcha challenge is
@ -343,6 +389,11 @@ class String(Field):
# field content in all the supported languages. The field is also used # field content in all the supported languages. The field is also used
# by the CK spell checker. # by the CK spell checker.
self.languages = languages self.languages = languages
# When content exists in several languages, how to render them? Either
# horizontally (one below the other), or vertically (one besides the
# other). Specify here a dict whose keys are layouts ("edit", "view")
# and whose values are either "horizontal" or "vertical".
self.languagesLayouts = languagesLayouts
# When format in XHTML, can the field be inline-edited (ckeditor)? # When format in XHTML, can the field be inline-edited (ckeditor)?
self.inlineEdit = inlineEdit self.inlineEdit = inlineEdit
# The following field has a direct impact on the text entered by the # The following field has a direct impact on the text entered by the
@ -392,8 +443,13 @@ class String(Field):
def checkParameters(self): def checkParameters(self):
'''Ensures this String is correctly defined.''' '''Ensures this String is correctly defined.'''
if self.isSelect and self.isMultilingual(): error = None
raise Exception("A selection field can't be multilingual.") if self.isMultilingual():
if self.isSelect:
error = "A selection field can't be multilingual."
elif self.format in (String.PASSWORD, String.CAPTCHA):
error = "A password or captcha field can't be multilingual."
if error: raise Exception(error)
def isSelection(self): def isSelection(self):
'''Does the validator of this type definition define a list of values '''Does the validator of this type definition define a list of values
@ -428,6 +484,13 @@ class String(Field):
elif self.isMultiValued(): elif self.isMultiValued():
return {'view': 'l-f', 'edit': 'lrv-f'} return {'view': 'l-f', 'edit': 'lrv-f'}
def getLanguagesLayout(self, layoutType):
'''Gets the way to render a multilingual field on p_layoutType.'''
if self.languagesLayouts and (layoutType in self.languagesLayouts):
return self.languagesLayouts[layoutType]
# Else, return a default value that depends of the format.
return String.defaultLanguagesLayouts[self.format][layoutType]
def getValue(self, obj): 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() if self.name == 'state': return obj.State()
@ -545,17 +608,30 @@ class String(Field):
res[lg]=self.getUnilingualFormattedValue(obj,value[lg],showChanges) res[lg]=self.getUnilingualFormattedValue(obj,value[lg],showChanges)
return res return res
def extractText(self, value):
'''Extracts pure text from XHTML p_value.'''
return XhtmlTextExtractor(raiseOnError=False).parse('<p>%s</p>' % value)
emptyStringTuple = ('',) emptyStringTuple = ('',)
emptyValuesCatalogIgnored = (None, '') emptyValuesCatalogIgnored = (None, '')
def getIndexValue(self, obj, forSearch=False): def getIndexValue(self, obj, forSearch=False):
'''For indexing purposes, we return only strings, not unicodes.''' '''Pure text must be extracted from rich content; multilingual content
res = Field.getIndexValue(self, obj, forSearch) must be concatenated.'''
if isinstance(res, unicode): print 'INDEX value computing...', self.name, obj.title
res = res.encode('utf-8') isXhtml = self.format == String.XHTML
if res and forSearch and (self.format == String.XHTML): if self.isMultilingual():
# Convert the value to simple text. res = self.getValue(obj)
extractor = XhtmlTextExtractor(raiseOnError=False) if res:
res = extractor.parse('<p>%s</p>' % res) vals = []
for v in res.itervalues():
if isinstance(v, unicode): v = v.encode('utf-8')
if isXhtml: vals.append(self.extractText(v))
else: vals.append(v)
res = ' '.join(vals)
else:
res = Field.getIndexValue(self, obj, forSearch)
if res and isXhtml: res = self.extractText(res)
# Ugly catalog: if I give an empty tuple as index value, it keeps the # Ugly catalog: if I give an empty tuple as index value, it keeps the
# previous value. If I give him a tuple containing an empty string, it # previous value. If I give him a tuple containing an empty string, it
# is ok. # is ok.
@ -563,6 +639,7 @@ class String(Field):
# Ugly catalog: if value is an empty string or None, it keeps the # Ugly catalog: if value is an empty string or None, it keeps the
# previous index value. # previous index value.
if res in self.emptyValuesCatalogIgnored: res = ' ' if res in self.emptyValuesCatalogIgnored: res = ' '
print 'INDEX value for', self.name, 'is', res
return res return res
def getPossibleValues(self, obj, withTranslations=False, def getPossibleValues(self, obj, withTranslations=False,

View file

@ -65,7 +65,7 @@ input.button { color: #666666; height: 20px; margin-bottom: 5px;
input.buttonSmall { width: 100px !important; font-size: 85%; height: 18px; input.buttonSmall { width: 100px !important; font-size: 85%; height: 18px;
margin-bottom: 3px } margin-bottom: 3px }
.fake { background-color: #e6e6e6 !important ; cursor:help !important } .fake { background-color: #e6e6e6 !important ; cursor:help !important }
.xhtml { background-color: white; padding: 6px; font-size: 95% } .xhtml { background-color: white; padding: 4px; font-size: 95% }
.xhtml img { margin-right: 5px } .xhtml img { margin-right: 5px }
.xhtml p { margin: 3px 0 7px 0 } .xhtml p { margin: 3px 0 7px 0 }
.clickable { cursor: pointer } .clickable { cursor: pointer }
@ -168,4 +168,4 @@ td.search { padding-top: 8px }
.tab { padding: 0 10px 0 10px; text-align: center; font-size: 90%; .tab { padding: 0 10px 0 10px; text-align: center; font-size: 90%;
font-weight: bold} font-weight: bold}
.language {color: grey; font-size: 7pt; border: grey 1px solid; padding: 2px; .language {color: grey; font-size: 7pt; border: grey 1px solid; padding: 2px;
margin: 0 2px 0 4px} margin: 0 2px 0 4px; font-family: monospace }