2013-07-08 16:39:16 -05:00
|
|
|
# -*- 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
|
2013-07-10 02:56:35 -05:00
|
|
|
from appy.px import Px
|
2013-07-08 16:39:16 -05:00
|
|
|
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'
|
|
|
|
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, appyType):
|
|
|
|
'''Gets the text that corresponds to p_value.'''
|
|
|
|
for v, text in appyType.getPossibleValues(obj, withTranslations=True):
|
|
|
|
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})?\/.*)?')
|
|
|
|
|
2013-07-10 02:56:35 -05:00
|
|
|
pxView = Px('''
|
|
|
|
<x var="fmt=field.format; isUrl=field.isUrl;
|
|
|
|
mayAjaxEdit=not showChanges and field.inlineEdit and \
|
|
|
|
contextObj.mayEdit(field.writePermission)">
|
|
|
|
<x if="fmt in (0, 3)">
|
|
|
|
<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>
|
|
|
|
<!-- Unformatted text -->
|
|
|
|
<x if="value and (fmt == 1)">::contextObj.formatText(value, format='html')
|
|
|
|
</x>
|
|
|
|
<!-- XHTML text -->
|
|
|
|
<x if="value and (fmt == 2)">
|
|
|
|
<div if="not mayAjaxEdit" class="xhtml">::value</div>
|
|
|
|
<div if="mayAjaxEdit" class="xhtml" contenteditable="true"
|
|
|
|
id=":'%s_%s_ck' % (contextObj.UID(), name)">::value</div>
|
|
|
|
<script if="mayAjaxEdit">:field.getJsInlineInit(contextObj)"></script>
|
|
|
|
</x>
|
|
|
|
<input type="hidden" if="masterCss" class=":masterCss" value=":rawValue"
|
|
|
|
name=":name" id=":name"/>
|
|
|
|
</x>''')
|
|
|
|
|
|
|
|
pxEdit = Px('''
|
|
|
|
<x var="fmt=field.format;
|
|
|
|
isSelect=field.isSelect;
|
|
|
|
isMaster=field.slaves;
|
|
|
|
isOneLine=fmt in (0,3,4)">
|
|
|
|
<x if="isSelect">
|
|
|
|
<select var="possibleValues=field.getPossibleValues(contextObj, \
|
|
|
|
withTranslations=True, withBlankValue=True)"
|
|
|
|
name=":name" id=":name" class=":masterCss"
|
|
|
|
multiple=":isMultiple and 'multiple' or ''"
|
|
|
|
onchange=":isMaster and 'updateSlaves(this)' or ''"
|
|
|
|
size=":isMultiple and field.height or 1">
|
|
|
|
<option for="val in possibleValues" value=":val[0]"
|
|
|
|
selected=":field.isSelected(contextObj, val[0], rawValue)"
|
|
|
|
title=":val[1]">:ztool.truncateValue(val[1], field.width)">
|
|
|
|
</option>
|
|
|
|
</select>
|
|
|
|
</x>
|
|
|
|
<x if="isOneLine and not isSelect">
|
|
|
|
<input id=":name" name=":name" size=":field.width"
|
|
|
|
maxlength=":field.maxChars"
|
|
|
|
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>
|
|
|
|
</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(contextObj)</script>
|
|
|
|
</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('''
|
|
|
|
<x>
|
|
|
|
<label lfor="widgetName">:_(field.labelId)"></label><br/>
|
|
|
|
<!-- Show a simple search field for most String fields -->
|
|
|
|
<x if="not field.isSelect">
|
|
|
|
<input type="text" maxlength=":field.maxChars" size=":field.swidth"
|
|
|
|
name=":'%s*string-%s' % (widgetName, field.transform)"
|
|
|
|
style=":'text-transform:%s' % field.transform"
|
|
|
|
value=":field.sdefault"/>
|
|
|
|
</x>
|
|
|
|
<!-- 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 var="operName='o_%s' % name;
|
|
|
|
orName='%s_or' % operName;
|
|
|
|
andName='%s_and' % operName"
|
|
|
|
if="field.multiplicity[1] != 1">
|
|
|
|
<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">
|
|
|
|
<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/>
|
|
|
|
</x>''')
|
|
|
|
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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
|
2013-07-10 11:05:06 -05:00
|
|
|
|
|
|
|
@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)
|
|
|
|
|
2013-07-08 16:39:16 -05:00
|
|
|
@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
|
|
|
|
|
|
|
|
# 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,
|
|
|
|
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,
|
|
|
|
transform='none', styles=('p','h1','h2','h3','h4'),
|
|
|
|
allowImageUpload=True, 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
|
|
|
|
# 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 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
|
|
|
|
# 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
|
|
|
|
Field.__init__(self, validator, multiplicity, default, show, page,
|
|
|
|
group, layouts, move, indexed, searchable,
|
|
|
|
specificReadPermission, specificWritePermission, width,
|
|
|
|
height, maxChars, colspan, master, masterValue, focus,
|
|
|
|
historized, True, mapping, label, sdefault, scolspan,
|
|
|
|
swidth, sheight)
|
|
|
|
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 (self.format == String.LINE) and \
|
|
|
|
not self.isSelect
|
|
|
|
self.swidth = self.swidth or self.width
|
|
|
|
self.sheight = self.sheight or self.height
|
|
|
|
|
|
|
|
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 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 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 store(self, obj, value):
|
|
|
|
'''When the value is XHTML, we perform some cleanup.'''
|
|
|
|
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 getDiffValue(self, obj, value):
|
|
|
|
'''Returns a version of p_value that includes the cumulative diffs
|
|
|
|
between successive versions.'''
|
|
|
|
res = None
|
|
|
|
lastEvent = None
|
|
|
|
for event in obj.workflow_history.values()[0]:
|
|
|
|
if event['action'] != '_datachange_': continue
|
|
|
|
if self.name not in event['changes']: continue
|
|
|
|
if res == None:
|
|
|
|
# We have found the first version of the field
|
|
|
|
res = event['changes'][self.name][0] or ''
|
|
|
|
else:
|
|
|
|
# We need to produce the difference between current result and
|
|
|
|
# this version.
|
|
|
|
iMsg, dMsg = obj.getHistoryTexts(lastEvent)
|
|
|
|
thisVersion = event['changes'][self.name][0] or ''
|
|
|
|
comparator = HtmlDiff(res, thisVersion, iMsg, dMsg)
|
|
|
|
res = comparator.get()
|
|
|
|
lastEvent = event
|
|
|
|
# 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 getFormattedValue(self, obj, value, showChanges=False):
|
|
|
|
if self.isEmptyValue(value): 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) for v in value]
|
|
|
|
else:
|
|
|
|
return val.getText(obj, value, self)
|
|
|
|
else:
|
|
|
|
# Value(s) come from a fixed vocabulary whose texts are in
|
|
|
|
# i18n files.
|
|
|
|
t = obj.translate
|
|
|
|
if self.isMultiValued():
|
|
|
|
res = [t('%s_list_%s' % (self.labelId, v)) for v in value]
|
|
|
|
else:
|
|
|
|
res = t('%s_list_%s' % (self.labelId, value))
|
|
|
|
elif (self.format == String.XHTML) and showChanges:
|
|
|
|
# Compute the successive changes that occurred on p_value.
|
|
|
|
res = self.getDiffValue(obj, res)
|
|
|
|
# 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
|
|
|
|
|
|
|
|
emptyStringTuple = ('',)
|
|
|
|
emptyValuesCatalogIgnored = (None, '')
|
|
|
|
def getIndexValue(self, obj, forSearch=False):
|
|
|
|
'''For indexing purposes, we return only strings, not unicodes.'''
|
|
|
|
res = Field.getIndexValue(self, obj, forSearch)
|
|
|
|
if isinstance(res, unicode):
|
|
|
|
res = res.encode('utf-8')
|
|
|
|
if res and forSearch and (self.format == String.XHTML):
|
|
|
|
# Convert the value to simple text.
|
|
|
|
extractor = XhtmlTextExtractor(raiseOnError=False)
|
|
|
|
res = extractor.parse('<p>%s</p>' % res)
|
|
|
|
# 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(res, tuple) and not res: res = self.emptyStringTuple
|
|
|
|
# Ugly catalog: if value is an empty string or None, it keeps the
|
|
|
|
# previous index value.
|
|
|
|
if res in self.emptyValuesCatalogIgnored: res = ' '
|
|
|
|
return res
|
|
|
|
|
2013-07-10 02:56:35 -05:00
|
|
|
def getPossibleValues(self, obj, withTranslations=False,
|
|
|
|
withBlankValue=False, className=None):
|
|
|
|
'''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). 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.')
|
2013-07-08 16:39:16 -05:00
|
|
|
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:]
|
2013-07-10 02:56:35 -05:00
|
|
|
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()
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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)) )
|
|
|
|
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'))
|
|
|
|
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)
|
|
|
|
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')
|
|
|
|
|
|
|
|
accents = {'é':'e','è':'e','ê':'e','ë':'e','à':'a','â':'a','ä':'a',
|
|
|
|
'ù':'u','û':'u','ü':'u','î':'i','ï':'i','ô':'o','ö':'o',
|
|
|
|
'ç':'c', 'Ç':'C',
|
|
|
|
'Ù':'U','Û':'U','Ü':'U','Î':'I','Ï':'I','Ô':'O','Ö':'O',
|
|
|
|
'É':'E','È':'E','Ê':'E','Ë':'E','À':'A','Â':'A','Ä':'A'}
|
|
|
|
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
|
|
|
|
# (1) 'é'.upper() or 'Ê'.lower() has no effect;
|
|
|
|
# (2) most of the time, if the user wants to apply such effect, it
|
|
|
|
# is for ease of data manipulation, so I guess without accent.
|
|
|
|
for c, n in self.accents.iteritems():
|
|
|
|
if c in value: value = value.replace(c, n)
|
|
|
|
# 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 getStorableValue(self, value):
|
|
|
|
isString = isinstance(value, basestring)
|
|
|
|
# Apply transform if required
|
|
|
|
if isString and not self.isEmptyValue(value) and \
|
|
|
|
(self.transform != 'none'):
|
|
|
|
value = self.applyTransform(value)
|
|
|
|
# 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 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 digits or letters
|
|
|
|
# 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']
|
2013-07-10 02:56:35 -05:00
|
|
|
|
|
|
|
def getJsInit(self, obj):
|
|
|
|
'''Gets the Javascript init code for displaying a rich editor for this
|
|
|
|
field (rich field only).'''
|
|
|
|
# Define the attributes that will initialize the ckeditor instance for
|
|
|
|
# this field.
|
|
|
|
ckAttrs = {'toolbar': 'Appy',
|
|
|
|
'format_tags': '%s' % ';'.join(self.styles)}
|
|
|
|
if self.width: ckAttrs['width'] = self.width
|
|
|
|
if self.allowImageUpload:
|
|
|
|
ckAttrs['filebrowserUploadUrl'] = '%s/upload' % obj.absolute_url()
|
|
|
|
ck = []
|
|
|
|
for k, v in ckAttrs.iteritems():
|
|
|
|
if isinstance(v, int): sv = str(v)
|
|
|
|
else: sv = '"%s"' % v
|
|
|
|
ck.append('%s: %s' % (k, sv))
|
|
|
|
return 'CKEDITOR.replace("%s", {%s})' % (name, ', '.join(ck))
|
|
|
|
|
|
|
|
def getJsInlineInit(self, obj):
|
|
|
|
'''Gets the Javascript init code for enabling inline edition of this
|
|
|
|
field (rich text only).'''
|
|
|
|
uid = obj.UID()
|
|
|
|
return "CKEDITOR.disableAutoInline = true;\n" \
|
|
|
|
"CKEDITOR.inline('%s_%s_ck', {on: {blur: " \
|
|
|
|
"function( event ) { var data = event.editor.getData(); " \
|
|
|
|
"askAjaxChunk('%s_%s','POST','%s','page','saveField', "\
|
|
|
|
"{'fieldName':'%s', 'fieldContent': encodeURIComponent(data)}, "\
|
|
|
|
"null, evalInnerScripts);}}});"% \
|
|
|
|
(uid, self.name, uid, self.name, obj.absolute_url(), self.name)
|
|
|
|
|
|
|
|
def isSelected(self, obj, vocabValue, dbValue):
|
|
|
|
'''When displaying a selection box (only for fields with a validator
|
|
|
|
being a list), must the _vocabValue appear as selected?'''
|
|
|
|
rq = obj.REQUEST
|
|
|
|
# Get the value we must compare (from request or from database)
|
|
|
|
if rq.has_key(self.name):
|
|
|
|
compValue = rq.get(self.name)
|
|
|
|
else:
|
|
|
|
compValue = dbValue
|
|
|
|
# Compare the value
|
|
|
|
if type(compValue) in sutils.sequenceTypes:
|
|
|
|
return vocabValue in compValue
|
|
|
|
return vocabValue == compValue
|
2013-07-08 16:39:16 -05:00
|
|
|
# ------------------------------------------------------------------------------
|