# -*- 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 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(''' - ******** :value ::value ''') # pxView part for format String.TEXT. pxViewText = Px(''' - ::zobj.formatText(value, format='html')''') # 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, 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)? 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, 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 (self.format == String.LINE) and \ not self.isSelect 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('

%s

' % value) emptyStringTuple = ('',) emptyValuesCatalogIgnored = (None, '') def getIndexValue(self, obj, forSearch=False): '''Pure text must be extracted from rich content; multilingual content must be concatenated.''' 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) # 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 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). previousData[self.name] = previousData[self.name].copy() # 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 # ------------------------------------------------------------------------------