# -*- coding: utf-8 -*-
## FormKit, Yet Another Webware Form processor
## Copyright (C) 2002, 2003 dAlchemy, Inc: www.dalchemy.com
##  
## This library is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser General Public
## License as published by the Free Software Foundation; either
## version 2.1 of the License, or (at your option) any later version.
##
## This library 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
## Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public
## License along with this library; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
## or visit www.gnu.org

# FormKit, A Webware Form processor
# Copyright (C) 2002, dAlchemy, Inc: www.dalchemy.com


# This module contains a base class for making forms

from types import DictionaryType, ListType, TupleType, StringType, UnicodeType
import sys

import BaseValidatorClasses



try:
    # try and get the default encoding for this machine/python installation
    DEFAULT_ENCODING = sys.getdefaultencoding()
except:
    DEFAULT_ENCODING = 'ascii'

class Form:
    ''' '''

    def __init__( self, action="", method="post", validators = [], languageStandard='XHTML', encoding=DEFAULT_ENCODING, decoding_strict=1 ):
        '''The Form constructor basically sets up a bunch of containers and switches.
        definition is currently not used, but may be a way to have a form build itself later.
        action is where the post goes (servlet, etc.) method is either get or post (defaults to post).
        languageStandard sets the markup language the generated tags are expected to
        conform to ( XHTML | HTML )

        encoding is the kind of ascii (encoded unicode or other character set) that we're expecting...
        assuming it's not just plain ascii already. For example, if you're expecting utf-8 from a form, set this to
        utf-8 when you make a new Form() instance.
        NB: To get this to work, your web server will have to send the html in the right encoding; if using apache,
        use AddDefaultCharset NNNN (see http://httpd.apache.org/docs/mod/core.html#adddefaultcharset)

        decoding_strict is a flag that tells the form to raise an exception when it gets confused decoding encoding strings...
        if this is turned off, it simply quietly passes through perhaps bogus data, and you'll get garbage characters from your
        fields, BUT it won't crash...
        '''
        ## @@ TODO:
        ## - consider making the uploadEncoding thing automatic somehow
        
        # a bunch of containers

        # storage
        self._formid = None             # stores a unique int so forms can find themselves on multi-form pages
        self._fields = [ ]              # list of field objects
        self._action = action           # standard form action: URL to send the data on a submit query
        self._validators = validators[:]    # form validators
                                            # note, python rules say that we should COPY the list, rather than just store
                                            # a reference, hence the [:] (Thanks to Jason Hildebrand)
        self._domID = ''

        # store the derived (or default) encoding
        self._encoding = encoding.lower()

        # input
        self._inputData = { }

        # status flags
        self._validated = None
        self._error = None              # this holds a validation error message (for form validators), if there is one
        
        # behavior switches:
        # ------------------

        # flag for strict or non-strict unicode decoding
        self._strictFlag = decoding_strict
    
        ## @@ TODO - consider implementing a switch to tell the form whether to include a cancel
        ##  button with the submit button a la
        ## self._showCancelButton = None

        # method of posting; defaults to POST
        if method.lower() == 'get':
            self._postWithGet = 1
        else:
            self._postWithGet = None


    ## Processing ##

    def process( self, inputData ):
        '''inputData is a dictionary, where keys are the field names.
        InputData is probably from post and querystring variables from a servlet,
        but so long as it's a dictionary with keys that correspond to fields,
        and values that fields can understand, it's kosher.'''

        # Assume everything is fine, to start with
        self._validated = 1
        self._error = None

        ## Note about _validated...
        ##  It just reflects self.error and self.errors, so it's arguably unneccessary,
        ##  BUT, a new form that hasn't been processed will contain no errors, and still not be "successful"

        # Try to process the input data through the field classes
        if inputData:
            assert type(inputData) == DictionaryType, "process inputData must be a dictionary"

            # transform inputData if necessary
            # NB: This is due to a bug/omission in HTTP... a browser sends in ASCII that is supposed to represent
            # unicode or other character-sets... the receiving process (cgi.FieldStorage for webware) has no way
            # to derive what that is, or even if it's supposed to be anything other than vanilla ascii...
            if self._encoding != 'ascii': # don't bother if it's ascii already
                for key in inputData.keys():
                    if type(inputData[key]) is type(['a','b','c']):
                        # if it's a list, we need to decode each member of the list
                        inputData[key] = map( self._decodeUnicode, inputData[key] )
                    elif type(inputData[key]) is type('string'):
                        inputData[key] = self._decodeUnicode( inputData[key] )
                    # it's also possible that there's a file in here, obviously, we don't want to decode that
                    # or, if we're not getting our inputData from a web browser, it might already be unicode,
                    # so just grab onto lists and plain strings

            # store it for possible later use
            self._inputData = inputData         

            # if we've got any data, then start processing it
            # by sending it off to the fields for their total amusement,
            # and the eventual validation and conversion of what's in inputData
            for field in self.fields():
                field._processRawValues( inputData )

        # Next, run any form validators, if applicable
        try:
            for v in self._validators:
                if v.validatePartialForm() or not self.errors():
                    v.validate( self.values() )
        except BaseValidatorClasses.InvalidField, msg:
            self._error = str(msg)

        if self._error or self.errors():
            self._validated = 0


    def _decodeUnicode( self, value ):
        '''private method that attempts to guess if a given value is in fact encoded unicode, and if so, decodes it.
        If it is NOT encoded unicode, or it fails, it simply returns the original value.'''
        if isinstance(value, UnicodeType):
            return value
        try:
            decodedValue = unicode( value, self._encoding )
            # now, our test is based on len()... if the original, possibly encoded value and the decoded value
            # have the same length, we can assume that no meaningful transformation took place...for example,
            # len('basic text') == len(u'basic text') but len('un\xc3\xadcode') != len(u'unícode')... in utf-8,
            # it takes two bytes to represent that accented "i". In something like latin-1, it doesn't, so it will
            # pass through, but that should be ok...
            if len(value) != len(decodedValue):
                # they're different, so...
                return decodedValue
            else:
                # they're probably the same, so...
                return value
        except UnicodeError:
            if self._strictFlag:
                raise UnicodeError, '''Failed trying to decode the string value '%s' from encoding '%s'. (Current sys.defaultencoding is '%s'). Perhaps the source of this value is using another encoding? (Check your apache/browser combo).''' % ( value, self._encoding, sys.getdefaultencoding() )
            else:
                # don't raise an error, just pass through the garbage
                return value


    ## Building, modifying ##

    def _setFormID( self, number ):
        '''This is cruft to allow us to re-associate posted fields with the correct form class instance.
        A servlet's forms are stored in a list, and each is given a numeric identifier. This identifier
        is printed to html in a hidden form field. When a post happens, FormKit can recover that field
        and thereby know which form to process.'''
        self._formid = number


    def seed( self, data={ } ):
        '''seed sends values (defaults) to the self.fields() field classes. data is a dictionary,
        with the keys being the names of the field class instances. These values are NOT validated
        or converted (but will be after they go through a submit-query and the resulting .process()).
        
        Multiple calls to this method are allowed, and encouraged. If a second (or any subsequent)
        call sends the same key as a previous call, it will stomp the values (as expected).

        SPECIAL NOTE! It may be obvious, but empty strings DO stomp former values. This is good;
        empty strings from a database SHOULD stomp "real" data from querystring or whatever.

        'None' values DO NOT stomp previous values (as dictated by class Field )
        '''
        # check for right dataType
        assert type(data) == DictionaryType, "If you pass in seed data, it must be a dictionary"

        # send it off to fields
        for field in self.fields():
            field._processDefaults( data )

    def addField( self, field ):
        '''adds a field object into the form'''
        self._fields.append( field )


    def removeAllFields( self ):
        '''removes all fields in the form object'''
        # this method is used when dynamically building forms
        self.reset()
        self._fields = []       # reset to an empty list

    def removeField( self, fieldName ):
        '''removes the field object called "fieldName" from the form; be careful with this method!'''

        # NOTE: there are practically no circumstances where you would use this method

        index = -1
        count = 0
        for field in self._fields:
            if field.name() == fieldName:
                index = count
            count += 1
        if index > 0:
            self._fields.pop(index)
        else:
            raise 'No field %s found' % fieldName

    def reset( self ):
        '''resets form to empty field values, clears errors, clears defaults, sets some flags back to off'''
        self._inputData = { }   # clear out the storage
        self._validated = self._converted = None # clear out the status flags
        for field in self.fields():
            field._reset()  # reset each field

    def addFormValidator( self, validator ):
        '''Adds a form validator'''
        self._validators.append(validator)      


    ## Set things ##

    def setDOMID( self, string='' ):
        '''For setting a DOM id (no internal consequences other than the id='<string>' appears in the form start tag)'''
        self._domID = string

    def setMethod( self, method ):
        '''set the post method: get or post'''
        method = method.lower()
        assert method in ('get','post'), "You can only use 'get' or 'post'"
        if method == 'get':
            self._postWithGet = 1

    def setAction( self, target ):
        '''changes the form action (URL)'''
        assert type(target) == StringType, "target should be a string"
        self._action = target


    def setEncoding( self, encoding=DEFAULT_ENCODING ):
        '''sets the encoding type for anticipated data (see __init__ docstrings)'''
        self._encoding = encoding


    def setTabOrder( self, order=None ):
        '''sets the tab order of the fields (using the tabindex parameter to html tags). Default is
        to set them in the order that they were added to the form; alternatively, pass in a list of integers
        that is the same length as the number of fields, or simply poke at each field with field.setTabIndex()'''
        fields = self.fields()

        if order:
            assert len(order) == len(fields), "Any passed in order sequence must have the same number of members as the number of fields in the form"
            for i in range(len(order)):
                fields[i].setTabIndex( order[i] )

        else:
            # just set them in linear order
            for i in range(len(fields)):
                fields[i].setTabIndex( i+1 )



    ## Access ##

    def isSuccessful( self ):
        # reports whether the form valid and converted?
        return self._validated

    def __getattr__( self, fieldName ):
        '''convenience method that allows us to refer to fields of form
        with .thing notation. Just returns the field class instance itself.'''
        return self.field( fieldName )

    def field( self, fieldName ):
        '''returns the asked for field class instance.'''
        for field in self.fields():
            if field.name() == fieldName:
                return field
        # if we made it through that, then there is no match...
        return None

    def fields( self ):
        '''returns all of the fields as a list of field class instances; this list
        preserves the order in which the fields were added'''
        return self._fields

    def defaults( self ):
        '''returns all of the default values for all fields as a dictionary'''
        defs = {}
        # loop through the fields, adding their key, value to the dictionary
        for field in self.fields():
            defs.update( { field.name() : field.default() } )
        return defs

    def rawValues( self ):
        '''returns dict raw (un-validated, un-converted, etc.) values from the form'''
        return self._inputData

    def value( self, fieldName ):
        '''returns the VALUE of the specified field class instance.'''
        field = self.field( fieldName )
        return unicode(field.value())

    def values( self ):
        '''returns all of the values for all fields as a dictionary'''
        # make the dictionary
        values = {}
        # loop through the fields, adding their key, value to the dictionary
        for field in self.fields():
            values.update( { field.name() : field.value() } )
        return values

    def tag( self, fieldName ):
        '''returns the html <input> tag string of the asked-for field class instance.'''
        field = self.field( fieldName )
        return field.tag()

    def error(self):
        '''Form-wide error message access method (returns None if no error)'''
        if self._error:
            return self._error
        else:
            return None

    def errors( self ):
        '''returns all of the error strings for all fields as a dictionary.
        The key is the field name, the error is the value.'''
        # make the dictionary
        errors = {}
        # loop through the fields, adding their key, value to the dictionary
        for field in self.fields():
            if field.error():
                errors.update( { field.name() : field.error() } )
        return errors


    ## Output ##

    def startTags( self ):
        '''returns the html that's required to start a form; basically an opening <form> tag with attributes'''
        # determine what kind of post we want
        if self._postWithGet:
            method = "get"
        else:
            method = "post"
            
        # check with the fields to see if we have to use multipart/binary encoding
        encoding = ''
        for field in self._fields:
            try:
                temp = field._requireMultipartEncoding()
                encoding = ''' enctype="multipart/form-data"'''
            except AttributeError:
                pass

        # here it is. Action is where the form is posted (defaults to the same servlet that it's on)
        if self._domID:
            return """<form action="%s"%s method="%s" name="%s" id="%s">""" % ( self._action, encoding, method, self._domID, self._domID )
        else:
            return """<form action="%s"%s method="%s">""" % ( self._action, encoding, method )


    def endTags( self ):
        '''returns the html that's required to end a form; basically a closing </form> tag, but 
        including a hidden field to establish the form's name (for multiple forms)'''
        
        return """<input type="hidden" name="__formID__" value="%s" />\n</form>""" % self._formid


    def dump( self ):
        '''returns a big string of the html required to SIMPLY paint the form for quick-and-dirty pages and for debugging'''

        # is NOT an attempt at fancy formatting or templating

        tmpStr = self.startTags() + "\n"    # start with the start of the form...
        tmpStr += '''<table cellpadding="10">\n'''
        for field in self.fields():
            tmpStr += '''<tr>\n\t<td valign="top" align="right">%s</td>\n''' % field.label()
            tmpStr += '''\t<td>'''
            if field.error():
                tmpStr += '''<span style="color:red;">%s</span><br /> ''' % field.error()
            tmpStr += field.tag()           # add in the fields
            tmpStr += "</td>\n</tr>\n"              # separate the buggers
        tmpStr += '''</table>'''
        tmpStr += self.endTags() + "\n"     # and the end tags
        return tmpStr