Exemplo n.º 1
0
 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)
Exemplo n.º 2
0
 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)
Exemplo n.º 3
0
class String(Field):
    # Javascript files sometimes required by this type
    jsFiles = {
        'edit': ('ckeditor/ckeditor.js', ),
        'view': ('ckeditor/ckeditor.js', )
    }

    # Some predefined regular expressions that may be used as validators
    c = re.compile
    EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \
              '[a-zA-Z][a-zA-Z\.]*[a-zA-Z]')
    ALPHANUMERIC = c('[\w-]+')
    URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \
            '(([0-9]{1,5})?\/.*)?')

    pxView = Px('''
     <x var="fmt=field.format; isUrl=field.isUrl;
             mayAjaxEdit=not showChanges and field.inlineEdit and \
                         zobj.mayEdit(field.writePermission)">
      <x if="fmt in (0, 3)">
       <ul if="value and isMultiple">
        <li for="sv in value"><i>::sv</i></li>
       </ul>
       <x if="value and not isMultiple">
        <!-- A password -->
        <x if="fmt == 3">********</x>
        <!-- A URL -->
        <a if="(fmt != 3) and isUrl" target="_blank" href=":value">:value</a>
        <!-- Any other value -->
        <x if="(fmt != 3) and not isUrl">::value</x>
       </x>
      </x>
      <!-- Unformatted text -->
      <x if="value and (fmt == 1)">::zobj.formatText(value, format='html')</x>
      <!-- XHTML text -->
      <x if="fmt == 2">
       <div if="not mayAjaxEdit" class="xhtml">::value or '-'</div>
       <div if="mayAjaxEdit" class="xhtml" contenteditable="true"
            id=":'%s_%s_ck' % (zobj.id, name)">::value or '-'</div>
       <script if="mayAjaxEdit">::field.getJsInlineInit(zobj)</script>
      </x>
      <span if="not value and (fmt != 2)" class="smaller">-</span>
      <input type="hidden" if="masterCss" class=":masterCss" value=":rawValue"
             name=":name" id=":name"/>
     </x>''')

    pxEdit = Px('''
     <x var="fmt=field.format;
             isOneLine=fmt in (0,3,4)">
      <select if="field.isSelect"
              var2="possibleValues=field.getPossibleValues(zobj, \
                      withTranslations=True, withBlankValue=True)"
              name=":name" id=":name" class=":masterCss"
              multiple=":isMultiple and 'multiple' or ''"
              onchange=":field.getOnChange(zobj, layoutType)"
              size=":isMultiple and field.height or 1">
       <option for="val in possibleValues" value=":val[0]"
               selected=":field.isSelected(zobj, name, val[0], rawValue)"
               title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
      </select>
      <x if="isOneLine and not field.isSelect"
         var2="placeholder=field.getAttribute(obj, 'placeholder') or ''">
       <input id=":name" name=":name" size=":field.width"
              maxlength=":field.maxChars" placeholder=":placeholder"
              value=":inRequest and requestValue or value"
              style=":'text-transform:%s' % field.transform"
              type=":(fmt == 3) and 'password' or 'text'"/>
       <!-- Display a captcha if required -->
       <span if="fmt == 4">:_('captcha_text', \
                              mapping=field.getCaptchaChallenge(req.SESSION))
       </span>
      </x>
      <x if="fmt in (1,2)">
       <textarea id=":name" name=":name" cols=":field.width"
                 class=":(fmt == 2) and ('rich_%s' % name) or ''"
                 style=":'text-transform:%s' % field.transform"
                 rows=":field.height">:inRequest and requestValue or value
       </textarea>
       <script if="fmt == 2"
               type="text/javascript">::field.getJsInit(zobj)</script>
      </x>
     </x>''')

    pxCell = Px('''
     <x var="multipleValues=value and isMultiple">
      <x if="multipleValues">:', '.join(value)</x>
      <x if="not multipleValues">:field.pxView</x>
     </x>''')

    pxSearch = Px('''
     <!-- Show a simple search field for most String fields -->
     <input if="not field.isSelect" type="text" maxlength=":field.maxChars"
            size=":field.swidth" value=":field.sdefault"
            name=":'%s*string-%s' % (widgetName, field.transform)"
            style=":'text-transform:%s' % field.transform"/>
     <!-- Show a multi-selection box for fields whose validator defines a list
          of values, with a "AND/OR" checkbox. -->
     <x if="field.isSelect">
      <!-- The "and" / "or" radio buttons -->
      <x if="field.multiplicity[1] != 1"
         var2="operName='o_%s' % name;
              orName='%s_or' % operName;
              andName='%s_and' % operName">
       <input type="radio" name=":operName" id=":orName" checked="checked"
              value="or"/>
       <label lfor=":orName">:_('search_or')</label>
       <input type="radio" name=":operName" id=":andName" value="and"/>
       <label lfor=":andName">:_('search_and')</label><br/>
      </x>
      <!-- The list of values -->
      <select var="preSelected=field.sdefault"
              name=":widgetName" size=":field.sheight" multiple="multiple"
              onchange=":field.getOnChange(ztool, 'search', className)">
       <option for="v in field.getPossibleValues(ztool, withTranslations=True,\
                                     withBlankValue=False, className=className)"
               selected=":v[0] in preSelected" value=":v[0]"
               title=":v[1]">:ztool.truncateValue(v[1], field.swidth)</option>
      </select>
     </x><br/>''')

    # Some predefined functions that may also be used as validators
    @staticmethod
    def _MODULO_97(obj, value, complement=False):
        '''p_value must be a string representing a number, like a bank account.
           this function checks that the 2 last digits are the result of
           computing the modulo 97 of the previous digits. Any non-digit
           character is ignored. If p_complement is True, it does compute the
           complement of modulo 97 instead of modulo 97. p_obj is not used;
           it will be given by the Appy validation machinery, so it must be
           specified as parameter. The function returns True if the check is
           successful.'''
        if not value: return True
        # First, remove any non-digit char
        v = ''
        for c in value:
            if digit.match(c): v += c
        # There must be at least 3 digits for performing the check
        if len(v) < 3: return False
        # Separate the real number from the check digits
        number = int(v[:-2])
        checkNumber = int(v[-2:])
        # Perform the check
        if complement:
            return (97 - (number % 97)) == checkNumber
        else:
            # The check number can't be 0. In this case, we force it to be 97.
            # This is the way Belgian bank account numbers work. I hope this
            # behaviour is general enough to be implemented here.
            mod97 = (number % 97)
            if mod97 == 0: return checkNumber == 97
            else: return checkNumber == mod97

    @staticmethod
    def MODULO_97(obj, value):
        return String._MODULO_97(obj, value)

    @staticmethod
    def MODULO_97_COMPLEMENT(obj, value):
        return String._MODULO_97(obj, value, True)

    BELGIAN_ENTERPRISE_NUMBER = MODULO_97_COMPLEMENT

    @staticmethod
    def BELGIAN_NISS(obj, value):
        '''Returns True if the NISS in p_value is valid.'''
        if not value: return True
        # Remove any non-digit from nrn
        niss = sutils.keepDigits(value)
        # NISS must be made of 11 numbers
        if len(niss) != 11: return False
        # When NRN begins with 0 or 1, it must be prefixed with number "2" for
        # checking the modulo 97 complement.
        nissForModulo = niss
        if niss.startswith('0') or niss.startswith('1'):
            nissForModulo = '2' + niss
        # Check modulo 97 complement
        return String.MODULO_97_COMPLEMENT(None, nissForModulo)

    @staticmethod
    def IBAN(obj, value):
        '''Checks that p_value corresponds to a valid IBAN number. IBAN stands
           for International Bank Account Number (ISO 13616). If the number is
           valid, the method returns True.'''
        if not value: return True
        # First, remove any non-digit or non-letter char
        v = ''
        for c in value:
            if alpha.match(c): v += c
        # Maximum size is 34 chars
        if (len(v) < 8) or (len(v) > 34): return False
        # 2 first chars must be a valid country code
        if not countries.exists(v[:2].upper()): return False
        # 2 next chars are a control code whose value must be between 0 and 96.
        try:
            code = int(v[2:4])
            if (code < 0) or (code > 96): return False
        except ValueError:
            return False
        # Perform the checksum
        vv = v[4:] + v[:4]  # Put the 4 first chars at the end.
        nv = ''
        for c in vv:
            # Convert each letter into a number (A=10, B=11, etc)
            # Ascii code for a is 65, so A=10 if we perform "minus 55"
            if letter.match(c): nv += str(ord(c.upper()) - 55)
            else: nv += c
        return int(nv) % 97 == 1

    @staticmethod
    def BIC(obj, value):
        '''Checks that p_value corresponds to a valid BIC number. BIC stands
           for Bank Identifier Code (ISO 9362). If the number is valid, the
           method returns True.'''
        if not value: return True
        # BIC number must be 8 or 11 chars
        if len(value) not in (8, 11): return False
        # 4 first chars, representing bank name, must be letters
        for c in value[:4]:
            if not letter.match(c): return False
        # 2 next chars must be a valid country code
        if not countries.exists(value[4:6].upper()): return False
        # Last chars represent some location within a country (a city, a
        # province...). They can only be letters or figures.
        for c in value[6:]:
            if not alpha.match(c): return False
        return True

    # Possible values for "format"
    LINE = 0
    TEXT = 1
    XHTML = 2
    PASSWORD = 3
    CAPTCHA = 4

    def __init__(self,
                 validator=None,
                 multiplicity=(0, 1),
                 default=None,
                 format=LINE,
                 show=True,
                 page='main',
                 group=None,
                 layouts=None,
                 move=0,
                 indexed=False,
                 searchable=False,
                 specificReadPermission=False,
                 specificWritePermission=False,
                 width=None,
                 height=None,
                 maxChars=None,
                 colspan=1,
                 master=None,
                 masterValue=None,
                 focus=False,
                 historized=False,
                 mapping=None,
                 label=None,
                 sdefault='',
                 scolspan=1,
                 swidth=None,
                 sheight=None,
                 persist=True,
                 transform='none',
                 placeholder=None,
                 styles=('p', 'h1', 'h2', 'h3', 'h4'),
                 allowImageUpload=True,
                 spellcheck=False,
                 contentLanguage=None,
                 inlineEdit=False):
        # According to format, the widget will be different: input field,
        # textarea, inline editor... Note that there can be only one String
        # field of format CAPTCHA by page, because the captcha challenge is
        # stored in the session at some global key.
        self.format = format
        self.isUrl = validator == String.URL
        # When format is XHTML, the list of styles that the user will be able to
        # select in the styles dropdown is defined hereafter.
        self.styles = styles
        # When format is XHTML, do we allow the user to upload images in it ?
        self.allowImageUpload = allowImageUpload
        # When format is XHTML, do we run the CK spellchecker ?
        self.spellcheck = spellcheck
        # What is the language of field content?
        self.contentLanguage = contentLanguage
        # When format in XHTML, can the field be inline-edited (ckeditor)?
        self.inlineEdit = inlineEdit
        # The following field has a direct impact on the text entered by the
        # user. It applies a transformation on it, exactly as does the CSS
        # "text-transform" property. Allowed values are those allowed for the
        # CSS property: "none" (default), "uppercase", "capitalize" or
        # "lowercase".
        self.transform = transform
        # "placeholder", similar to the HTML attribute of the same name, allows
        # to specify a short hint that describes the expected value of the input
        # field. It is shown inside the input field and disappears as soon as
        # the user encodes something in it. Works only for strings whose format
        # is LINE. Does not work with IE < 10. You can specify a method here,
        # that can, for example, return an internationalized value.
        self.placeholder = placeholder
        Field.__init__(self, validator, multiplicity, default, show, page,
                       group, layouts, move, indexed, searchable,
                       specificReadPermission, specificWritePermission, width,
                       height, maxChars, colspan, master, masterValue, focus,
                       historized, mapping, label, sdefault, scolspan, swidth,
                       sheight, persist)
        self.isSelect = self.isSelection()
        # If self.isSelect, self.sdefault must be a list of value(s).
        if self.isSelect and not sdefault:
            self.sdefault = []
        # Default width, height and maxChars vary according to String format
        if width == None:
            if format == String.TEXT: self.width = 60
            # This width corresponds to the standard width of an Appy page.
            if format == String.XHTML: self.width = None
            else: self.width = 30
        if height == None:
            if format == String.TEXT: self.height = 5
            elif self.isSelect: self.height = 4
            else: self.height = 1
        if maxChars == None:
            if self.isSelect: pass
            elif format == String.LINE: self.maxChars = 256
            elif format == String.TEXT: self.maxChars = 9999
            elif format == String.XHTML: self.maxChars = 99999
            elif format == String.PASSWORD: self.maxChars = 20
        self.filterable = self.indexed and (self.format == String.LINE) and \
                          not self.isSelect
        self.swidth = self.swidth or self.width
        self.sheight = self.sheight or self.height

    def isSelection(self):
        '''Does the validator of this type definition define a list of values
           into which the user must select one or more values?'''
        res = True
        if type(self.validator) in (list, tuple):
            for elem in self.validator:
                if not isinstance(elem, basestring):
                    res = False
                    break
        else:
            if not isinstance(self.validator, Selection):
                res = False
        return res

    def getDefaultLayouts(self):
        '''Returns the default layouts for this type. Default layouts can vary
           acccording to format, multiplicity or history.'''
        if self.format == String.TEXT:
            return {'view': 'l-f', 'edit': 'lrv-d-f'}
        elif self.format == String.XHTML:
            if self.historized:
                # self.historized can be a method or a boolean. If it is a
                # method, it means that under some condition, historization will
                # be enabled. So we come here also in this case.
                view = 'lc-f'
            else:
                view = 'l-f'
            return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'}
        elif self.isMultiValued():
            return {'view': 'l-f', 'edit': 'lrv-f'}

    def getValue(self, obj):
        # Cheat if this field represents p_obj's state
        if self.name == 'state': return obj.State()
        value = Field.getValue(self, obj)
        if not value:
            if self.isMultiValued(): return emptyTuple
            else: return value
        if isinstance(value, basestring) and self.isMultiValued():
            value = [value]
        elif isinstance(value, tuple):
            value = list(value)
        return value

    def store(self, obj, value):
        '''When the value is XHTML, we perform some cleanup.'''
        if not self.persist: return
        if (self.format == String.XHTML) and value:
            # When image upload is allowed, ckeditor inserts some "style" attrs
            # (ie for image size when images are resized). So in this case we
            # can't remove style-related information.
            try:
                value = XhtmlCleaner(keepStyles=False).clean(value)
            except XhtmlCleaner.Error, e:
                # Errors while parsing p_value can't prevent the user from
                # storing it.
                obj.log('Unparsable XHTML content in field "%s".' % self.name,
                        type='warning')
        Field.store(self, obj, value)