# -*- 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 . # ------------------------------------------------------------------------------ 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. Method String.getJs # below determines when the files must be included. cdnUrl = '//cdn.ckeditor.com/%s/%s/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(''' - ******** :value ::value ''') # pxView part for format String.TEXT pxViewText = Px(''' -::value''') # pxView part for format String.XHTML pxViewRich = Px('''
::value or '-'
::value or '-'
''') # PX displaying the language code and name besides the part of the # multilingual field storing content in this language. pxLanguage = Px(''' :lg.upper() ''') pxMultilingual = Px(''' :field.pxLanguage
:field.subPx[layoutType][fmt]
:field.pxLanguage
:field.subPx[layoutType][fmt]
''') pxView = Px(''' - ::value
  • ::sv
:field.subPx['view'][fmt] :field.pxMultilingual
''') # pxEdit part for formats String.LINE (but that are not selections), # String.PASSWORD and String.CAPTCHA. pxEditLine = Px(''' :_('captcha_text', \ mapping=field.getCaptchaChallenge(req.SESSION)) ''') # pxEdit part for formats String.TEXT and String.XHTML pxEditTextArea = Px(''' ''') pxEdit = Px(''' :field.subPx['edit'][fmt] :field.pxMultilingual ''') pxCell = Px(''' :', '.join(value) :field.pxView ''') pxSearch = Px('''

''') # 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 elif format == String.XHTML: self.width = None else: self.width = 30 if height == None: if format == String.TEXT: self.height = 5 elif format == String.XHTML: self.height = None 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 getSelectSize(self, isMultiple): '''When this field renders as a selection list, get the value of its "size" attribute.''' if not isMultiple: return 1 if isinstance(self.height, int): return self.height # "height" can be defined as a string. In this case it is used to define # height via a attribute "style", not "size". return '' def getSelectStyle(self, isMultiple): '''When thiss field renders as a selection list, get the value of its "style" attribute.''' if not isMultiple or not isinstance(self.height, str): return '' return 'height: %s' % self.height 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 isinstance(obj, dict): return Field.isEmptyValue(self, obj, value) # p_value is a dict of multilingual values. For such values, as soon # as a value is not empty for a given language, the whole value is # considered as not being empty. for v in value.itervalues(): if not Field.isEmptyValue(self, obj, v): return 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, layoutType='view', 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) elif self.format == String.TEXT: if layoutType != 'edit': res = obj.formatText(res, format='html') # 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, layoutType='view', 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, layoutType, 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, layoutType, showChanges, language=lg) return res def getShownValue(self, obj, value, layoutType='view', 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, layoutType, 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], layoutType, showChanges, language=lg) def extractText(self, value): '''Extracts pure text from XHTML p_value.''' return XhtmlTextExtractor(raiseOnError=False).parse('

%s

' % 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 isHistorized = self.getAttribute(obj, 'historized') previousData = None if isHistorized: previousData = obj.rememberPreviousData([self]) if self.isMultilingual(obj): if isHistorized: # 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 isHistorized and 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, config): if (self.format == String.XHTML) and (layoutType in ('edit', 'view')): # Compute the URL to ckeditor CDN ckUrl = String.cdnUrl % (config.ckVersion, config.ckDistribution) if ckUrl not in res: res.append(ckUrl) 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''' base = obj.getTool().getSiteUrl() ckAttrs = {'customConfig': '%s/ui/ckeditor/config.js' % base, 'contentsCss': '%s/ui/ckeditor/contents.css' % base, 'stylesSet': '%s/ui/ckeditor/styles.js' % base, 'toolbar': 'Appy', 'format_tags': ';'.join(self.styles), 'scayt_sLang': self.getCkLanguage(obj, language)} if self.width: ckAttrs['width'] = self.width if self.height: ckAttrs['height'] = self.height 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 # ------------------------------------------------------------------------------