1070 lines
		
	
	
	
		
			51 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1070 lines
		
	
	
	
		
			51 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # -*- coding: utf-8 -*-
 | |
| # ------------------------------------------------------------------------------
 | |
| # This file is part of Appy, a framework for building applications in the Python
 | |
| # language. Copyright (C) 2007 Gaetan Delannay
 | |
| 
 | |
| # Appy is free software; you can redistribute it and/or modify it under the
 | |
| # terms of the GNU General Public License as published by the Free Software
 | |
| # Foundation; either version 3 of the License, or (at your option) any later
 | |
| # version.
 | |
| 
 | |
| # Appy is distributed in the hope that it will be useful, but WITHOUT ANY
 | |
| # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 | |
| # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 | |
| 
 | |
| # You should have received a copy of the GNU General Public License along with
 | |
| # Appy. If not, see <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| # ------------------------------------------------------------------------------
 | |
| import re, random
 | |
| from appy.gen.layout import Table
 | |
| from appy.gen.indexer import XhtmlTextExtractor
 | |
| from appy.fields import Field
 | |
| from appy.px import Px
 | |
| from appy.shared.data import countries
 | |
| from appy.shared.xml_parser import XhtmlCleaner
 | |
| from appy.shared.diff import HtmlDiff
 | |
| from appy.shared import utils as sutils
 | |
| 
 | |
| # ------------------------------------------------------------------------------
 | |
| digit  = re.compile('[0-9]')
 | |
| alpha  = re.compile('[a-zA-Z0-9]')
 | |
| letter = re.compile('[a-zA-Z]')
 | |
| digits = '0123456789'
 | |
| letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
 | |
| # No "0" or "1" that could be interpreted as letters "O" or "l".
 | |
| passwordDigits = '23456789'
 | |
| # No letters i, l, o (nor lowercase nor uppercase) that could be misread.
 | |
| passwordLetters = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'
 | |
| emptyTuple = ()
 | |
| 
 | |
| # ------------------------------------------------------------------------------
 | |
| class Selection:
 | |
|     '''Instances of this class may be given as validator of a String, in order
 | |
|        to tell Appy that the validator is a selection that will be computed
 | |
|        dynamically.'''
 | |
|     def __init__(self, methodName):
 | |
|         # The p_methodName parameter must be the name of a method that will be
 | |
|         # called every time Appy will need to get the list of possible values
 | |
|         # for the related field. It must correspond to an instance method of
 | |
|         # the class defining the related field. This method accepts no argument
 | |
|         # and must return a list (or tuple) of pairs (lists or tuples):
 | |
|         # (id, text), where "id" is one of the possible values for the
 | |
|         # field, and "text" is the value as will be shown on the screen.
 | |
|         # You can use self.translate within this method to produce an
 | |
|         # internationalized version of "text" if needed.
 | |
|         self.methodName = methodName
 | |
| 
 | |
|     def getText(self, obj, value, field, language=None):
 | |
|         '''Gets the text that corresponds to p_value.'''
 | |
|         if language:
 | |
|             withTranslations = language
 | |
|         else:
 | |
|             withTranslations = True
 | |
|         vals = field.getPossibleValues(obj, ignoreMasterValues=True,\
 | |
|                                        withTranslations=withTranslations)
 | |
|         for v, text in vals:
 | |
|             if v == value: return text
 | |
|         return value
 | |
| 
 | |
| # ------------------------------------------------------------------------------
 | |
| class String(Field):
 | |
|     # Javascript files sometimes required by this type. 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('''
 | |
|      <span if="not value" class="smaller">-</span>
 | |
|      <x if="value">
 | |
|       <!-- A password -->
 | |
|       <x if="fmt == 3">********</x>
 | |
|       <!-- A URL -->
 | |
|       <a if="(fmt != 3) and isUrl" target="_blank" href=":value">:value</a>
 | |
|       <!-- Any other value -->
 | |
|       <x if="(fmt != 3) and not isUrl">::value</x>
 | |
|      </x>''')
 | |
| 
 | |
|     # pxView part for format String.TEXT
 | |
|     pxViewText = Px('''
 | |
|      <span if="not value" class="smaller">-</span><x if="value">::value</x>''')
 | |
| 
 | |
|     # pxView part for format String.XHTML
 | |
|     pxViewRich = Px('''
 | |
|      <div if="not mayAjaxEdit" class="xhtml">::value or '-'</div>
 | |
|      <x if="mayAjaxEdit" var2="name=lg and ('%s_%s' % (name, lg)) or name">
 | |
|       <div class="xhtml" contenteditable="true"
 | |
|            id=":'%s_%s_ck' % (zobj.id, name)">::value or '-'</div>
 | |
|       <script if="mayAjaxEdit">::field.getJsInlineInit(zobj, name, lg)</script>
 | |
|      </x>''')
 | |
| 
 | |
|     # PX displaying the language code and name besides the part of the
 | |
|     # multilingual field storing content in this language.
 | |
|     pxLanguage = Px('''
 | |
|      <td style=":'padding-top:%dpx' % lgTop" width="25px">
 | |
|       <span class="language help"
 | |
|             title=":ztool.getLanguageName(lg)">:lg.upper()</span>
 | |
|      </td>''')
 | |
| 
 | |
|     pxMultilingual = Px('''
 | |
|      <!-- Horizontally-layouted multilingual field -->
 | |
|      <table if="mLayout == 'horizontal'" width="100%"
 | |
|             var="count=len(languages)">
 | |
|       <tr valign="top"><x for="lg in languages"><x>:field.pxLanguage</x>
 | |
|        <td width=":'%d%%' % int(100.0/count)"
 | |
|            var="requestValue=requestValue[lg]|None;
 | |
|                 value=value[lg]|emptyDefault">:field.subPx[layoutType][fmt]</td>
 | |
|       </x></tr></table>
 | |
| 
 | |
|      <!-- Vertically-layouted multilingual field -->
 | |
|      <table if="mLayout == 'vertical'">
 | |
|       <tr valign="top" height="20px" for="lg in languages">
 | |
|        <x>:field.pxLanguage</x>
 | |
|        <td var="requestValue=requestValue[lg]|None;
 | |
|                 value=value[lg]|emptyDefault">:field.subPx[layoutType][fmt]</td>
 | |
|      </tr></table>''')
 | |
| 
 | |
|     pxView = Px('''
 | |
|      <x var="fmt=field.format; isUrl=field.isUrl;
 | |
|              languages=field.getAttribute(obj, 'languages');
 | |
|              multilingual=len(languages) > 1;
 | |
|              mLayout=multilingual and field.getLanguagesLayout('view');
 | |
|              inlineEdit=field.getAttribute(obj, 'inlineEdit');
 | |
|              mayAjaxEdit=not showChanges and inlineEdit and \
 | |
|                          (layoutType != 'cell') and \
 | |
|                          zobj.mayEdit(field.writePermission)">
 | |
|       <x if="field.isSelect">
 | |
|        <span if="not value" class="smaller">-</span>
 | |
|        <x if="value and not isMultiple">::value</x>
 | |
|        <ul if="value and isMultiple"><li for="sv in value"><i>::sv</i></li></ul>
 | |
|       </x>
 | |
|       <!-- Any other unilingual field -->
 | |
|       <x if="not field.isSelect and not multilingual"
 | |
|          var2="lg=None">:field.subPx['view'][fmt]</x>
 | |
|       <!-- Any other multilingual field -->
 | |
|       <x if="not field.isSelect and multilingual"
 | |
|          var2="lgTop=1; emptyDefault='-'">:field.pxMultilingual</x>
 | |
|       <!-- If this field is a master field -->
 | |
|       <input type="hidden" if="masterCss" class=":masterCss" value=":rawValue"
 | |
|              name=":name" id=":name"/></x>''')
 | |
| 
 | |
|     # pxEdit part for formats String.LINE (but that are not selections),
 | |
|     # String.PASSWORD and String.CAPTCHA.
 | |
|     pxEditLine = Px('''
 | |
|      <input var="inputId=not lg and name or '%s_%s' % (name, lg);
 | |
|                  placeholder=field.getAttribute(obj, 'placeholder') or ''"
 | |
|             id=":inputId" name=":inputId" size=":field.width"
 | |
|             maxlength=":field.maxChars" placeholder=":placeholder"
 | |
|             value=":inRequest and requestValue or value"
 | |
|             style=":'text-transform:%s' % field.transform"
 | |
|             type=":(fmt == 3) and 'password' or 'text'"/>
 | |
|      <!-- Display a captcha if required -->
 | |
|      <span if="fmt == 4">:_('captcha_text', \
 | |
|                             mapping=field.getCaptchaChallenge(req.SESSION))
 | |
|      </span>''')
 | |
| 
 | |
|     # pxEdit part for formats String.TEXT and String.XHTML
 | |
|     pxEditTextArea = Px('''
 | |
|      <textarea var="inputId=not lg and name or '%s_%s' % (name, lg)"
 | |
|                id=":inputId" name=":inputId" cols=":field.width"
 | |
|                style=":'text-transform:%s' % field.transform"
 | |
|                rows=":field.height">:inRequest and requestValue or value
 | |
|      </textarea>
 | |
|      <script if="fmt == 2"
 | |
|              type="text/javascript">::field.getJsInit(zobj, lg)</script>''')
 | |
| 
 | |
|     pxEdit = Px('''
 | |
|      <x var="fmt=field.format;
 | |
|              languages=field.getAttribute(zobj, 'languages');
 | |
|              multilingual=len(languages) > 1;
 | |
|              mLayout=multilingual and field.getLanguagesLayout('edit')">
 | |
|       <select if="field.isSelect"
 | |
|               var2="possibleValues=field.getPossibleValues(zobj, \
 | |
|                       withTranslations=True, withBlankValue=True)"
 | |
|               name=":name" id=":name" class=":masterCss"
 | |
|               multiple=":isMultiple and 'multiple' or ''"
 | |
|               onchange=":field.getOnChange(zobj, layoutType)"
 | |
|               size=":field.getSelectSize(isMultiple)"
 | |
|               style=":field.getSelectStyle(isMultiple)">
 | |
|        <option for="val in possibleValues" value=":val[0]"
 | |
|                selected=":field.isSelected(zobj, name, val[0], rawValue)"
 | |
|                title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
 | |
|       </select>
 | |
|       <!-- Any other unilingual field -->
 | |
|       <x if="not field.isSelect and not multilingual"
 | |
|          var2="lg=None">:field.subPx['edit'][fmt]</x>
 | |
|       <!-- Any other multilingual field -->
 | |
|       <x if="not field.isSelect and multilingual"
 | |
|          var2="lgTop=(fmt!=2) and 3 or 1;
 | |
|                emptyDefault=''">:field.pxMultilingual</x>
 | |
|       </x>''')
 | |
| 
 | |
|     pxCell = Px('''
 | |
|      <x var="multipleValues=value and isMultiple">
 | |
|       <x if="multipleValues">:', '.join(value)</x>
 | |
|       <x if="not multipleValues">:field.pxView</x>
 | |
|      </x>''')
 | |
| 
 | |
|     pxSearch = Px('''
 | |
|      <!-- Show a simple search field for most String fields -->
 | |
|      <input if="not field.isSelect" type="text" maxlength=":field.maxChars"
 | |
|             size=":field.swidth" value=":field.sdefault"
 | |
|             name=":'%s*string-%s' % (widgetName, field.transform)"
 | |
|             style=":'text-transform:%s' % field.transform"/>
 | |
|      <!-- Show a multi-selection box for fields whose validator defines a list
 | |
|           of values, with a "AND/OR" checkbox. -->
 | |
|      <x if="field.isSelect">
 | |
|       <!-- The "and" / "or" radio buttons -->
 | |
|       <x if="field.multiplicity[1] != 1"
 | |
|          var2="operName='o_%s' % name;
 | |
|               orName='%s_or' % operName;
 | |
|               andName='%s_and' % operName">
 | |
|        <input type="radio" name=":operName" id=":orName" checked="checked"
 | |
|               value="or"/>
 | |
|        <label lfor=":orName">:_('search_or')</label>
 | |
|        <input type="radio" name=":operName" id=":andName" value="and"/>
 | |
|        <label lfor=":andName">:_('search_and')</label><br/>
 | |
|       </x>
 | |
|       <!-- The list of values -->
 | |
|       <select var="preSelected=field.sdefault"
 | |
|               name=":widgetName" size=":field.sheight" multiple="multiple"
 | |
|               onchange=":field.getOnChange(ztool, 'search', className)">
 | |
|        <option for="v in field.getPossibleValues(ztool, withTranslations=True,\
 | |
|                                      withBlankValue=False, className=className)"
 | |
|                selected=":v[0] in preSelected" value=":v[0]"
 | |
|                title=":v[1]">:ztool.truncateValue(v[1], field.swidth)</option>
 | |
|       </select>
 | |
|      </x><br/>''')
 | |
| 
 | |
|     # Sub-PX to use according to String format.
 | |
|     subPx = {
 | |
|      'edit': {LINE:pxEditLine, TEXT:pxEditTextArea, XHTML:pxEditTextArea,
 | |
|               PASSWORD:pxEditLine, CAPTCHA:pxEditLine},
 | |
|      'view': {LINE:pxViewLine, TEXT:pxViewText, XHTML:pxViewRich,
 | |
|               PASSWORD:pxViewLine, CAPTCHA:pxViewLine}
 | |
|     }
 | |
|     subPx['cell'] = subPx['view']
 | |
| 
 | |
|     # Some predefined functions that may also be used as validators
 | |
|     @staticmethod
 | |
|     def _MODULO_97(obj, value, complement=False):
 | |
|         '''p_value must be a string representing a number, like a bank account.
 | |
|            this function checks that the 2 last digits are the result of
 | |
|            computing the modulo 97 of the previous digits. Any non-digit
 | |
|            character is ignored. If p_complement is True, it does compute the
 | |
|            complement of modulo 97 instead of modulo 97. p_obj is not used;
 | |
|            it will be given by the Appy validation machinery, so it must be
 | |
|            specified as parameter. The function returns True if the check is
 | |
|            successful.'''
 | |
|         if not value: return True
 | |
|         # First, remove any non-digit char
 | |
|         v = ''
 | |
|         for c in value:
 | |
|             if digit.match(c): v += c
 | |
|         # There must be at least 3 digits for performing the check
 | |
|         if len(v) < 3: return False
 | |
|         # Separate the real number from the check digits
 | |
|         number = int(v[:-2])
 | |
|         checkNumber = int(v[-2:])
 | |
|         # Perform the check
 | |
|         if complement:
 | |
|             return (97 - (number % 97)) == checkNumber
 | |
|         else:
 | |
|             # The check number can't be 0. In this case, we force it to be 97.
 | |
|             # This is the way Belgian bank account numbers work. I hope this
 | |
|             # behaviour is general enough to be implemented here.
 | |
|             mod97 = (number % 97)
 | |
|             if mod97 == 0: return checkNumber == 97
 | |
|             else:          return checkNumber == mod97
 | |
|     @staticmethod
 | |
|     def MODULO_97(obj, value): return String._MODULO_97(obj, value)
 | |
|     @staticmethod
 | |
|     def MODULO_97_COMPLEMENT(obj, value):
 | |
|         return String._MODULO_97(obj, value, True)
 | |
|     BELGIAN_ENTERPRISE_NUMBER = MODULO_97_COMPLEMENT
 | |
| 
 | |
|     @staticmethod
 | |
|     def BELGIAN_NISS(obj, value):
 | |
|         '''Returns True if the NISS in p_value is valid.'''
 | |
|         if not value: return True
 | |
|         # Remove any non-digit from nrn
 | |
|         niss = sutils.keepDigits(value)
 | |
|         # NISS must be made of 11 numbers
 | |
|         if len(niss) != 11: return False
 | |
|         # When NRN begins with 0 or 1, it must be prefixed with number "2" for
 | |
|         # checking the modulo 97 complement.
 | |
|         nissForModulo = niss
 | |
|         if niss.startswith('0') or niss.startswith('1'):
 | |
|             nissForModulo = '2'+niss
 | |
|         # Check modulo 97 complement
 | |
|         return String.MODULO_97_COMPLEMENT(None, nissForModulo)
 | |
| 
 | |
|     @staticmethod
 | |
|     def IBAN(obj, value):
 | |
|         '''Checks that p_value corresponds to a valid IBAN number. IBAN stands
 | |
|            for International Bank Account Number (ISO 13616). If the number is
 | |
|            valid, the method returns True.'''
 | |
|         if not value: return True
 | |
|         # First, remove any non-digit or non-letter char
 | |
|         v = ''
 | |
|         for c in value:
 | |
|             if alpha.match(c): v += c
 | |
|         # Maximum size is 34 chars
 | |
|         if (len(v) < 8) or (len(v) > 34): return False
 | |
|         # 2 first chars must be a valid country code
 | |
|         if not countries.exists(v[:2].upper()): return False
 | |
|         # 2 next chars are a control code whose value must be between 0 and 96.
 | |
|         try:
 | |
|             code = int(v[2:4])
 | |
|             if (code < 0) or (code > 96): return False
 | |
|         except ValueError:
 | |
|             return False
 | |
|         # Perform the checksum
 | |
|         vv = v[4:] + v[:4] # Put the 4 first chars at the end.
 | |
|         nv = ''
 | |
|         for c in vv:
 | |
|             # Convert each letter into a number (A=10, B=11, etc)
 | |
|             # Ascii code for a is 65, so A=10 if we perform "minus 55"
 | |
|             if letter.match(c): nv += str(ord(c.upper()) - 55)
 | |
|             else: nv += c
 | |
|         return int(nv) % 97 == 1
 | |
| 
 | |
|     @staticmethod
 | |
|     def BIC(obj, value):
 | |
|         '''Checks that p_value corresponds to a valid BIC number. BIC stands
 | |
|            for Bank Identifier Code (ISO 9362). If the number is valid, the
 | |
|            method returns True.'''
 | |
|         if not value: return True
 | |
|         # BIC number must be 8 or 11 chars
 | |
|         if len(value) not in (8, 11): return False
 | |
|         # 4 first chars, representing bank name, must be letters
 | |
|         for c in value[:4]:
 | |
|             if not letter.match(c): return False
 | |
|         # 2 next chars must be a valid country code
 | |
|         if not countries.exists(value[4:6].upper()): return False
 | |
|         # Last chars represent some location within a country (a city, a
 | |
|         # province...). They can only be letters or figures.
 | |
|         for c in value[6:]:
 | |
|             if not alpha.match(c): return False
 | |
|         return True
 | |
| 
 | |
|     def __init__(self, validator=None, multiplicity=(0,1), default=None,
 | |
|                  format=LINE, show=True, page='main', group=None, layouts=None,
 | |
|                  move=0, indexed=False, mustIndex=True, searchable=False,
 | |
|                  specificReadPermission=False, specificWritePermission=False,
 | |
|                  width=None, height=None, maxChars=None, colspan=1, master=None,
 | |
|                  masterValue=None, focus=False, historized=False, mapping=None,
 | |
|                  label=None, sdefault='', scolspan=1, swidth=None, sheight=None,
 | |
|                  persist=True, transform='none', placeholder=None,
 | |
|                  styles=('p','h1','h2','h3','h4'), allowImageUpload=True,
 | |
|                  spellcheck=False, languages=('en',), languagesLayouts=None,
 | |
|                  inlineEdit=False, view=None, xml=None):
 | |
|         # According to format, the widget will be different: input field,
 | |
|         # textarea, inline editor... Note that there can be only one String
 | |
|         # field of format CAPTCHA by page, because the captcha challenge is
 | |
|         # stored in the session at some global key.
 | |
|         self.format = format
 | |
|         self.isUrl = validator == String.URL
 | |
|         # When format is XHTML, the list of styles that the user will be able to
 | |
|         # select in the styles dropdown is defined hereafter.
 | |
|         self.styles = styles
 | |
|         # When format is XHTML, do we allow the user to upload images in it ?
 | |
|         self.allowImageUpload = allowImageUpload
 | |
|         # When format is XHTML, do we run the CK spellchecker ?
 | |
|         self.spellcheck = spellcheck
 | |
|         # If "languages" holds more than one language, the field will be
 | |
|         # multi-lingual and several widgets will allow to edit/visualize the
 | |
|         # field content in all the supported languages. The field is also used
 | |
|         # by the CK spell checker.
 | |
|         self.languages = languages
 | |
|         # When content exists in several languages, how to render them? Either
 | |
|         # horizontally (one below the other), or vertically (one besides the
 | |
|         # other). Specify here a dict whose keys are layouts ("edit", "view")
 | |
|         # and whose values are either "horizontal" or "vertical".
 | |
|         self.languagesLayouts = languagesLayouts
 | |
|         # When format in XHTML, can the field be inline-edited (ckeditor)? A
 | |
|         # method can be specified.
 | |
|         self.inlineEdit = inlineEdit
 | |
|         # The following field has a direct impact on the text entered by the
 | |
|         # user. It applies a transformation on it, exactly as does the CSS
 | |
|         # "text-transform" property. Allowed values are those allowed for the
 | |
|         # CSS property: "none" (default), "uppercase", "capitalize" or
 | |
|         # "lowercase".
 | |
|         self.transform = transform
 | |
|         # "placeholder", similar to the HTML attribute of the same name, allows
 | |
|         # to specify a short hint that describes the expected value of the input
 | |
|         # field. It is shown inside the input field and disappears as soon as
 | |
|         # the user encodes something in it. Works only for strings whose format
 | |
|         # is LINE. Does not work with IE < 10. You can specify a method here,
 | |
|         # that can, for example, return an internationalized value.
 | |
|         self.placeholder = placeholder
 | |
|         Field.__init__(self, validator, multiplicity, default, show, page,
 | |
|                        group, layouts, move, indexed, mustIndex, searchable,
 | |
|                        specificReadPermission, specificWritePermission, width,
 | |
|                        height, maxChars, colspan, master, masterValue, focus,
 | |
|                        historized, mapping, label, sdefault, scolspan, swidth,
 | |
|                        sheight, persist, view, xml)
 | |
|         self.isSelect = self.isSelection()
 | |
|         # If self.isSelect, self.sdefault must be a list of value(s).
 | |
|         if self.isSelect and not sdefault:
 | |
|             self.sdefault = []
 | |
|         # Default width, height and maxChars vary according to String format
 | |
|         if width == None:
 | |
|             if format == String.TEXT:    self.width  = 60
 | |
|             # This width corresponds to the standard width of an Appy page
 | |
|             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('<p>%s</p>' % value)
 | |
| 
 | |
|     def getValidCatalogValue(self, value, forSearch):
 | |
|         '''p_value is the new value we want to index in the catalog, for this
 | |
|            field, for some object. p_value as is may not be an acceptable value
 | |
|            for the catalog: if it represents some empty value, like an empty
 | |
|            string, None or an empty tuple, instead of using it, the catalog will
 | |
|            keep the previously catalogued value! For those cases, this method
 | |
|            produces "empty" values that will really overwrite previous ones.'''
 | |
|         # Ugly catalog: if I give an empty tuple as index value, it keeps the
 | |
|         # previous value. If I give him a tuple containing an empty string, it
 | |
|         # is ok.
 | |
|         if isinstance(value, tuple) and not value:
 | |
|             value = forSearch and ' ' or ('',)
 | |
|         # Ugly catalog: if value is an empty string or None, it keeps the
 | |
|         # previous index value.
 | |
|         elif value in (None, ''): return ' '
 | |
|         return value
 | |
| 
 | |
|     def getIndexValue(self, obj, forSearch=False):
 | |
|         '''Pure text must be extracted from rich content; multilingual content
 | |
|            must be concatenated.'''
 | |
|         # Must we produce an index value?
 | |
|         if not self.getAttribute(obj, 'mustIndex'):
 | |
|             return self.getValidCatalogValue(None, forSearch)
 | |
|         isXhtml = self.format == String.XHTML
 | |
|         if self.isMultilingual(obj):
 | |
|             res = self.getValue(obj)
 | |
|             if res:
 | |
|                 vals = []
 | |
|                 for v in res.itervalues():
 | |
|                     if isinstance(v, unicode): v = v.encode('utf-8')
 | |
|                     if isXhtml: vals.append(self.extractText(v))
 | |
|                     else: vals.append(v)
 | |
|                 res = ' '.join(vals)
 | |
|         else:
 | |
|             res = Field.getIndexValue(self, obj, forSearch)
 | |
|             if res and isXhtml: res = self.extractText(res)
 | |
|         return self.getValidCatalogValue(res, forSearch)
 | |
| 
 | |
|     def getPossibleValues(self, obj, withTranslations=False,
 | |
|                           withBlankValue=False, className=None,
 | |
|                           ignoreMasterValues=False):
 | |
|         '''Returns the list of possible values for this field (only for fields
 | |
|            with self.isSelect=True). If p_withTranslations is True, instead of
 | |
|            returning a list of string values, the result is a list of tuples
 | |
|            (s_value, s_translation). Moreover, p_withTranslations can hold a
 | |
|            given language: in this case, this language is used instead of the
 | |
|            user language. If p_withBlankValue is True, a blank value is
 | |
|            prepended to the list, excepted if the type is multivalued. If
 | |
|            p_className is given, p_obj is the tool and, if we need an instance
 | |
|            of p_className, we will need to use obj.executeQuery to find one.'''
 | |
|         if not self.isSelect: raise Exception('This field is not a selection.')
 | |
|         # Get the user language for translations, from "withTranslations".
 | |
|         lg = isinstance(withTranslations, str) and withTranslations or None
 | |
|         req = obj.REQUEST
 | |
|         if ('masterValues' in req) and not ignoreMasterValues:
 | |
|             # Get possible values from self.masterValue
 | |
|             masterValues = req['masterValues']
 | |
|             if '*' in masterValues: masterValues = masterValues.split('*')
 | |
|             values = self.masterValue(obj.appy(), masterValues)
 | |
|             if not withTranslations: res = values
 | |
|             else:
 | |
|                 res = []
 | |
|                 for v in values:
 | |
|                     res.append( (v, self.getFormattedValue(obj,v,language=lg)) )
 | |
|         else:
 | |
|             # If this field is an ajax-updatable slave, no need to compute
 | |
|             # possible values: it will be overridden by method self.masterValue
 | |
|             # by a subsequent ajax request (=the "if" statement above).
 | |
|             if self.masterValue and callable(self.masterValue) and \
 | |
|                not ignoreMasterValues: return []
 | |
|             if isinstance(self.validator, Selection):
 | |
|                 # We need to call self.methodName for getting the (dynamic)
 | |
|                 # values. If methodName begins with _appy_, it is a special Appy
 | |
|                 # method: we will call it on the Mixin (=p_obj) directly. Else,
 | |
|                 # it is a user method: we will call it on the wrapper
 | |
|                 # (p_obj.appy()). Some args can be hidden into p_methodName,
 | |
|                 # separated with stars, like in this example: method1*arg1*arg2.
 | |
|                 # Only string params are supported.
 | |
|                 methodName = self.validator.methodName
 | |
|                 # Unwrap parameters if any.
 | |
|                 if methodName.find('*') != -1:
 | |
|                     elems = methodName.split('*')
 | |
|                     methodName = elems[0]
 | |
|                     args = elems[1:]
 | |
|                 else:
 | |
|                     args = ()
 | |
|                 # On what object must we call the method that will produce the
 | |
|                 # values?
 | |
|                 if methodName.startswith('tool:'):
 | |
|                     obj = obj.getTool()
 | |
|                     methodName = methodName[5:]
 | |
|                 else:
 | |
|                     # We must call on p_obj. But if we have something in
 | |
|                     # p_className, p_obj is the tool and not an instance of
 | |
|                     # p_className as required. So find such an instance.
 | |
|                     if className:
 | |
|                         brains = obj.executeQuery(className, maxResults=1,
 | |
|                                                   brainsOnly=True)
 | |
|                         if brains:
 | |
|                             obj = brains[0].getObject()
 | |
|                 # Do we need to call the method on the object or on the wrapper?
 | |
|                 if methodName.startswith('_appy_'):
 | |
|                     exec 'res = obj.%s(*args)' % methodName
 | |
|                 else:
 | |
|                     exec 'res = obj.appy().%s(*args)' % methodName
 | |
|                 if not withTranslations: res = [v[0] for v in res]
 | |
|                 elif isinstance(res, list): res = res[:]
 | |
|             else:
 | |
|                 # The list of (static) values is directly given in
 | |
|                 # self.validator.
 | |
|                 res = []
 | |
|                 for value in self.validator:
 | |
|                     label = '%s_list_%s' % (self.labelId, value)
 | |
|                     if withTranslations:
 | |
|                         res.append( (value, obj.translate(label, language=lg)) )
 | |
|                     else:
 | |
|                         res.append(value)
 | |
|         if withBlankValue and not self.isMultiValued():
 | |
|             # Create the blank value to insert at the beginning of the list
 | |
|             if withTranslations:
 | |
|                 blankValue = ('', obj.translate('choose_a_value', language=lg))
 | |
|             else:
 | |
|                 blankValue = ''
 | |
|             # Insert the blank value in the result
 | |
|             if isinstance(res, tuple):
 | |
|                 res = (blankValue,) + res
 | |
|             else:
 | |
|                 res.insert(0, blankValue)
 | |
|         return res
 | |
| 
 | |
|     def validateValue(self, obj, value):
 | |
|         if self.format == String.CAPTCHA:
 | |
|             challenge = obj.REQUEST.SESSION.get('captcha', None)
 | |
|             # Compute the challenge minus the char to remove
 | |
|             i = challenge['number']-1
 | |
|             text = challenge['text'][:i] + challenge['text'][i+1:]
 | |
|             if value != text:
 | |
|                 return obj.translate('bad_captcha')
 | |
|         elif self.isSelect:
 | |
|             # Check that the value is among possible values
 | |
|             possibleValues = self.getPossibleValues(obj,ignoreMasterValues=True)
 | |
|             if isinstance(value, basestring):
 | |
|                 error = value not in possibleValues
 | |
|             else:
 | |
|                 error = False
 | |
|                 for v in value:
 | |
|                     if v not in possibleValues:
 | |
|                         error = True
 | |
|                         break
 | |
|             if error: return obj.translate('bad_select_value')
 | |
| 
 | |
|     def applyTransform(self, value):
 | |
|         '''Applies a transform as required by self.transform on single
 | |
|            value p_value.'''
 | |
|         if self.transform in ('uppercase', 'lowercase'):
 | |
|             # For those transforms, I will remove any accent, because, most of
 | |
|             # the time, if the user wants to apply such effect, it is for ease
 | |
|             # of data manipulation, so I guess without accent.
 | |
|             value = sutils.normalizeString(value, usage='noAccents')
 | |
|         # Apply the transform
 | |
|         if   self.transform == 'lowercase':  return value.lower()
 | |
|         elif self.transform == 'uppercase':  return value.upper()
 | |
|         elif self.transform == 'capitalize': return value.capitalize()
 | |
|         return value
 | |
| 
 | |
|     def getUnilingualStorableValue(self, obj, value):
 | |
|         isString = isinstance(value, basestring)
 | |
|         isEmpty = Field.isEmptyValue(self, obj, value)
 | |
|         # Apply transform if required
 | |
|         if isString and not isEmpty and (self.transform != 'none'):
 | |
|            value = self.applyTransform(value)
 | |
|         # Clean XHTML strings
 | |
|         if not isEmpty and (self.format == String.XHTML):
 | |
|             # When image upload is allowed, ckeditor inserts some "style" attrs
 | |
|             # (ie for image size when images are resized). So in this case we
 | |
|             # can't remove style-related information.
 | |
|             try:
 | |
|                 value = XhtmlCleaner(keepStyles=False).clean(value)
 | |
|             except XhtmlCleaner.Error, e:
 | |
|                 # Errors while parsing p_value can't prevent the user from
 | |
|                 # storing it.
 | |
|                 pass
 | |
|         # Clean TEXT strings
 | |
|         if not isEmpty and (self.format == String.TEXT):
 | |
|             value = value.replace('\r', '')
 | |
|         # Truncate the result if longer than self.maxChars
 | |
|         if isString and self.maxChars and (len(value) > self.maxChars):
 | |
|             value = value[:self.maxChars]
 | |
|         # Get a multivalued value if required.
 | |
|         if value and self.isMultiValued() and \
 | |
|            (type(value) not in sutils.sequenceTypes):
 | |
|             value = [value]
 | |
|         return value
 | |
| 
 | |
|     def getStorableValue(self, obj, value):
 | |
|         languages = self.getAttribute(obj, 'languages')
 | |
|         if len(languages) == 1:
 | |
|             return self.getUnilingualStorableValue(obj, value)
 | |
|         # A multilingual value is stored as a dict whose keys are ISO 2-letters
 | |
|         # language codes and whose values are strings storing content in the
 | |
|         # language ~{s_language: s_content}~.
 | |
|         if not value: return
 | |
|         for lg in languages:
 | |
|             value[lg] = self.getUnilingualStorableValue(obj, value[lg])
 | |
|         return value
 | |
| 
 | |
|     def store(self, obj, value):
 | |
|         '''Stores p_value on p_obj for this field.'''
 | |
|         languages = self.getAttribute(obj, 'languages')
 | |
|         if (len(languages) > 1) and value and \
 | |
|            (not isinstance(value, dict) or (len(value) != len(languages))):
 | |
|             raise Exception('Multilingual field "%s" accepts a dict whose '\
 | |
|                             'keys are in field.languages and whose ' \
 | |
|                             'values are strings.' % self.name)
 | |
|         Field.store(self, obj, value)
 | |
| 
 | |
|     def storeFromAjax(self, obj):
 | |
|         '''Stores the new field value from an Ajax request, or do nothing if
 | |
|            the action was canceled.'''
 | |
|         rq = obj.REQUEST
 | |
|         if rq.get('cancel') == 'True': return
 | |
|         requestValue = rq['fieldContent']
 | |
|         # Remember previous value if the field is historized
 | |
|         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
 | |
| # ------------------------------------------------------------------------------
 | 
