class FallbackIntegerField(FallbackMixinField, IntegerField):
    pass class FallbackCharField(FallbackMixinField, CharField):
    pass class SortDirectionField(FallbackCharField):
    def __init__(self, *args, **kwargs):
        assert 'choices' not in kwargs, 'No custom choices can be set for a ' \
            'SortDirectionField'
        kwargs['choices'] = ['asc', 'desc']
        super(SortDirectionField, self).__init__(*args, **kwargs) class RepeatableField(MultiValueField):
    '''Field that collects a list of values, all of them of the same kind.

    By passing a reference field to its initializer, the RepeatableField will
    take charge of creating enough copies of it to cover all initial values
    and still let room for another empty field that can be used to add an
    extra value to the list.

    As any MultiValueField, fields will have names like "name_0", with
    increasing numbers. Extra fields may be created using JS by cloning any
    pre-existent field and following this naming convention.
    '''
    class Widget(MultiWidget):
        def __init__(self, reference_field, *args, **kwargs):
            self.reference_field = reference_field
            super(RepeatableField.Widget, self).__init__(*args, **kwargs)

        def render(self, *args, **kwargs):
            return mark_safe(
                '<div class="repeatable-field">%s</div>' % super(
                    RepeatableField.Widget, self).render(*args, **kwargs))

        def value_from_datadict(self, data, files, name):
            # Rebuild widgets to adapt to the number of values that have been
            # sent (it could have changed if new fields were created with JS).
            self.widgets = [self.reference_field.widget] * len(
                [key for key in data.keys() if key.startswith(name + '_')])
            return super(RepeatableField.Widget, self).value_from_datadict(
                data, files, name)

        def decompress(self, value):
            # Make sure there's a value for every current widget.
            if value:
                return value + [u''] * (len(self.widgets) - len(value))
            return [u''] * len(self.widgets)

    def __init__(self, reference_field, *args, **kwargs):
        self.reference_field = reference_field
        kwargs['initial'] = kwargs.get('initial') or []
        # Build enough fields to cover the initial values plus one for allowing
        # a new value to be added.
        fields = [reference_field] * (len(kwargs['initial']) + 1)
        super(RepeatableField, self).__init__(
            fields=fields,
            require_all_fields=False,
            widget=RepeatableField.Widget(
                reference_field=reference_field,
                widgets=[field.widget for field in fields]
            ),
            *args, **kwargs)

    def clean(self, value):
        if isinstance(value, (list, tuple)):
            # Rebuild fields to adapt to the number of values that have been
            # sent (it could have been changed if new fields were created with
            # JS).
            self.fields = [self.reference_field] * len(value)
        return super(RepeatableField, self).clean(value)

    def compress(self, data_list):
        # Ignore empty values.
        return [value for value in data_list if value]

    def has_changed(self, initial, data):
        # Take into account the case when the number of subfields has changed.
        initial = self.widget.decompress(initial)
        return \
            (isinstance(initial, list) and
             isinstance(data, list) and
             len(initial) != len(data)) or \
            super(RepeatableField, self).has_changed(initial, data) class VACTemplaterConfig(object):
    class ConfigError(Exception):
        def __init__(self, messages, *args, **kwargs):
            self.messages = messages
            super(VACTemplaterConfig.ConfigError, self).__init__(
                *args, **kwargs)

    def __init__(self, users=None, settings=None):
        self.users = users or []
        self.settings = settings or []

    @classmethod
    def parse(cls, vcl):
        '''Parse a VCL searching for a valid VAC Templater configuration.

        A VACTemplaterConfig.ConfigError exception is raised if any error is
        found in the configuration or in any usage of a setting in the VCL.
        Otherwise, a VACTemplaterConfig instance is be returned.

        '''
        # Default empty config.
        config = cls()

        # Iterate through comments searching for a VAC Templater config.
        for comment in re.finditer(r'\/\*.*?(vac-templater:.*?)\*\/',
                                   vcl,
                                   re.DOTALL | re.MULTILINE):
            try:
                parsed_config = yaml.load(
                    re.sub(r'\n \*', '\n', comment.group(1)))['vac-templater']
            except yaml.YAMLError as e:
                print e
            else:
                # Build instance from parsed config. This may raise a
                # VACTemplaterConfig.ConfigError exception if the config is
                # invalid.
                config = cls.build(parsed_config)

                # Extra check: validate usages of all settings in the VCL to
                # warn about invalid representations. This may raise a
                # VACTemplaterConfig.ConfigError exception.
                config.validate_usages(vcl)

                # Don't search anymore.
                break

        # Done!
        return config

    @classmethod
    def build(cls, parsed_config):
        '''Build a VACTemplaterConfig instance from a YAML parsed config.

        A VACTemplaterConfig.ConfigError exception is raised if any error is
        found in the configuration.

        '''
        config = None
        errors = []

        if isinstance(parsed_config, dict):
            users = []
            settings = []
            roles = set()

            # Parse users and build up a set of known roles.
            if 'users' in parsed_config:
                if isinstance(parsed_config['users'], list):
                    for user_config in parsed_config['users']:
                        try:
                            user = VACTemplaterUser.build(user_config)
                        except VACTemplaterConfig.ConfigError as e:
                            errors += e.messages
                        else:
                            users.append(user)
                            roles |= set(user.roles)
                else:
                    errors.append(
                        _('"users" entry must be a list'))

            # Parse settings.
            if 'settings' in parsed_config:
                if isinstance(parsed_config['settings'], list):
                    for setting_config in parsed_config['settings']:
                        try:
                            setting = VACTemplaterSetting.build(
                                roles, setting_config)
                        except VACTemplaterConfig.ConfigError as e:
                            errors += e.messages
                        else:
                            settings.append(setting)
                else:
                    errors.append(
                        _('"settings" entry must be a list'))

            config = cls(users, settings)
        else:
            errors.append(
                _('Invalid configuration: '
                  '%(parsed_config)s') % {
                    'parsed_config': parsed_config,
                })

        if errors:
            raise VACTemplaterConfig.ConfigError(errors)
        else:
            return config

    @classmethod
    def parse_value(cls, vcl, setting):
        '''Return the current value for a given setting in a certain VCL.

        The current value is extracted from the first usage of the setting.
        If no usage is done or if the current value is invalid, None is
        returned.

        '''
        match = re.search(
            r'/\* {{ %s(?:\|(\w+))? \*/(.*?)/\* }} \*/' % setting.id,
            vcl,
            re.DOTALL | re.MULTILINE)
        if match:
            representation = match.group(1)
            raw_value = match.group(2)
            try:
                return setting.to_python(raw_value, representation)
            except ValueError:
                pass
        return None

    def substitute(self, values, vcl):
        '''Substitute values for all current settings in a VCL.

        Only settings to which values have been provided are substituted.
        The new VCL is returned unless a ValueError exception is raised if any
        of the supplied values has an invalid type for the specific setting.

        '''
        return self._substitute(values, vcl, self.settings)

    def validate_usages(self, vcl, settings=None):
        '''Check that all variable usages are valid.

        If any invalid representation is being tried to be used for a certain
        setting, a VACTemplaterConfig.ConfigError exception is raised.

        '''
        return self._validate_usages(vcl, self.settings)

    def _substitute(self, values, vcl, settings):
        # Substitute all usages in the VCL of every setting for which values
        # were provided.
        for setting in settings:
            if type(setting) == VACTemplaterGroupSetting:
                vcl = self._substitute(values, vcl, setting.settings)
            elif setting.id in values:
                vcl = re.sub(
                    r'/\* {{ %s(\|(\w+))? \*/(.*?)/\* }} \*/' % setting.id,
                    lambda match, setting=setting: '/* {{ %(id)s%(representation)s */%(value)s/* }} */' % {
                        'id': setting.id,
                        'representation': match.group(1) or '',
                        'value': setting.to_vcl(
                            values[setting.id], match.group(2)),
                    },
                    vcl,
                    flags=re.DOTALL | re.MULTILINE)

        # Done!
        return vcl

    def _validate_usages(self, vcl, settings):
        errors = []

        # Check all usages for each setting.
        for setting in settings:
            if type(setting) == VACTemplaterGroupSetting:
                try:
                    self._validate_usages(vcl, setting.settings)
                except VACTemplaterConfig.ConfigError as e:
                    errors += e.messages
            else:
                for usage in re.finditer(
                        r'/\* {{ %s(?:\|(\w+))? \*/(.*?)/\* }} \*/' % setting.id,
                        vcl,
                        re.DOTALL | re.MULTILINE):
                    representation = usage.group(1)
                    if representation is not None and \
                       representation not in setting.REPRESENTATIONS:
                        errors.append(
                            'Invalid representation "%(representation)s" for '
                            'setting "%(setting)s"' % {
                                'representation': representation,
                                'setting': setting.id,
                            })

        # If any error was found, raise a ConfigError exception.
        if errors:
            raise VACTemplaterConfig.ConfigError(errors) class VACTemplaterUser(object):
    def __init__(self, id, roles=None):
        self.id = id
        self.roles = roles if roles is not None else []

        # All users have the implicit "user" role.
        if 'user' not in self.roles:
            self.roles.append('user')

    @classmethod
    def build(cls, user_config):
        '''Build a VACTemplaterUser instance from a YAML parsed config.

        A VACTemplaterConfig.ConfigError exception is raised if any error is
        found in the configuration.

        '''
        user = None
        errors = []

        if isinstance(user_config, dict) and \
           len(user_config) == 1:
            id = user_config.keys()[0]
            roles = user_config.values()[0]
            if isinstance(roles, list) and \
               all(isinstance(role, basestring) for role in roles):
                user = VACTemplaterUser(id, roles)
            else:
                errors.append(
                    _('Invalid roles list for user "%(user)s": %(roles)s') % {
                        'user': id,
                        'roles': roles,
                    })
        else:
            errors.append(
                _('Invalid user configuration: '
                  '%(user_config)s') % {
                    'user_config': user_config,
                })

        if errors:
            raise VACTemplaterConfig.ConfigError(errors)
        else:
            return user class VACTemplaterTextSetting(VACTemplaterSetting):
    TYPE = 'text'

    @classmethod
    def to_vcl(cls, value, representation=None):
        # Default representation is "str".
        representation = representation or 'str'

        # Convert!
        if isinstance(value, basestring):
            if representation == 'str':
                return '"%s"' % value

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def to_python(cls, raw_value, representation=None):
        # Default representation is "str".
        representation = representation or 'str'

        # Convert!
        if representation == 'str' and \
           raw_value[0] == raw_value[-1] == '"':
            return raw_value[1:-1]

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def _build_validator(cls, id, validator, value):
        if validator in ('min', 'max'):
            if isinstance(value, int):
                return value
            else:
                raise VACTemplaterConfig.ConfigError([
                    _('"%(validator)s" validator for setting "%(setting)s" '
                      'should be an integer.') % {
                        'validator': validator,
                        'setting': id,
                    }])
        elif validator == 'regexp':
            if isinstance(value, basestring):
                return value
            else:
                raise VACTemplaterConfig.ConfigError([
                    _('"regexp" validator for setting "%(setting)s" should be '
                      'a string.') % {
                        'setting': id,
                    }])
        else:
            return super(VACTemplaterTextSetting, cls)._build_validator(
                id, validator, value)

    def validate(self, value):
        errors = []

        try:
            super(VACTemplaterTextSetting, self).validate(value)
        except ValidationError as e:
            errors += e.messages

        if 'min' in self.validators and \
           len(value) < self.validators['min']:
            errors.append(_('Value is too short. Min chars are %(min)d.') % {
                'min': self.validators['min'],
            })

        if 'max' in self.validators and \
           len(value) > self.validators['max']:
            errors.append(_('Value is too long. Max chars are %(max)d.') % {
                'max': self.validators['max'],
            })

        if 'regexp' in self.validators and \
           not re.match(self.validators['regexp'], value):
            errors.append(_('Invalid format.'))

        if errors:
            raise ValidationError(errors) class VACTemplaterLongTextSetting(VACTemplaterTextSetting):
    TYPE = 'longtext'

    @classmethod
    def to_vcl(cls, value, representation=None):
        # Default representation is "str".
        representation = representation or 'str'

        # Convert!
        if isinstance(value, basestring):
            if representation == 'str':
                return '{"%s"}' % value

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def to_python(cls, raw_value, representation=None):
        # Default representation is "str".
        representation = representation or 'str'

        # Convert!
        if representation == 'str' and \
           raw_value.startswith('{"') and \
           raw_value.endswith('"}'):
            return raw_value[2:-2]

        # No conversion done? Fail.
        raise ValueError() class VACTemplaterIntegerSetting(VACTemplaterSetting):
    TYPE = 'integer'
    REPRESENTATIONS = ('int', 'str')

    @classmethod
    def to_vcl(cls, value, representation=None):
        # Default representation is "int".
        representation = representation or 'int'

        # Convert!
        if isinstance(value, int):
            if representation == 'int':
                return '%d' % value
            elif representation == 'str':
                return '"%d"' % value

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def to_python(cls, raw_value, representation=None):
        # Default representation is "int".
        representation = representation or 'int'

        # Convert!
        if representation == 'int':
            return int(raw_value)
        elif representation == 'str' and \
                raw_value[0] == raw_value[-1] == '"':
            return int(raw_value[1:-1])

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def _build_validator(cls, id, validator, value):
        if validator in ('min', 'max'):
            if isinstance(value, int):
                return value
            else:
                raise VACTemplaterConfig.ConfigError([
                    _('"%(validator)s" validator for setting "%(setting)s" '
                      'should be an integer.') % {
                        'validator': validator,
                        'setting': id,
                    }])
        else:
            return super(VACTemplaterIntegerSetting, cls)._build_validator(
                id, validator, value)

    def validate(self, value):
        errors = []

        try:
            super(VACTemplaterIntegerSetting, self).validate(value)
        except ValidationError as e:
            errors += e.messages

        if 'min' in self.validators and \
           value < self.validators['min']:
            errors.append(_('Value is too small. Minimum is %(min)d.') % {
                'min': self.validators['min'],
            })

        if 'max' in self.validators and \
           value > self.validators['max']:
            errors.append(_('Value is too big. Maximum is %(max)d.') % {
                'max': self.validators['max'],
            })

        if errors:
            raise ValidationError(errors) class VACTemplaterDurationSetting(VACTemplaterSetting):
    TYPE = 'duration'
    GRANULARITIES = {
        'ms': {
            'label': _('milliseconds'),
            'ms': 1,
        },
        's': {
            'label': _('seconds'),
            'ms': 1000,
        },
        'm': {
            'label': _('minutes'),
            'ms': 60 * 1000,
        },
        'h': {
            'label': _('hours'),
            'ms': 60 * 60 * 1000,
        },
        'd': {
            'label': _('days'),
            'ms': 24 * 60 * 60 * 1000,
        },
        'w': {
            'label': _('weeks'),
            'ms': 7 * 24 * 60 * 60 * 1000,
        },
    }

    @classmethod
    def to_vcl(cls, value, representation=None):
        # Default representation is "duration".
        representation = representation or 'duration'

        # Convert!
        if isinstance(value, VACTemplaterDuration):
            if representation == 'duration':
                return "%s%s" % (
                    str(value.amount if (value.amount % 1 > 0) else int(value.amount)),
                    value.granularity,
                )

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def to_python(cls, raw_value, representation=None):
        # Default representation is "duration".
        representation = representation or 'duration'

        # Convert!
        if representation == 'duration':
            match = re.match(
                r'^([0-9]*\.?[0-9]+)(%s)$' % '|'.join(cls.GRANULARITIES.keys()),
                raw_value)
            if match is not None:
                return VACTemplaterDuration(
                    float(match.group(1)), match.group(2))

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def _build_validator(cls, id, validator, value):
        if validator in ('min', 'max'):
            if isinstance(value, basestring):
                try:
                    return cls.to_python(value)
                except ValueError:
                    raise VACTemplaterConfig.ConfigError([
                        _('Invalid duration at "%(validator)s" validator for '
                          'setting "%(setting)s": %(duration)s') % {
                            'validator': validator,
                            'setting': id,
                            'duration': value,
                        }])
            else:
                raise VACTemplaterConfig.ConfigError([
                    _('"%(validator)s" validator for setting "%(setting)s" '
                      'should be a valid string.') % {
                        'validator': validator,
                        'setting': id,
                    }])
        else:
            return super(VACTemplaterDurationSetting, cls)._build_validator(
                id, validator, value)

    def validate(self, value):
        errors = []

        try:
            super(VACTemplaterDurationSetting, self).validate(value)
        except ValidationError as e:
            errors += e.messages

        if value.amount < 0:
            errors.append(_('Duration must be positive.'))

        value_in_milliseconds = value.amount * self.GRANULARITIES[value.granularity]['ms']

        if 'min' in self.validators and \
           value_in_milliseconds < self.validators['min'].amount * self.GRANULARITIES[self.validators['min'].granularity]['ms']:
            errors.append(_('Value is too small. Minimum is %(min)s.') % {
                'min': self.to_vcl(self.validators['min']),
            })

        if 'max' in self.validators and \
           value_in_milliseconds > self.validators['max'].amount * self.GRANULARITIES[self.validators['max'].granularity]['ms']:
            errors.append(_('Value is too big. Maximum is %(max)s.') % {
                'max': self.to_vcl(self.validators['max']),
            })

        if errors:
            raise ValidationError(errors) class VACTemplaterDuration(object):
    def __init__(self, amount, granularity):
        self.amount = amount
        self.granularity = granularity

    def __repr__(self):
        return "%s %s" % (
            str(self.amount if (self.amount % 1 > 0) else int(self.amount)),
            VACTemplaterDurationSetting.GRANULARITIES[self.granularity]['label'],
        ) class VACTemplaterBooleanSetting(VACTemplaterSetting):
    TYPE = 'boolean'
    REPRESENTATIONS = ('bool', 'int', 'str')

    @classmethod
    def to_vcl(cls, value, representation=None):
        # Default representation is "bool".
        representation = representation or 'bool'

        # Convert!
        if isinstance(value, bool):
            if representation == 'bool':
                return 'true' if value else 'false'
            elif representation == 'int':
                return '1' if value else '0'
            elif representation == 'str':
                return '"1"' if value else '"0"'

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def to_python(cls, raw_value, representation=None):
        # Default representation is "bool".
        representation = representation or 'bool'

        # Convert!
        if representation == 'bool' and \
           raw_value in ('true', 'false'):
            return raw_value == 'true'
        elif representation == 'int' and \
                raw_value.isdigit():
            return raw_value == '1'
        elif representation == 'str' and \
                raw_value[0] == raw_value[-1] == '"':
            return raw_value == '"1"'

        # No conversion done? Fail.
        raise ValueError() class VACTemplaterTimeSetting(VACTemplaterSetting):
    TYPE = 'time'
    REPRESENTATIONS = ('real', 'str')

    @classmethod
    def to_vcl(cls, value, representation=None):
        # Default representation is "real".
        representation = representation or 'real'

        # Convert!
        if isinstance(value, datetime):
            if representation == 'real':
                return '%.2f' % time.mktime(value.timetuple())
            elif representation == 'str':
                return '"%.2f"' % time.mktime(value.timetuple())
        elif value is None:
            if representation == 'real':
                return '0.00'
            else:
                return '"0.00"'

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def to_python(cls, raw_value, representation=None):
        # Default representation is "real".
        representation = representation or 'real'

        # Convert!
        if representation == 'real':
            # 0.00 gets transformed into None.
            if float(raw_value):
                try:

                    return datetime.fromtimestamp(
                        float(raw_value), tz=pytz.utc)
                except (OverflowError, OSError):
                    pass
            else:
                return None
        elif representation == 'str' and \
                raw_value[0] == raw_value[-1] == '"':
            # 0.00 gets transformed into None.
            if float(raw_value[1:-1]):
                try:
                    return datetime.fromtimestamp(
                        float(raw_value[1:-1]), tz=pytz.utc)
                except (OverflowError, OSError):
                    pass
            else:
                return None

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def _build_validator(cls, id, validator, value):
        if validator in ('min', 'max'):
            if isinstance(value, (int, float)):
                try:
                    return datetime.fromtimestamp(float(value), tz=pytz.utc)
                except (OverflowError, OSError):
                    raise VACTemplaterConfig.ConfigError([
                        _('"%(validator)s" validator for setting '
                          '"%(setting)s" should be a valid UNIX '
                          'timestamp.') % {
                            'validator': validator,
                            'setting': id,
                        }])
            else:
                raise VACTemplaterConfig.ConfigError([
                    _('"%(validator)s" validator for setting "%(setting)s" '
                      'should be a valid UNIX timestamp.') % {
                        'validator': validator,
                        'setting': id,
                    }])
        else:
            return super(VACTemplaterTimeSetting, cls)._build_validator(
                id, validator, value)

    def validate(self, value):
        errors = []

        try:
            super(VACTemplaterTimeSetting, self).validate(value)
        except ValidationError as e:
            errors += e.messages

        if 'min' in self.validators and \
           value is not None and \
           value < self.validators['min']:
            errors.append(
                _('That\'s too soon. Earliest valid date is %(min)s.') % {
                    'min': formats.date_format(
                        self.validators['min'], 'DATETIME_FORMAT'),
                }
            )

        if 'max' in self.validators and \
           value is not None and \
           value > self.validators['max']:
            errors.append(
                _('That\'s too late. Latest valid date is %(max)s.') % {
                    'max': formats.date_format(
                        self.validators['max'], 'DATETIME_FORMAT'),
                }
            )

        if errors:
            raise ValidationError(errors) class VACTemplaterACLSetting(VACTemplaterSetting):
    TYPE = 'acl'

    @classmethod
    def to_vcl(cls, value, representation=None):
        # Convert!
        if isinstance(value, VACTemplaterACL):
            if representation is None:
                return '\n%s\n' % '\n'.join('%s;' % val for val in value.acls)

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def to_python(cls, raw_value, representation=None):
        # Convert!
        if representation is None:
            return VACTemplaterACL([
                value.strip() for value in raw_value.split(';')[:-1]])

        # No conversion done? Fail.
        raise ValueError()

    def validate(self, value):
        errors = []

        try:
            super(VACTemplaterACLSetting, self).validate(value)
        except ValidationError as e:
            errors += e.messages

        for val in value.acls:
            if not re.match(r'^\s*\!?\s*"[^"]+"(\/\d+)?\s*$', val) and \
               not re.match(r'^\s*\(\!?\s*"[^"]+"(\/\d+)?\)\s*$', val):
                errors.append(_('Invalid value: %(val)s.') % {
                    'val': val,
                })

        if errors:
            raise ValidationError(errors) class VACTemplaterACL(object):
    def __init__(self, acls):
        self.acls = acls

    def __repr__(self):
        return '\n'.join(self.acls) class VACTemplaterSelectSetting(VACTemplaterSetting):
    TYPE = 'select'

    @classmethod
    def extract_args(cls, id, roles, setting_config):
        args = {}
        errors = []

        try:
            args.update(super(VACTemplaterSelectSetting, cls).extract_args(
                id, roles, setting_config))
        except VACTemplaterConfig.ConfigError as e:
            errors += e.messages

        # Extra validators check: 'options' validator is mandatory.
        if 'validators' not in setting_config or \
           (isinstance(setting_config['validators'], dict) and
                'options' not in setting_config['validators']):
            errors.append(
                _('Setting "%(setting)s" must provide an "options" '
                  'validator.') % {
                    'setting': id,
                })

        if errors:
            raise VACTemplaterConfig.ConfigError(messages=errors)
        else:
            return args

    @classmethod
    def to_vcl(cls, value, representation=None):
        # Default representation is "str".
        representation = representation or 'str'

        # Convert!
        if isinstance(value, basestring):
            if representation == 'str':
                return '"%s"' % value

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def to_python(cls, raw_value, representation=None):
        # Default representation is "str".
        representation = representation or 'str'

        # Convert!
        if representation == 'str' and \
           raw_value[0] == raw_value[-1] == '"':
            return raw_value[1:-1]

        # No conversion done? Fail.
        raise ValueError()

    @classmethod
    def _build_validator(cls, id, validator, value):
        if validator == 'options':
            if isinstance(value, list):
                errors = []
                for option in value:
                    if not isinstance(option, basestring):
                        errors.append(
                            _('Invalid option for setting "%(setting)s": '
                              '%(option)s') % {
                                'setting': id,
                                'option': option,
                            })
                if errors:
                    raise VACTemplaterConfig.ConfigError(errors)
                else:
                    return value
            else:
                raise VACTemplaterConfig.ConfigError([
                    _('"options" validator for setting "%(setting)s" '
                      'should be a list.') % {
                        'setting': id,
                    }])
        else:
            return super(VACTemplaterSelectSetting, cls)._build_validator(
                id, validator, value)

    def validate(self, value):
        errors = []

        try:
            super(VACTemplaterSelectSetting, self).validate(value)
        except ValidationError as e:
            errors += e.messages

        if value not in self.validators['options']:
            errors.append(_('Invalid choice.'))

        if errors:
            raise ValidationError(errors) class VACTemplaterGroupSetting(VACTemplaterSetting):
    TYPE = 'group'

    def __init__(self, id, settings=None, **kwargs):
        super(VACTemplaterGroupSetting, self).__init__(id, **kwargs)
        self.settings = settings if settings is not None else []

    @classmethod
    def extract_args(cls, id, roles, setting_config):
        args = {}
        errors = []

        try:
            args.update(super(VACTemplaterGroupSetting, cls).extract_args(
                id, roles, setting_config))
        except VACTemplaterConfig.ConfigError as e:
            errors += e.messages

        # Parse extra settings argument (optional).
        args['settings'] = []
        if 'settings' in setting_config:
            subsettings_config = setting_config['settings']
            if isinstance(subsettings_config, list):
                for subsetting_config in subsettings_config:
                    try:
                        subsetting = VACTemplaterSetting.build(
                            roles,
                            subsetting_config,
                            prefix=id + ':',
                            default_role=args.get('role'))
                        args['settings'].append(subsetting)
                    except VACTemplaterConfig.ConfigError as e:
                        errors += e.messages
            else:
                errors.append(
                    _('Invalid settings for group "%(setting)s"') % {
                        'setting': id,
                    })

        if errors:
            raise VACTemplaterConfig.ConfigError(messages=errors)
        else:
            return args class Paginator(BasePaginator):
    def __init__(self, page=1, expander=None, *args, **kwargs):
        super(Paginator, self).__init__(*args, **kwargs)
        self.expander = expander
        self.current_page = self.page(page)

    def page_range_slice(self):
        left = self.current_page.number - 2
        right = self.current_page.number + 2
        overflow_left = max(0, (-1 * left) + 1)
        overflow_right = max(0, right - self.num_pages)
        left = max(left - overflow_right, 1)
        right = min(right + overflow_left, self.num_pages)
        return range(left, right + 1)

    def _get_page(self, *args, **kwargs):
        return Page(*args, **kwargs) class Deployment(models.Model):
    user = models.ForeignKey(
        'User',
        null=True,
        on_delete=models.SET_NULL
    )

    group_name = models.CharField(
        _('group name'),
        null=False,
        blank=False,
        max_length=1024)

    group_oid = models.CharField(
        _('group OID'),
        null=False,
        blank=False,
        max_length=OID_MAX_LENGTH)

    branch_name = models.CharField(
        _('branch name'),
        null=False,
        blank=False,
        max_length=1024)

    branch_oid = models.CharField(
        _('branch OID'),
        null=False,
        blank=False,
        max_length=OID_MAX_LENGTH)

    old_head_oid = models.CharField(
        _('old head OID'),
        null=False,
        blank=False,
        max_length=OID_MAX_LENGTH)

    new_head_oid = models.CharField(
        _('new head OID'),
        null=False,
        blank=False,
        max_length=OID_MAX_LENGTH)

    vcl = models.TextField(
        _('VCL'),
        null=False,
        blank=False)

    message = models.TextField(
        _('message'),
        null=False,
        blank=False)

    changes = JSONField(
        _('changes'),
        null=False,
        blank=False)

    created_at = models.DateTimeField(
        _('created at'),
        null=False,
        auto_now_add=True)

    updated_at = models.DateTimeField(
        _('updated at'),
        null=False,
        auto_now=True)
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from vac_templater.forms.base import FallbackIntegerField, FallbackCharField
from vac_templater.forms.base import SortDirectionField, RepeatableField
from vac_templater.helpers.vac_templater_config import (
    VACTemplaterConfig, VACTemplaterUser, VACTemplaterTextSetting,
    VACTemplaterLongTextSetting, VACTemplaterIntegerSetting,
    VACTemplaterDurationSetting, VACTemplaterDuration,
    VACTemplaterBooleanSetting, VACTemplaterTimeSetting,
    VACTemplaterACLSetting, VACTemplaterACL, VACTemplaterSelectSetting,
    VACTemplaterGroupSetting)
from vac_templater.helpers.paginator import Paginator
from vac_templater.models import Deployment
        self.config_errors = []
        self.fieldsets = {}
        self.changes = []

        # Result state. This will hold the result of the execution.
        self.new_vcl = None

        # Retrieve current VCL.
        if group.active_vcl:
            self.vcl_commit = vac.vcl_head(group.active_vcl)
            if self.vcl_commit:
                # Keep track of the current VCLCommit id in a hidden field to
                # make sure it doesn't change while filling the form.
                self.fields['vcl_commit_id'] = forms.CharField(
                    initial=self.vcl_commit.id,
                    widget=forms.HiddenInput)

                try:
                    # Parse VAC Templater config.
                    self.config = VACTemplaterConfig.parse(self.vcl_commit.content)
                except VACTemplaterConfig.ConfigError as e:
                    self.config_errors = e.messages
                else:
                    # Apply roles to current user if defined in the VCL.
                    self.vac_templater_user = next(
                        (vac_templater_user for vac_templater_user in self.config.users
                         if vac_templater_user.id == user.username),
                        self.vac_templater_user)

                    # Build all dynamic settings fields.
                    for setting in self.config.settings:
                        self._add_setting(setting)

    def clean(self):
        # Check VCL is still loaded.
        if self.vcl_commit:
            cleaned_data = super(DeployForm, self).clean()
            # Check VCL hasn't changed while filling the form.
            if self.vcl_commit.id == cleaned_data.get('vcl_commit_id'):
                # Validate values supplied for all settings.
                for setting in self.config.settings:
                    self._clean_setting(setting, cleaned_data)
            else:
                raise forms.ValidationError(_(
                    'The VCL has been modified while you were updating it! '
                    'Please, reload the page, redo all required changes and '
                    'try again.'), code='vcl-missmatch')
        else:
            raise forms.ValidationError(_(
                'The VCL has been undeployed from this group while you were '
                'updating it! No changes have been made.'), code='no-vcl')

    def execute(self):
        # Build new VCL.
        self.new_vcl = self.config.substitute(
            self.cleaned_data, self.vcl_commit.content)

    def _add_setting(self, setting):
        if type(setting) == VACTemplaterGroupSetting:
            # Add all subsettings.
            for subsetting in setting.settings:
                self._add_setting(subsetting)

            # If a field (or fieldset) for any subsetting has been created,
            # add this fieldset to the list. Otherwise, it shouldn't be taken
            # into acount because the user has nothing to edit on it.
            if any(subsetting for subsetting in setting.settings
                   if (subsetting.id in self.fields or
                       subsetting.id in self.fieldsets)):
                self.fieldsets[setting.id] = {
                    'legend': setting.name,
                    'description': setting.description,
                    'subsettings': setting.settings,
                }
        else:
            if setting.role in self.vac_templater_user.roles:
                field_cls = DeployForm.FIELD_MAPPING[type(setting)]
                field_attrs = {
                    'label': setting.name,
                    'required': False,
                    'initial': self.config.parse_value(
                        self.vcl_commit.content, setting),
                    'help_text': setting.description,
                }

                # Setting type customizations.
                if type(setting) == VACTemplaterLongTextSetting:
                    field_attrs['widget'] = forms.Textarea()

                elif type(setting) == VACTemplaterIntegerSetting:
                    field_attrs['required'] = True
                    if 'max' in setting.validators:
                        field_attrs['max_value'] = \
                            setting.validators['max']
                    if 'min' in setting.validators:
                        field_attrs['min_value'] = \
                            setting.validators['min']

                elif type(setting) == VACTemplaterDurationSetting:
                    field_attrs['required'] = True

                elif type(setting) == VACTemplaterTimeSetting:
                    field_attrs['localize'] = True
                    field_attrs['widget'] = forms.DateTimeInput(
                        attrs={
                            'class': 'datetime-input',
                        }
                    )

                elif type(setting) == VACTemplaterSelectSetting:
                    field_attrs['required'] = True
                    field_attrs['choices'] = [
                        (option, option)
                        for option in setting.validators['options']]

                # Build field.
                self.fields[setting.id] = field_cls(**field_attrs)

    def _clean_setting(self, setting, cleaned_data, groups=None):
        groups = groups or []

        if type(setting) == VACTemplaterGroupSetting: