1041 lines
49 KiB
Python
1041 lines
49 KiB
Python
# -*- coding: utf-8 -*-
|
|
# ------------------------------------------------------------------------------
|
|
# This file is part of Appy, a framework for building applications in the Python
|
|
# language. Copyright (C) 2007 Gaetan Delannay
|
|
|
|
# Appy is free software; you can redistribute it and/or modify it under the
|
|
# terms of the GNU General Public License as published by the Free Software
|
|
# Foundation; either version 3 of the License, or (at your option) any later
|
|
# version.
|
|
|
|
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
# ------------------------------------------------------------------------------
|
|
import re, random
|
|
from appy.gen.layout import Table
|
|
from appy.gen.indexer import XhtmlTextExtractor
|
|
from appy.fields import Field
|
|
from appy.px import Px
|
|
from appy.shared.data import countries
|
|
from appy.shared.xml_parser import XhtmlCleaner
|
|
from appy.shared.diff import HtmlDiff
|
|
from appy.shared import utils as sutils
|
|
|
|
# ------------------------------------------------------------------------------
|
|
digit = re.compile('[0-9]')
|
|
alpha = re.compile('[a-zA-Z0-9]')
|
|
letter = re.compile('[a-zA-Z]')
|
|
digits = '0123456789'
|
|
letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
# No "0" or "1" that could be interpreted as letters "O" or "l".
|
|
passwordDigits = '23456789'
|
|
# No letters i, l, o (nor lowercase nor uppercase) that could be misread.
|
|
passwordLetters = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'
|
|
emptyTuple = ()
|
|
|
|
# ------------------------------------------------------------------------------
|
|
class Selection:
|
|
'''Instances of this class may be given as validator of a String, in order
|
|
to tell Appy that the validator is a selection that will be computed
|
|
dynamically.'''
|
|
def __init__(self, methodName):
|
|
# The p_methodName parameter must be the name of a method that will be
|
|
# called every time Appy will need to get the list of possible values
|
|
# for the related field. It must correspond to an instance method of
|
|
# the class defining the related field. This method accepts no argument
|
|
# and must return a list (or tuple) of pairs (lists or tuples):
|
|
# (id, text), where "id" is one of the possible values for the
|
|
# field, and "text" is the value as will be shown on the screen.
|
|
# You can use self.translate within this method to produce an
|
|
# internationalized version of "text" if needed.
|
|
self.methodName = methodName
|
|
|
|
def getText(self, obj, value, field, language=None):
|
|
'''Gets the text that corresponds to p_value.'''
|
|
if language:
|
|
withTranslations = language
|
|
else:
|
|
withTranslations = True
|
|
vals = field.getPossibleValues(obj, ignoreMasterValues=True,\
|
|
withTranslations=withTranslations)
|
|
for v, text in vals:
|
|
if v == value: return text
|
|
return value
|
|
|
|
# ------------------------------------------------------------------------------
|
|
class String(Field):
|
|
# Javascript files sometimes required by this type
|
|
jsFiles = {'edit': ('ckeditor/ckeditor.js',),
|
|
'view': ('ckeditor/ckeditor.js',)}
|
|
|
|
# Some predefined regular expressions that may be used as validators
|
|
c = re.compile
|
|
EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \
|
|
'[a-zA-Z][a-zA-Z\.]*[a-zA-Z]')
|
|
ALPHANUMERIC = c('[\w-]+')
|
|
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
|
|
|
|
# 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>
|
|
<x if="mayAjaxEdit" var2="name=lg and ('%s_%s' % (name, lg)) or name">
|
|
<div class="xhtml" contenteditable="true"
|
|
id=":'%s_%s_ck' % (zobj.id, name)">::value or '-'</div>
|
|
<script if="mayAjaxEdit">::field.getJsInlineInit(zobj, name, lg)</script>
|
|
</x>''')
|
|
|
|
# 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%"
|
|
var="count=len(languages)">
|
|
<tr valign="top"><x for="lg in languages"><x>:field.pxLanguage</x>
|
|
<td width=":'%d%%' % int(100.0/count)"
|
|
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 languages">
|
|
<x>:field.pxLanguage</x>
|
|
<td var="requestValue=requestValue[lg]|None;
|
|
value=value[lg]|emptyDefault">:field.subPx[layoutType][fmt]</td>
|
|
</tr></table>''')
|
|
|
|
pxView = Px('''
|
|
<x var="fmt=field.format; isUrl=field.isUrl;
|
|
languages=field.getAttribute(obj, 'languages');
|
|
multilingual=len(languages) > 1;
|
|
mLayout=multilingual and field.getLanguagesLayout('view');
|
|
inlineEdit=field.getAttribute(obj, 'inlineEdit');
|
|
mayAjaxEdit=not showChanges and inlineEdit and \
|
|
(layoutType != 'cell') and \
|
|
zobj.mayEdit(field.writePermission)">
|
|
<x if="field.isSelect">
|
|
<span if="not value" class="smaller">-</span>
|
|
<x if="value and not isMultiple">::value</x>
|
|
<ul if="value and isMultiple"><li for="sv in value"><i>::sv</i></li></ul>
|
|
</x>
|
|
<!-- Any other unilingual field -->
|
|
<x if="not field.isSelect and not multilingual"
|
|
var2="lg=None">:field.subPx['view'][fmt]</x>
|
|
<!-- Any other multilingual field -->
|
|
<x if="not field.isSelect and multilingual"
|
|
var2="lgTop=1; emptyDefault='-'">:field.pxMultilingual</x>
|
|
<!-- If this field is a master field -->
|
|
<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.
|
|
pxEditLine = 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"
|
|
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>''')
|
|
|
|
pxEdit = Px('''
|
|
<x var="fmt=field.format;
|
|
languages=field.getAttribute(zobj, 'languages');
|
|
multilingual=len(languages) > 1;
|
|
mLayout=multilingual and field.getLanguagesLayout('edit')">
|
|
<select if="field.isSelect"
|
|
var2="possibleValues=field.getPossibleValues(zobj, \
|
|
withTranslations=True, withBlankValue=True)"
|
|
name=":name" id=":name" class=":masterCss"
|
|
multiple=":isMultiple and 'multiple' or ''"
|
|
onchange=":field.getOnChange(zobj, layoutType)"
|
|
size=":isMultiple and field.height or 1">
|
|
<option for="val in possibleValues" value=":val[0]"
|
|
selected=":field.isSelected(zobj, name, val[0], rawValue)"
|
|
title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
|
|
</select>
|
|
<!-- Any other unilingual field -->
|
|
<x if="not field.isSelect and not multilingual"
|
|
var2="lg=None">:field.subPx['edit'][fmt]</x>
|
|
<!-- Any other multilingual field -->
|
|
<x if="not field.isSelect and multilingual"
|
|
var2="lgTop=(fmt!=2) and 3 or 1;
|
|
emptyDefault=''">:field.pxMultilingual</x>
|
|
</x>''')
|
|
|
|
pxCell = Px('''
|
|
<x var="multipleValues=value and isMultiple">
|
|
<x if="multipleValues">:', '.join(value)</x>
|
|
<x if="not multipleValues">:field.pxView</x>
|
|
</x>''')
|
|
|
|
pxSearch = Px('''
|
|
<!-- Show a simple search field for most String fields -->
|
|
<input if="not field.isSelect" type="text" maxlength=":field.maxChars"
|
|
size=":field.swidth" value=":field.sdefault"
|
|
name=":'%s*string-%s' % (widgetName, field.transform)"
|
|
style=":'text-transform:%s' % field.transform"/>
|
|
<!-- Show a multi-selection box for fields whose validator defines a list
|
|
of values, with a "AND/OR" checkbox. -->
|
|
<x if="field.isSelect">
|
|
<!-- The "and" / "or" radio buttons -->
|
|
<x if="field.multiplicity[1] != 1"
|
|
var2="operName='o_%s' % name;
|
|
orName='%s_or' % operName;
|
|
andName='%s_and' % operName">
|
|
<input type="radio" name=":operName" id=":orName" checked="checked"
|
|
value="or"/>
|
|
<label lfor=":orName">:_('search_or')</label>
|
|
<input type="radio" name=":operName" id=":andName" value="and"/>
|
|
<label lfor=":andName">:_('search_and')</label><br/>
|
|
</x>
|
|
<!-- The list of values -->
|
|
<select var="preSelected=field.sdefault"
|
|
name=":widgetName" size=":field.sheight" multiple="multiple"
|
|
onchange=":field.getOnChange(ztool, 'search', className)">
|
|
<option for="v in field.getPossibleValues(ztool, withTranslations=True,\
|
|
withBlankValue=False, className=className)"
|
|
selected=":v[0] in preSelected" value=":v[0]"
|
|
title=":v[1]">:ztool.truncateValue(v[1], field.swidth)</option>
|
|
</select>
|
|
</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}
|
|
}
|
|
subPx['cell'] = subPx['view']
|
|
|
|
# Some predefined functions that may also be used as validators
|
|
@staticmethod
|
|
def _MODULO_97(obj, value, complement=False):
|
|
'''p_value must be a string representing a number, like a bank account.
|
|
this function checks that the 2 last digits are the result of
|
|
computing the modulo 97 of the previous digits. Any non-digit
|
|
character is ignored. If p_complement is True, it does compute the
|
|
complement of modulo 97 instead of modulo 97. p_obj is not used;
|
|
it will be given by the Appy validation machinery, so it must be
|
|
specified as parameter. The function returns True if the check is
|
|
successful.'''
|
|
if not value: return True
|
|
# First, remove any non-digit char
|
|
v = ''
|
|
for c in value:
|
|
if digit.match(c): v += c
|
|
# There must be at least 3 digits for performing the check
|
|
if len(v) < 3: return False
|
|
# Separate the real number from the check digits
|
|
number = int(v[:-2])
|
|
checkNumber = int(v[-2:])
|
|
# Perform the check
|
|
if complement:
|
|
return (97 - (number % 97)) == checkNumber
|
|
else:
|
|
# The check number can't be 0. In this case, we force it to be 97.
|
|
# This is the way Belgian bank account numbers work. I hope this
|
|
# behaviour is general enough to be implemented here.
|
|
mod97 = (number % 97)
|
|
if mod97 == 0: return checkNumber == 97
|
|
else: return checkNumber == mod97
|
|
@staticmethod
|
|
def MODULO_97(obj, value): return String._MODULO_97(obj, value)
|
|
@staticmethod
|
|
def MODULO_97_COMPLEMENT(obj, value):
|
|
return String._MODULO_97(obj, value, True)
|
|
BELGIAN_ENTERPRISE_NUMBER = MODULO_97_COMPLEMENT
|
|
|
|
@staticmethod
|
|
def BELGIAN_NISS(obj, value):
|
|
'''Returns True if the NISS in p_value is valid.'''
|
|
if not value: return True
|
|
# Remove any non-digit from nrn
|
|
niss = sutils.keepDigits(value)
|
|
# NISS must be made of 11 numbers
|
|
if len(niss) != 11: return False
|
|
# When NRN begins with 0 or 1, it must be prefixed with number "2" for
|
|
# checking the modulo 97 complement.
|
|
nissForModulo = niss
|
|
if niss.startswith('0') or niss.startswith('1'):
|
|
nissForModulo = '2'+niss
|
|
# Check modulo 97 complement
|
|
return String.MODULO_97_COMPLEMENT(None, nissForModulo)
|
|
|
|
@staticmethod
|
|
def IBAN(obj, value):
|
|
'''Checks that p_value corresponds to a valid IBAN number. IBAN stands
|
|
for International Bank Account Number (ISO 13616). If the number is
|
|
valid, the method returns True.'''
|
|
if not value: return True
|
|
# First, remove any non-digit or non-letter char
|
|
v = ''
|
|
for c in value:
|
|
if alpha.match(c): v += c
|
|
# Maximum size is 34 chars
|
|
if (len(v) < 8) or (len(v) > 34): return False
|
|
# 2 first chars must be a valid country code
|
|
if not countries.exists(v[:2].upper()): return False
|
|
# 2 next chars are a control code whose value must be between 0 and 96.
|
|
try:
|
|
code = int(v[2:4])
|
|
if (code < 0) or (code > 96): return False
|
|
except ValueError:
|
|
return False
|
|
# Perform the checksum
|
|
vv = v[4:] + v[:4] # Put the 4 first chars at the end.
|
|
nv = ''
|
|
for c in vv:
|
|
# Convert each letter into a number (A=10, B=11, etc)
|
|
# Ascii code for a is 65, so A=10 if we perform "minus 55"
|
|
if letter.match(c): nv += str(ord(c.upper()) - 55)
|
|
else: nv += c
|
|
return int(nv) % 97 == 1
|
|
|
|
@staticmethod
|
|
def BIC(obj, value):
|
|
'''Checks that p_value corresponds to a valid BIC number. BIC stands
|
|
for Bank Identifier Code (ISO 9362). If the number is valid, the
|
|
method returns True.'''
|
|
if not value: return True
|
|
# BIC number must be 8 or 11 chars
|
|
if len(value) not in (8, 11): return False
|
|
# 4 first chars, representing bank name, must be letters
|
|
for c in value[:4]:
|
|
if not letter.match(c): return False
|
|
# 2 next chars must be a valid country code
|
|
if not countries.exists(value[4:6].upper()): return False
|
|
# Last chars represent some location within a country (a city, a
|
|
# province...). They can only be letters or figures.
|
|
for c in value[6:]:
|
|
if not alpha.match(c): return False
|
|
return True
|
|
|
|
def __init__(self, validator=None, multiplicity=(0,1), default=None,
|
|
format=LINE, show=True, page='main', group=None, layouts=None,
|
|
move=0, indexed=False, mustIndex=True, searchable=False,
|
|
specificReadPermission=False, specificWritePermission=False,
|
|
width=None, height=None, maxChars=None, colspan=1, master=None,
|
|
masterValue=None, focus=False, historized=False, mapping=None,
|
|
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, languages=('en',), languagesLayouts=None,
|
|
inlineEdit=False, view=None, xml=None):
|
|
# 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
|
|
# stored in the session at some global key.
|
|
self.format = format
|
|
self.isUrl = validator == String.URL
|
|
# When format is XHTML, the list of styles that the user will be able to
|
|
# select in the styles dropdown is defined hereafter.
|
|
self.styles = styles
|
|
# When format is XHTML, do we allow the user to upload images in it ?
|
|
self.allowImageUpload = allowImageUpload
|
|
# When format is XHTML, do we run the CK spellchecker ?
|
|
self.spellcheck = spellcheck
|
|
# 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 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)? A
|
|
# method can be specified.
|
|
self.inlineEdit = inlineEdit
|
|
# The following field has a direct impact on the text entered by the
|
|
# user. It applies a transformation on it, exactly as does the CSS
|
|
# "text-transform" property. Allowed values are those allowed for the
|
|
# CSS property: "none" (default), "uppercase", "capitalize" or
|
|
# "lowercase".
|
|
self.transform = transform
|
|
# "placeholder", similar to the HTML attribute of the same name, allows
|
|
# to specify a short hint that describes the expected value of the input
|
|
# field. It is shown inside the input field and disappears as soon as
|
|
# the user encodes something in it. Works only for strings whose format
|
|
# is LINE. Does not work with IE < 10. You can specify a method here,
|
|
# that can, for example, return an internationalized value.
|
|
self.placeholder = placeholder
|
|
Field.__init__(self, validator, multiplicity, default, show, page,
|
|
group, layouts, move, indexed, mustIndex, searchable,
|
|
specificReadPermission, specificWritePermission, width,
|
|
height, maxChars, colspan, master, masterValue, focus,
|
|
historized, mapping, label, sdefault, scolspan, swidth,
|
|
sheight, persist, view, xml)
|
|
self.isSelect = self.isSelection()
|
|
# If self.isSelect, self.sdefault must be a list of value(s).
|
|
if self.isSelect and not sdefault:
|
|
self.sdefault = []
|
|
# Default width, height and maxChars vary according to String format
|
|
if width == None:
|
|
if format == String.TEXT: self.width = 60
|
|
# This width corresponds to the standard width of an Appy page.
|
|
if format == String.XHTML: self.width = None
|
|
else: self.width = 30
|
|
if height == None:
|
|
if format == String.TEXT: self.height = 5
|
|
elif self.isSelect: self.height = 4
|
|
else: self.height = 1
|
|
if maxChars == None:
|
|
if self.isSelect: pass
|
|
elif format == String.LINE: self.maxChars = 256
|
|
elif format == String.TEXT: self.maxChars = 9999
|
|
elif format == String.XHTML: self.maxChars = 99999
|
|
elif format == String.PASSWORD: self.maxChars = 20
|
|
self.filterable = self.indexed and not self.isSelect and \
|
|
(self.format in (String.LINE, String.TEXT))
|
|
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.'''
|
|
error = None
|
|
if self.isMultilingual(None):
|
|
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):
|
|
'''Does the validator of this type definition define a list of values
|
|
into which the user must select one or more values?'''
|
|
res = True
|
|
if type(self.validator) in (list, tuple):
|
|
for elem in self.validator:
|
|
if not isinstance(elem, basestring):
|
|
res = False
|
|
break
|
|
else:
|
|
if not isinstance(self.validator, Selection):
|
|
res = False
|
|
return res
|
|
|
|
def isMultilingual(self, obj, dontKnow=False):
|
|
'''Is this field multilingual ? If we don't know, say p_dontKnow.'''
|
|
# In the following case, impossible to know: we say no.
|
|
if not obj:
|
|
if callable(self.languages): return dontKnow
|
|
else: return len(self.languages) > 1
|
|
return len(self.getAttribute(obj, 'languages')) > 1
|
|
|
|
def getDefaultLayouts(self):
|
|
'''Returns the default layouts for this type. Default layouts can vary
|
|
acccording to format, multiplicity or history.'''
|
|
if self.format == String.TEXT:
|
|
return {'view': 'l-f', 'edit': 'lrv-d-f'}
|
|
elif self.format == String.XHTML:
|
|
if self.historized:
|
|
# self.historized can be a method or a boolean. If it is a
|
|
# method, it means that under some condition, historization will
|
|
# be enabled. So we come here also in this case.
|
|
view = 'lc-f'
|
|
else:
|
|
view = 'l-f'
|
|
return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'}
|
|
elif self.isMultiValued():
|
|
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):
|
|
# Cheat if this field represents p_obj's state.
|
|
if self.name == 'state': return obj.State()
|
|
value = Field.getValue(self, obj)
|
|
if not value:
|
|
if self.isMultiValued(): return emptyTuple
|
|
else: return value
|
|
if isinstance(value, basestring) and self.isMultiValued():
|
|
value = [value]
|
|
elif isinstance(value, tuple):
|
|
value = list(value)
|
|
return value
|
|
|
|
def getCopyValue(self, obj):
|
|
'''If the value is mutable (ie, a dict for a multilingual field), return
|
|
a copy of it instead of the value stored in the database.'''
|
|
res = self.getValue(obj)
|
|
if isinstance(res, dict): res = res.copy()
|
|
return res
|
|
|
|
def valueIsInRequest(self, obj, request, name, layoutType):
|
|
# If we are on the search layout, p_obj, if not None, is certainly not
|
|
# the p_obj we want here (can be a home object).
|
|
if layoutType == 'search':
|
|
return Field.valueIsInRequest(self, obj, request, name, layoutType)
|
|
languages = self.getAttribute(obj, 'languages')
|
|
if len(languages) == 1:
|
|
return Field.valueIsInRequest(self, obj, request, name, layoutType)
|
|
# Is is sufficient to check that at least one of the language-specific
|
|
# values is in the request.
|
|
return request.has_key('%s_%s' % (name, languages[0]))
|
|
|
|
def getRequestValue(self, obj, requestName=None):
|
|
'''The request value may be multilingual.'''
|
|
request = obj.REQUEST
|
|
name = requestName or self.name
|
|
languages = self.getAttribute(obj, 'languages')
|
|
# A unilingual field.
|
|
if len(languages) == 1: return request.get(name, None)
|
|
# A multilingual field.
|
|
res = {}
|
|
for language in languages:
|
|
res[language] = request.get('%s_%s' % (name, language), None)
|
|
return res
|
|
|
|
def isEmptyValue(self, obj, value):
|
|
'''Returns True if the p_value must be considered as an empty value.'''
|
|
if not self.isMultilingual(obj):
|
|
return Field.isEmptyValue(self, obj, value)
|
|
# 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, obj, v): return
|
|
return True
|
|
|
|
def isCompleteValue(self, obj, value):
|
|
'''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(obj):
|
|
return Field.isCompleteValue(self, obj, value)
|
|
# 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, obj, v): return
|
|
return True
|
|
|
|
def getDiffValue(self, obj, value, language):
|
|
'''Returns a version of p_value that includes the cumulative diffs
|
|
between successive versions. If the field is non-multilingual, it
|
|
must be called with p_language being None. Else, p_language
|
|
identifies the language-specific part we will work on.'''
|
|
res = None
|
|
lastEvent = None
|
|
name = language and ('%s-%s' % (self.name, language)) or self.name
|
|
for event in obj.workflow_history['appy']:
|
|
if event['action'] != '_datachange_': continue
|
|
if name not in event['changes']: continue
|
|
if res == None:
|
|
# We have found the first version of the field
|
|
res = event['changes'][name][0] or ''
|
|
else:
|
|
# We need to produce the difference between current result and
|
|
# this version.
|
|
iMsg, dMsg = obj.getHistoryTexts(lastEvent)
|
|
thisVersion = event['changes'][name][0] or ''
|
|
comparator = HtmlDiff(res, thisVersion, iMsg, dMsg)
|
|
res = comparator.get()
|
|
lastEvent = event
|
|
if not lastEvent:
|
|
# There is no diff to show for this p_language.
|
|
return value
|
|
# Now we need to compare the result with the current version.
|
|
iMsg, dMsg = obj.getHistoryTexts(lastEvent)
|
|
comparator = HtmlDiff(res, value or '', iMsg, dMsg)
|
|
return comparator.get()
|
|
|
|
def getUnilingualFormattedValue(self, obj, value, showChanges=False,
|
|
userLanguage=None, language=None):
|
|
'''If no p_language is specified, this method is called by
|
|
m_getFormattedValue for getting a non-multilingual value (ie, in
|
|
most cases). Else, this method returns a formatted value for the
|
|
p_language-specific part of a multilingual value.'''
|
|
if Field.isEmptyValue(self, obj, value) and not showChanges: return ''
|
|
res = value
|
|
if self.isSelect:
|
|
if isinstance(self.validator, Selection):
|
|
# Value(s) come from a dynamic vocabulary
|
|
val = self.validator
|
|
if self.isMultiValued():
|
|
return [val.getText(obj, v, self, language=userLanguage) \
|
|
for v in value]
|
|
else:
|
|
return val.getText(obj, value, self, language=userLanguage)
|
|
else:
|
|
# Value(s) come from a fixed vocabulary whose texts are in
|
|
# i18n files.
|
|
_ = obj.translate
|
|
if self.isMultiValued():
|
|
res = [_('%s_list_%s' % (self.labelId, v), \
|
|
language=userLanguage) for v in value]
|
|
else:
|
|
res = _('%s_list_%s' % (self.labelId, value), \
|
|
language=userLanguage)
|
|
elif (self.format == String.XHTML) and showChanges:
|
|
# Compute the successive changes that occurred on p_value.
|
|
res = self.getDiffValue(obj, res, language)
|
|
# If value starts with a carriage return, add a space; else, it will
|
|
# be ignored.
|
|
if isinstance(res, basestring) and \
|
|
(res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res
|
|
return res
|
|
|
|
def getFormattedValue(self, obj, value, showChanges=False, language=None):
|
|
'''Be careful: p_language represents the UI language, while "languages"
|
|
below represents the content language(s) of this field. p_language
|
|
can be used, ie, to translate a Selection value.'''
|
|
languages = self.getAttribute(obj, 'languages')
|
|
if len(languages) == 1:
|
|
return self.getUnilingualFormattedValue(obj, value, showChanges,
|
|
userLanguage=language)
|
|
# Return the dict of values whose individual, language-specific values
|
|
# have been formatted via m_getUnilingualFormattedValue.
|
|
if not value and not showChanges: return value
|
|
res = {}
|
|
for lg in languages:
|
|
if not value: val = ''
|
|
else: val = value[lg]
|
|
res[lg] = self.getUnilingualFormattedValue(obj, val, showChanges,
|
|
language=lg)
|
|
return res
|
|
|
|
def getShownValue(self, obj, value, showChanges=False, language=None):
|
|
'''Be careful: p_language represents the UI language, while "languages"
|
|
below represents the content language(s) of this field. For a
|
|
multilingual field, this method only shows one specific language
|
|
part.'''
|
|
languages = self.getAttribute(obj, 'languages')
|
|
if len(languages) == 1:
|
|
return self.getUnilingualFormattedValue(obj, value, showChanges,
|
|
userLanguage=language)
|
|
if not value: return value
|
|
# Try to propose the part that is in the user language, or the part of
|
|
# the first content language else.
|
|
lg = obj.getUserLanguage()
|
|
if lg not in value: lg = languages[0]
|
|
return self.getUnilingualFormattedValue(obj, value[lg], showChanges,
|
|
language=lg)
|
|
|
|
def extractText(self, value):
|
|
'''Extracts pure text from XHTML p_value.'''
|
|
return XhtmlTextExtractor(raiseOnError=False).parse('<p>%s</p>' % value)
|
|
|
|
def getValidCatalogValue(self, value, forSearch):
|
|
'''p_value is the new value we want to index in the catalog, for this
|
|
field, for some object. p_value as is may not be an acceptable value
|
|
for the catalog: if it represents some empty value, like an empty
|
|
string, None or an empty tuple, instead of using it, the catalog will
|
|
keep the previously catalogued value! For those cases, this method
|
|
produces "empty" values that will really overwrite previous ones.'''
|
|
# 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
|
|
# is ok.
|
|
if isinstance(value, tuple) and not value:
|
|
value = forSearch and ' ' or ('',)
|
|
# Ugly catalog: if value is an empty string or None, it keeps the
|
|
# previous index value.
|
|
elif value in (None, ''): return ' '
|
|
return value
|
|
|
|
def getIndexValue(self, obj, forSearch=False):
|
|
'''Pure text must be extracted from rich content; multilingual content
|
|
must be concatenated.'''
|
|
# Must we produce an index value?
|
|
if not self.getAttribute(obj, 'mustIndex'):
|
|
return self.getValidCatalogValue(None, forSearch)
|
|
isXhtml = self.format == String.XHTML
|
|
if self.isMultilingual(obj):
|
|
res = self.getValue(obj)
|
|
if 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)
|
|
return self.getValidCatalogValue(res, forSearch)
|
|
|
|
def getPossibleValues(self, obj, withTranslations=False,
|
|
withBlankValue=False, className=None,
|
|
ignoreMasterValues=False):
|
|
'''Returns the list of possible values for this field (only for fields
|
|
with self.isSelect=True). If p_withTranslations is True, instead of
|
|
returning a list of string values, the result is a list of tuples
|
|
(s_value, s_translation). Moreover, p_withTranslations can hold a
|
|
given language: in this case, this language is used instead of the
|
|
user language. If p_withBlankValue is True, a blank value is
|
|
prepended to the list, excepted if the type is multivalued. If
|
|
p_className is given, p_obj is the tool and, if we need an instance
|
|
of p_className, we will need to use obj.executeQuery to find one.'''
|
|
if not self.isSelect: raise Exception('This field is not a selection.')
|
|
# Get the user language for translations, from "withTranslations".
|
|
lg = isinstance(withTranslations, str) and withTranslations or None
|
|
req = obj.REQUEST
|
|
if ('masterValues' in req) and not ignoreMasterValues:
|
|
# Get possible values from self.masterValue
|
|
masterValues = req['masterValues']
|
|
if '*' in masterValues: masterValues = masterValues.split('*')
|
|
values = self.masterValue(obj.appy(), masterValues)
|
|
if not withTranslations: res = values
|
|
else:
|
|
res = []
|
|
for v in values:
|
|
res.append( (v, self.getFormattedValue(obj,v,language=lg)) )
|
|
else:
|
|
# If this field is an ajax-updatable slave, no need to compute
|
|
# possible values: it will be overridden by method self.masterValue
|
|
# by a subsequent ajax request (=the "if" statement above).
|
|
if self.masterValue and callable(self.masterValue) and \
|
|
not ignoreMasterValues: return []
|
|
if isinstance(self.validator, Selection):
|
|
# We need to call self.methodName for getting the (dynamic)
|
|
# values. If methodName begins with _appy_, it is a special Appy
|
|
# method: we will call it on the Mixin (=p_obj) directly. Else,
|
|
# it is a user method: we will call it on the wrapper
|
|
# (p_obj.appy()). Some args can be hidden into p_methodName,
|
|
# separated with stars, like in this example: method1*arg1*arg2.
|
|
# Only string params are supported.
|
|
methodName = self.validator.methodName
|
|
# Unwrap parameters if any.
|
|
if methodName.find('*') != -1:
|
|
elems = methodName.split('*')
|
|
methodName = elems[0]
|
|
args = elems[1:]
|
|
else:
|
|
args = ()
|
|
# On what object must we call the method that will produce the
|
|
# values?
|
|
if methodName.startswith('tool:'):
|
|
obj = obj.getTool()
|
|
methodName = methodName[5:]
|
|
else:
|
|
# We must call on p_obj. But if we have something in
|
|
# p_className, p_obj is the tool and not an instance of
|
|
# p_className as required. So find such an instance.
|
|
if className:
|
|
brains = obj.executeQuery(className, maxResults=1,
|
|
brainsOnly=True)
|
|
if brains:
|
|
obj = brains[0].getObject()
|
|
# Do we need to call the method on the object or on the wrapper?
|
|
if methodName.startswith('_appy_'):
|
|
exec 'res = obj.%s(*args)' % methodName
|
|
else:
|
|
exec 'res = obj.appy().%s(*args)' % methodName
|
|
if not withTranslations: res = [v[0] for v in res]
|
|
elif isinstance(res, list): res = res[:]
|
|
else:
|
|
# The list of (static) values is directly given in
|
|
# self.validator.
|
|
res = []
|
|
for value in self.validator:
|
|
label = '%s_list_%s' % (self.labelId, value)
|
|
if withTranslations:
|
|
res.append( (value, obj.translate(label, language=lg)) )
|
|
else:
|
|
res.append(value)
|
|
if withBlankValue and not self.isMultiValued():
|
|
# Create the blank value to insert at the beginning of the list
|
|
if withTranslations:
|
|
blankValue = ('', obj.translate('choose_a_value', language=lg))
|
|
else:
|
|
blankValue = ''
|
|
# Insert the blank value in the result
|
|
if isinstance(res, tuple):
|
|
res = (blankValue,) + res
|
|
else:
|
|
res.insert(0, blankValue)
|
|
return res
|
|
|
|
def validateValue(self, obj, value):
|
|
if self.format == String.CAPTCHA:
|
|
challenge = obj.REQUEST.SESSION.get('captcha', None)
|
|
# Compute the challenge minus the char to remove
|
|
i = challenge['number']-1
|
|
text = challenge['text'][:i] + challenge['text'][i+1:]
|
|
if value != text:
|
|
return obj.translate('bad_captcha')
|
|
elif self.isSelect:
|
|
# Check that the value is among possible values
|
|
possibleValues = self.getPossibleValues(obj,ignoreMasterValues=True)
|
|
if isinstance(value, basestring):
|
|
error = value not in possibleValues
|
|
else:
|
|
error = False
|
|
for v in value:
|
|
if v not in possibleValues:
|
|
error = True
|
|
break
|
|
if error: return obj.translate('bad_select_value')
|
|
|
|
def applyTransform(self, value):
|
|
'''Applies a transform as required by self.transform on single
|
|
value p_value.'''
|
|
if self.transform in ('uppercase', 'lowercase'):
|
|
# For those transforms, I will remove any accent, because, most of
|
|
# the time, if the user wants to apply such effect, it is for ease
|
|
# of data manipulation, so I guess without accent.
|
|
value = sutils.normalizeString(value, usage='noAccents')
|
|
# Apply the transform
|
|
if self.transform == 'lowercase': return value.lower()
|
|
elif self.transform == 'uppercase': return value.upper()
|
|
elif self.transform == 'capitalize': return value.capitalize()
|
|
return value
|
|
|
|
def getUnilingualStorableValue(self, obj, value):
|
|
isString = isinstance(value, basestring)
|
|
isEmpty = Field.isEmptyValue(self, obj, value)
|
|
# Apply transform if required
|
|
if isString and not isEmpty and (self.transform != 'none'):
|
|
value = self.applyTransform(value)
|
|
# Clean XHTML strings
|
|
if not isEmpty and (self.format == String.XHTML):
|
|
# 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
|
|
# Clean TEXT strings
|
|
if not isEmpty and (self.format == String.TEXT):
|
|
value = value.replace('\r', '')
|
|
# Truncate the result if longer than self.maxChars
|
|
if isString and self.maxChars and (len(value) > self.maxChars):
|
|
value = value[:self.maxChars]
|
|
# Get a multivalued value if required.
|
|
if value and self.isMultiValued() and \
|
|
(type(value) not in sutils.sequenceTypes):
|
|
value = [value]
|
|
return value
|
|
|
|
def getStorableValue(self, obj, value):
|
|
languages = self.getAttribute(obj, 'languages')
|
|
if len(languages) == 1:
|
|
return self.getUnilingualStorableValue(obj, 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 languages:
|
|
value[lg] = self.getUnilingualStorableValue(obj, value[lg])
|
|
return value
|
|
|
|
def store(self, obj, value):
|
|
'''Stores p_value on p_obj for this field.'''
|
|
languages = self.getAttribute(obj, 'languages')
|
|
if (len(languages) > 1) and value and \
|
|
(not isinstance(value, dict) or (len(value) != len(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': return
|
|
requestValue = rq['fieldContent']
|
|
# Remember previous value if the field is historized
|
|
previousData = obj.rememberPreviousData([self])
|
|
if self.isMultilingual(obj):
|
|
# We take a copy of previousData because it is mutable (dict)
|
|
prevData = previousData[self.name]
|
|
if prevData != None: prevData = prevData.copy()
|
|
previousData[self.name] = prevData
|
|
# We get a partial value, for one language only
|
|
language = rq['languageOnly']
|
|
v = self.getUnilingualStorableValue(obj, requestValue)
|
|
getattr(obj.aq_base, self.name)[language] = v
|
|
part = ' (%s)' % language
|
|
else:
|
|
self.store(obj, self.getStorableValue(obj, requestValue))
|
|
part = ''
|
|
# Update the object history when relevant
|
|
if previousData: obj.historizeData(previousData)
|
|
# Update obj's last modification date
|
|
from DateTime import DateTime
|
|
obj.modified = DateTime()
|
|
obj.reindex()
|
|
obj.log('ajax-edited %s%s on %s.' % (self.name, part, obj.id))
|
|
|
|
def getIndexType(self):
|
|
'''Index type varies depending on String parameters.'''
|
|
# If String.isSelect, be it multivalued or not, we define a ListIndex:
|
|
# this way we can use AND/OR operator.
|
|
if self.isSelect:
|
|
return 'ListIndex'
|
|
elif self.format == String.TEXT:
|
|
return 'TextIndex'
|
|
elif self.format == String.XHTML:
|
|
return 'XhtmlIndex'
|
|
return Field.getIndexType(self)
|
|
|
|
def getJs(self, layoutType, res):
|
|
if self.format == String.XHTML: Field.getJs(self, layoutType, res)
|
|
|
|
def getCaptchaChallenge(self, session):
|
|
'''Returns a Captcha challenge in the form of a dict. At key "text",
|
|
value is a string that the user will be required to re-type, but
|
|
without 1 character whose position is at key "number". The challenge
|
|
is stored in the p_session, for the server-side subsequent check.'''
|
|
length = random.randint(5, 9) # The length of the challenge to encode
|
|
number = random.randint(1, length) # The position of the char to remove
|
|
text = '' # The challenge the user needs to type (minus one char)
|
|
for i in range(length):
|
|
j = random.randint(0, 1)
|
|
chars = (j == 0) and passwordDigits or passwordLetters
|
|
# Choose a char
|
|
text += chars[random.randint(0,len(chars)-1)]
|
|
res = {'text': text, 'number': number}
|
|
session['captcha'] = res
|
|
return res
|
|
|
|
def generatePassword(self):
|
|
'''Generates a password (we recycle here the captcha challenge
|
|
generator).'''
|
|
return self.getCaptchaChallenge({})['text']
|
|
|
|
ckLanguages = {'en': 'en_US', 'pt': 'pt_BR', 'da': 'da_DK', 'nl': 'nl_NL',
|
|
'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, obj, 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.getAttribute(obj, 'languages')[0]
|
|
if language in self.ckLanguages: return self.ckLanguages[language]
|
|
return 'en_US'
|
|
|
|
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(obj, language)}
|
|
if self.width: ckAttrs['width'] = self.width
|
|
if self.spellcheck: ckAttrs['scayt_autoStartup'] = True
|
|
if self.allowImageUpload:
|
|
ckAttrs['filebrowserUploadUrl'] = '%s/upload' % obj.absolute_url()
|
|
ck = []
|
|
for k, v in ckAttrs.iteritems():
|
|
if isinstance(v, int): sv = str(v)
|
|
if isinstance(v, bool): sv = str(v).lower()
|
|
else: sv = '"%s"' % v
|
|
ck.append('%s: %s' % (k, sv))
|
|
return ', '.join(ck)
|
|
|
|
def getJsInit(self, obj, language):
|
|
'''Gets the Javascript init code for displaying a rich editor for this
|
|
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})' % \
|
|
(name, self.getCkParams(obj, language))
|
|
|
|
def getJsInlineInit(self, obj, name, language):
|
|
'''Gets the Javascript init code for enabling inline edition of this
|
|
field (rich text only). If the field is multilingual, the current
|
|
p_language is given and p_name includes it. Else, p_language is
|
|
None.'''
|
|
uid = obj.id
|
|
fieldName = language and name.rsplit('_',1)[0] or name
|
|
lg = language or ''
|
|
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,'%s')}}})" % \
|
|
(uid, name, self.getCkParams(obj, language), uid, fieldName,
|
|
obj.absolute_url(), lg)
|
|
|
|
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
|
|
is given and used instead of field.name because it may be a a fake
|
|
name containing a row number from a field within a list field.'''
|
|
rq = obj.REQUEST
|
|
# Get the value we must compare (from request or from database)
|
|
if rq.has_key(fieldName):
|
|
compValue = rq.get(fieldName)
|
|
else:
|
|
compValue = dbValue
|
|
# Compare the value
|
|
if type(compValue) in sutils.sequenceTypes:
|
|
return vocabValue in compValue
|
|
return vocabValue == compValue
|
|
# ------------------------------------------------------------------------------
|