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)
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})?\/.*)?') pxView = Px(''' <x var="fmt=field.format; isUrl=field.isUrl; mayAjaxEdit=not showChanges and field.inlineEdit and \ zobj.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)">::zobj.formatText(value, format='html')</x> <!-- XHTML text --> <x if="fmt == 2"> <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> </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 = Px(''' <x var="fmt=field.format; isOneLine=fmt in (0,3,4)"> <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> <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> </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>''') 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/>''') # 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 # 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, persist=True, transform='none', placeholder=None, styles=('p', 'h1', 'h2', 'h3', 'h4'), allowImageUpload=True, spellcheck=False, contentLanguage=None, 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 is XHTML, do we run the CK spellchecker ? self.spellcheck = spellcheck # What is the language of field content? self.contentLanguage = contentLanguage # 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 # "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, searchable, specificReadPermission, specificWritePermission, width, height, maxChars, colspan, master, masterValue, focus, historized, mapping, label, sdefault, scolspan, swidth, sheight, persist) 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 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)