#Validator
#============
#
#This module is not aimed to replace the newforms, but I would like manually write html code,
#and just need a pure validate module, so I write this, and many things may be similar with
#newforms. So if you like me would only need a pure validator module, you can use it.
#
#And it has some different features from newforms:
#    
#1. Support validator_list parameter, so you could use it just like the old manipuator class
#2. Supply easy method, such as `validate_and_save()`, so you can pass a request object, and 
#   get a tuple result `(flag, obj_or_error)`, if the `flag` is `True`, then the next value 
#   is an object; and if the `flag` is `False`, then the next value is error message.
#3. Each field has a `validate_and_get` method, and it'll validate first and then return the 
#   result, maybe an object or error message. Just like above.
#4. SplitDateTimeField is somewhat different from the newforms. For example::
#    
#    c = SplitDateTimeField('date', 'time')
#    print c.validate_and_get({'date':'2006/11/30', 'time':'12:13'})
#
#    So the first parameter is DateField's field_name, and the second parameter is TimeField's
#    field_name.
#5. Add yyyy/mm/dd date format support
#6. Support default value of a field. You can add a default value for a field, if this field 
#   is not required, and the value is *empty*, Validator will return the default value.
#    
#This module is new, so many things could be changed.
#
#Version: 0.2
#Author: limodou<limodou AT gmail.com>
#Update:
#    * 2007/02/17 0.1
#    * 2007/02/26 0.2 Fixed add_validator bug


import datetime
import time
import re
import copy
from django.utils.datastructures import MultiValueDict
from django.utils.datastructures import SortedDict

__all__ = (
    'Field', 'CharField', 'IntegerField',
    'DEFAULT_DATE_INPUT_FORMATS', 'DateField',
    'DEFAULT_TIME_INPUT_FORMATS', 'TimeField',
    'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField',
    'RegexField', 'EmailField', 'URLField', 'BooleanField',
    'ComboField', 'SplitDateTimeField',
    'ValidationError', 'Validator',
    'isChoices', 'isMultipleChoices',
)

try:
    set
except:
    from sets import Set as set
    
#from django.utils.translation import gettext as _
def _(v):
    return v

class SortedDictFromList(SortedDict):
    "A dictionary that keeps its keys in the order in which they're inserted."
    # This is different than django.utils.datastructures.SortedDict, because
    # this takes a list/tuple as the argument to __init__().
    def __init__(self, data=None):
        if data is None: data = []
        self.keyOrder = [d[0] for d in data]
        dict.__init__(self, dict(data))

    def copy(self):
        l = []
        for k, v in self.items():
            l.append((k, copy.copy(v)))
            v.validator_list = copy.copy(v.validator_list)
        return SortedDictFromList(l)

class ValidationError(Exception):
    def __init__(self, message):
        self.message = message
        
    def __str__(self):
        return str(self.message)

class Field(object):
    
    creation_counter = 0
    
    def __init__(self, field_name=None, required=True, multi_value=False, default=None, validator_list=None):
        self.field_name = field_name
        self.required = required
        self.default = default
        self.multi_value = multi_value
        self.default_validator_list = []
        if validator_list:
            self.validator_list = validator_list
        else:
            self.validator_list = []
            
        self.creation_counter = Field.creation_counter
        Field.creation_counter += 1
            
    def get_data_from_datadict(self, all_data):
        if not self.multi_value or not isinstance(all_data, MultiValueDict):
            return all_data.get(self.field_name, None)
        else:
            return all_data.getlist(self.field_name)
        
    def validate_and_get(self, data, all_data=None):
        if not data:
            if not self.required:
                if self.default is not None:
                    return True, self.default
                else:
                    return True, None
            else:
                return False, _(u'This field is required.')
        try:
            if isinstance(data, list):
                v = []
                for i in data:
                    v.append(self.convert(i, all_data))
                data = v
            else:
                data = self.convert(data, all_data)
        except ValidationError, e:
            return False, e.message
        except:
            return False, _(u'Convert data error.')
        try:
            for v in self.default_validator_list + self.validator_list:
                v(data, all_data)
        except ValidationError, e:
            return False, e.message
        return True, data
                
    def convert(self, data, all_data=None):
        raise Exception, 'Not implement yet!'
    
    def add_validator(self, validator):
        self.validator_list.append(validator)
    
class ValidatorMetaclass(type):
    """
    Metaclass that converts Field attributes to a dictionary called
    """
    def __new__(cls, name, bases, attrs):
        fields = []
        for field_name, obj in attrs.items():
            if isinstance(obj, Field):
                obj = attrs.pop(field_name)
                obj.field_name = field_name
                fields.append((field_name, obj))
        fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))

        # If this class is subclassing another Form, add that Form's fields.
        # Note that we loop over the bases in *reverse*. This is necessary in
        # order to preserve the correct order of fields.
        for base in bases[::-1]:
            if hasattr(base, 'base_fields'):
                fields = base.base_fields.items() + fields

        attrs['base_fields'] = SortedDictFromList(fields)
        
        old_init = attrs.get('__init__', None)
        def _f(self, *args, **kwargs):
            self.fields = self.base_fields.copy()
            if old_init:
                old_init(self, *args, **kwargs)
        attrs['__init__'] = _f
                
        return type.__new__(cls, name, bases, attrs)
    
class Validator(object):
    
    __metaclass__ = ValidatorMetaclass
    
    def validate(self, request_or_data):
        if hasattr(request_or_data, 'POST'):
            all_data = request_or_data.POST.copy()
            all_data.update(request_or_data.FILES)
        else:
            all_data = request_or_data
        if all_data:
            errors = {}
            new_data = {}
            
            #gather all fields
            for field_name, field in self.fields.items():
                new_data[field_name] = field.get_data_from_datadict(all_data)
            
            #validate and gather the result
            result = {}
            for field_name, field in self.fields.items():
                flag, value = field.validate_and_get(new_data[field_name], new_data)
                if not flag:
                    if isinstance(value, dict):
                        errors.update(value)
                    else:
                        errors[field_name] = value
                else:
                    result[field_name] = value
                    
            if flag:
                #validate global        
                try:
                    self.full_validate(result, new_data)
                except ValidationError, e:
                    errors['_'] = e.message
            
            #if there is errors, then result (False, error_messages)
            if errors or not flag:
                return False, errors
            
            return True, result
            
        else:
            return False, {'_':_(u'There is not data posted.')}
        
    def validate_and_save(self, request):
        flag, result = self.validate(request)
        if flag:
            #then try do the save
            try:
                obj = self.save(result)
            except:
                import traceback
                traceback.print_exc()
                return False, {'_':_(u'Saving object error.')}
            
            return True, obj
        else:
            return flag, result

    def save(self, data):
        pass
    
    def full_validate(self, new_data, all_data):
        pass

class CharField(Field):
    def __init__(self, max_length=None, min_length=None, *args, **kwargs):
        super(CharField, self).__init__(*args, **kwargs)
        self.max_length, self.min_length = max_length, min_length
        self.default_validator_list.append(self.validate_length)
        
    def validate_length(self, data, all_data=None):
        if self.max_length is not None and len(data) > self.max_length:
            raise ValidationError, _(u'Ensure this value has at most %d characters.') % self.max_length
        if self.min_length is not None and len(data) < self.min_length:
            raise ValidationError, _(u'Ensure this value has at least %d characters.') % self.min_length
        
    def convert(self, data, all_data=None):
        return str(data)
    
class IntegerField(Field):
    def __init__(self, max_value=None, min_value=None, *args, **kwargs):
        super(IntegerField, self).__init__(*args, **kwargs)
        self.max_value, self.min_value = max_value, min_value
        self.default_validator_list.append(self.validate_value)
        
    def validate_value(self, data, all_data=None):
        if self.max_value is not None and data > self.max_value:
            raise ValidationError, _(u'Ensure this value is less than or equal to %s.') % self.max_value
        if self.min_value is not None and data < self.min_value:
            raise ValidationError, _(u'Ensure this value is greater than or equal to %s.') % self.min_value
      
    def convert(self, data, all_data=None):
        try:
            return int(data)
        except (ValueError, TypeError):
            raise ValidationError, _(u'Enter a whole number.')

DEFAULT_DATE_INPUT_FORMATS = (
    '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', '%Y/%m/%d',  # '2006-10-25', '10/25/2006', '10/25/06'
    '%b %d %Y', '%b %d, %Y',            # 'Oct 25 2006', 'Oct 25, 2006'
    '%d %b %Y', '%d %b, %Y',            # '25 Oct 2006', '25 Oct, 2006'
    '%B %d %Y', '%B %d, %Y',            # 'October 25 2006', 'October 25, 2006'
    '%d %B %Y', '%d %B, %Y',            # '25 October 2006', '25 October, 2006'
)

class DateField(Field):
    def __init__(self, input_formats=None, *args, **kwargs):
        super(DateField, self).__init__(*args, **kwargs)
        self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS
        
    def convert(self, data, all_data=None):
        for format in self.input_formats:
            try:
                return datetime.date(*time.strptime(data, format)[:3])
            except ValueError:
                continue
        raise ValidationError, _(u'Date format is not right.')

DEFAULT_TIME_INPUT_FORMATS = (
    '%H:%M:%S',     # '14:30:59'
    '%H:%M',        # '14:30'
)

class TimeField(Field):
    def __init__(self, input_formats=None, *args, **kwargs):
        super(TimeField, self).__init__(*args, **kwargs)
        self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS
        
    def convert(self, data, all_data=None):
        for format in self.input_formats:
            try:
                return datetime.time(*time.strptime(data, format)[3:6])
            except ValueError:
                continue
        raise ValidationError, _(u'Time format is not right.')

DEFAULT_DATETIME_INPUT_FORMATS = (
    '%Y-%m-%d %H:%M:%S',     # '2006-10-25 14:30:59'
    '%Y-%m-%d %H:%M',        # '2006-10-25 14:30'
    '%Y-%m-%d',              # '2006-10-25'
    '%Y/%m/%d %H:%M:%S',     # '2006/10/25 14:30:59'
    '%Y/%m/%d %H:%M',        # '2006/10/25 14:30'
    '%Y/%m/%d ',             # '2006/10/25 '
    '%m/%d/%Y %H:%M:%S',     # '10/25/2006 14:30:59'
    '%m/%d/%Y %H:%M',        # '10/25/2006 14:30'
    '%m/%d/%Y',              # '10/25/2006'
    '%m/%d/%y %H:%M:%S',     # '10/25/06 14:30:59'
    '%m/%d/%y %H:%M',        # '10/25/06 14:30'
    '%m/%d/%y',              # '10/25/06'
)

class DateTimeField(Field):
    def __init__(self, input_formats=None, *args, **kwargs):
        super(DateTimeField, self).__init__(*args, **kwargs)
        self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS
        
    def convert(self, data, all_data=None):
        for format in self.input_formats:
            try:
                return datetime.datetime(*time.strptime(data, format)[:6])
            except ValueError:
                continue
        raise ValidationError, _(u'Time format is not right.')

class RegexField(CharField):
    def __init__(self, regex, error_message=None, *args, **kwargs):
        super(RegexField, self).__init__(*args, **kwargs)
        if isinstance(regex, basestring):
            regex = re.compile(regex)
        self.regex = regex
        self.default_validator_list.append(self.validate_string)
        self.error_message = error_message or _(u'Enter a valid value.')
        
    def validate_string(self, data, all_data=None):
        if not self.regex.match(data):
            raise ValidationError, self.error_message

email_re = re.compile(
    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
    r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain

class EmailField(RegexField):
    def __init__(self, *args, **kwargs):
        super(EmailField, self).__init__(email_re, _(u'Enter a valid e-mail address.'), *args, **kwargs)
        
url_re = re.compile(
    r'^https?://' # http:// or https://
    r'(?:[A-Z0-9-]+\.)+[A-Z]{2,6}' # domain
    r'(?::\d+)?' # optional port
    r'(?:/?|/\S+)$', re.IGNORECASE)

class URLField(RegexField):
    def __init__(self, verify_exists=False, validator_user_agent='User Agent', *args, **kwargs):
        super(URLField, self).__init__(url_re, _(u'Enter a valid URL.'), *args, **kwargs)
        self.verify_exists = verify_exists
        self.user_agent = validator_user_agent
        self.default_validator_list.append(self.validate_url)

    def validate_url(self, data, all_data=None):
        if self.verify_exists:
            import urllib2
            headers = {
                "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
                "Accept-Language": "en-us,en;q=0.5",
                "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
                "Connection": "close",
                "User-Agent": self.user_agent,
            }
            try:
                req = urllib2.Request(data, None, headers)
                u = urllib2.urlopen(req)
            except ValueError:
                raise ValidationError, _(u'Enter a valid URL.')
            except: # urllib2.URLError, httplib.InvalidURL, etc.
                raise ValidationError, _(u'This URL appears to be a broken link.')
        return data
    
class BooleanField(Field):
    def __init__(self):
        super(BooleanField, self).__init__()
        
    def convert(self, data, all_data=None):
        if isinstance(data, basestring):
            if data.lower() in ('on', 'true', 'yes', 'ok'):
                return True
            elif data.lower() in ('off', 'false', 'no', 'cancel'):
                return False
            else:
                raise ValidationError, _(u'Need a boolean value.')
        else:
            try:
                return bool(data)
            except:
                raise ValidationError, _(u'Need a boolean value.')
            
class ComboField(Field):
    def __init__(self, fields=()):
        super(ComboField, self).__init__()
        self.fields = fields

    def get_data_from_datadict(self, all_data):
        data = {}
        for field in self.fields:
            data[field.field_name] = field.get_data_from_datadict(all_data)
        return data
            
    def validate_and_get(self, data, all_data=None):
        errors = {}
        result = {}
        for field in self.fields:
            flag, value = field.validate_and_get(data[field.field_name], data)
            if not flag:
                if isinstance(value, dict):
                    errors.update(value)
                else:
                    errors[field.field_name] = value
            else:
                result[field.field_name] = value
        
        if not errors or not flag:
            try:
                data = self.convert(result, all_data)
            except ValidationError, e:
                return False, e.message
            except:
                return False, _(u'Convert data error.')
            
        return True, data
                
    def convert(self, data, all_data=None):
        raise Exception, 'Not implement yet!'
    
class SplitDateTimeField(ComboField):
    def __init__(self, date_fieldname, time_fieldname):
        self.date_fieldname = date_fieldname
        self.time_fieldname = time_fieldname
        fields = [DateField(field_name=date_fieldname), TimeField(field_name=time_fieldname)]
        super(SplitDateTimeField, self).__init__(fields)
        
    def convert(self, data, all_data=None):
        try:
            return datetime.datetime.combine(data[self.date_fieldname], data[self.time_fieldname])
        except:
            raise ValidationError, _(u'Time format is not right.')

def _get_choices_keys(choices):
    if isinstance(choices, dict):
        keys = set(choices.keys())
    elif isinstance(choices, (list, tuple)):
        keys = set([])
        for v in choices:
            if isinstance(v, (list, tuple)):
                keys.add(v[0])
            else:
                keys.add(v)
    else:
        raise ValidationError, _(u'Choices need a dict, tuple or list data.')
    return keys

def isChoices(choices):
    '''
    choices should be a list or a tuple, e.g. [1,2,3]
    '''
    def _f(data, all_data=None):
        if data not in _get_choices_keys(choices):
            raise ValidationError, _(u'Select a valid choice. That choice is not one of the available choices.')
    return _f

def isMultipleChoices(choices):
    '''
    choices should be a list or a tuple, e.g. [1,2,3]
    '''
    def _f(data, all_data=None):
        data = set(data)
        if data - _get_choices_keys(choices):
            raise ValidationError, _(u'Select a valid choice. That choice is not one of the available choices.')
    return _f
            
if __name__ == '__main__':
    c = CharField()
    print c.validate_and_get('abc')
    print c.validate_and_get('')
    print c.validate_and_get(None)
    c = CharField(max_length=10)
    print c.validate_and_get('abcdefghijklmn')
    c = CharField(required=False)
    print c.validate_and_get('')
    print c.validate_and_get('abc')
    c = IntegerField()
    print c.validate_and_get('abc')
    print c.validate_and_get('123')
    print c.validate_and_get(123)
    c = DateField()
    print c.validate_and_get('abc')
    print c.validate_and_get('2007/02/16')
    c = TimeField()
    print c.validate_and_get('abc')
    print c.validate_and_get('01:22:34')
    print c.validate_and_get('01:22')
    c = DateTimeField()
    print c.validate_and_get('abc')
    print c.validate_and_get('2007/02/13 01:22:34')
    print c.validate_and_get('2006-01-01 01:22')
    c = RegexField('\d+')
    print c.validate_and_get('abc')
    print c.validate_and_get('123')
    c = EmailField()
    print c.validate_and_get('abc')
    print c.validate_and_get('abc@gmail.com')
    c = URLField()
    print c.validate_and_get('abc')
    print c.validate_and_get('http://sina.com.cn')
    c = BooleanField()
    print c.validate_and_get('abc')
    print c.validate_and_get('True')
    c = IntegerField(validator_list=[isChoices([1,2,3])])
    print c.validate_and_get('a')
    print c.validate_and_get('1')
    print c.validate_and_get('4')
    c = IntegerField(validator_list=[isMultipleChoices([1,2,3])])
    print c.validate_and_get(['a'])
    print c.validate_and_get(['1', '2'])
    print c.validate_and_get(['1', '4'])
    c = SplitDateTimeField('date', 'time')
    print c.validate_and_get({'date':'2006/11/30', 'time':'12:13'})
    
    data = {'username':'limodou', 'email':'abc@gmail.com', 'age':'123', 'password':'limodou'}
    
    def validate_username(data, all_data):
        print 'validate_username', data
        if data != 'limodou':
            raise ValidationError, 'Username must be "limodou"'
    
    class TV(Validator):
        username = CharField(validator_list=[validate_username])
        email = EmailField()
        age = IntegerField()
        password = CharField()
        
        def __init__(self):
            self.fields['username'].add_validator(self.AnotherValidator)
        
        def full_validate(self, data, all_data):
            if not data.has_key('password'):
                raise ValidationError, _(u'Need password')
            
        def save(self, data):
            print 'ok'
            
        def AnotherValidator(self, data, all_data):
            print 'AnotherValidator', data
            return
        
    t = TV()
    print t.validate_and_save(data)
    t = TV()
    print t.validate_and_save(data)