# -*- coding: utf-8 -*-
#
# w.c.s. - web application for online forms
# Copyright (C) 2005-2010  Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.

import copy
import datetime
import time
import random
import re
import sys
import base64
import xml.etree.ElementTree as ET
import collections

from quixote import get_request, get_publisher
from quixote.html import htmltext, TemplateIO

from django.utils import six
from django.utils.encoding import force_bytes, force_text, smart_text
from django.utils.formats import date_format as django_date_format
from django.utils.html import urlize
from django.utils.six.moves.html_parser import HTMLParser

from .qommon import _, force_str
from .qommon import evalutils
from .qommon.form import *
from .qommon.misc import localstrftime, strftime, date_format, ellipsize, can_thumbnail
from .qommon.template import Template, TemplateError
from .qommon import get_cfg, get_logger

from . import data_sources
from . import portfolio
from .conditions import Condition


class PrefillSelectionWidget(CompositeWidget):
    def __init__(self, name, value=None, field=None, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)

        if not value:
            value = {}

        options = [('none', _('None')),
                ('string', _('String / Template')),
                ('formula', _('Python Expression')),
                ('user', _('User Field')),
                ('geolocation', _('Geolocation')),]

        if field and field.type == 'items':
            # limit choices to python as items field are prefilled with list
            # of strings
            options = [x for x in options if x[0] in ('none', 'formula')]
        elif field and field.type == 'map':
            # limit choices to geolocation
            options = [x for x in options if x[0] in ('none', 'string', 'geolocation')]

        self.add(SingleSelectWidget, 'type', options=options, value=value.get('type'),
                 attrs={'data-dynamic-display-parent': 'true'})

        self.parse()
        if not self.value:
            self.value = {}

        prefill_types = collections.OrderedDict(options)
        self.add(StringWidget, 'value_string', size=80,
                 value=value.get('value') if value.get('type') == 'string' else None,
                 attrs={'data-dynamic-display-child-of': 'prefill$type',
                        'data-dynamic-display-value': prefill_types.get('string')})
        self.add(StringWidget, 'value_formula', size=80,
                 value=value.get('value') if value.get('type') == 'formula' else None,
                 attrs={'data-dynamic-display-child-of': 'prefill$type',
                        'data-dynamic-display-value': prefill_types.get('formula')})

        formdef = get_publisher().user_class.get_formdef()
        users_cfg = get_cfg('users', {})
        if formdef:
            user_fields = []
            for user_field in formdef.fields:
                if user_field.label in [x[1] for x in user_fields]:
                    # do not allow duplicated field names
                    continue
                user_fields.append((user_field.id, user_field.label))
            if not users_cfg.get('field_email'):
                user_fields.append(('email', _('Email (builtin)')))
        else:
            user_fields = [('name', _('Name')), ('email', _('Email'))]
        self.add(SingleSelectWidget, 'value_user',
                 value=value.get('value') if value.get('type') == 'user' else None,
                 options=user_fields,
                 attrs={'data-dynamic-display-child-of': 'prefill$type',
                        'data-dynamic-display-value': prefill_types.get('user')})

        if field and field.type == 'map':
            # different prefilling sources on map fields
            geoloc_fields = [('position', _('Position'))]
        else:
            geoloc_fields = [
                ('house', _('Number')),
                ('road', _('Street')),
                ('number-and-street', _('Number and street')),
                ('postcode', _('Post Code')),
                ('city', _('City')),
                ('country', _('Country')),
            ]
        self.add(SingleSelectWidget, 'value_geolocation',
                 value=value.get('value') if value.get('type') == 'geolocation' else None,
                 options=geoloc_fields,
                 attrs={'data-dynamic-display-child-of': 'prefill$type',
                        'data-dynamic-display-value': prefill_types.get('geolocation')})

        self._parsed = False


    def _parse(self, request):
        values = {}
        type_ = self.get('type')
        if type_:
            values['type'] = type_
            value = self.get('value_%s' % type_)
            if value:
                values['value'] = value
        self.value = values or None
        if values and values['type'] == 'formula' and values.get('value'):
            try:
                compile(values.get('value', ''), '<string>', 'eval')
            except (SyntaxError, TypeError) as e:
                self.set_error(_('invalid expression: %s') % e)
        if values and values['type'] == 'string' and Template.is_template_string(values.get('value')):
            try:
                Template(values.get('value'), raises=True)
            except TemplateError as e:
                self.set_error(str(e))

    def render_content(self):
        r = TemplateIO(html=True)
        for widget in self.get_widgets():
            r += widget.render_content()
        return r.getvalue()


class Field(object):
    id = None
    type = None
    varname = None
    label = None
    extra_css_class = None
    convert_value_from_str = None
    convert_value_to_str = None
    convert_value_from_anything = None
    display_locations = []
    prefill = None
    store_display_value = None
    store_structured_value = None
    condition = None

    # flag a field for removal by AnonymiseWorkflowStatusItem
    # can be overriden in field' settings
    anonymise = True
    stats = None

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k.replace('-', '_'), v)

    @classmethod
    def init(cls):
        pass

    @property
    def include_in_listing(self):
        return 'listings' in (self.display_locations or [])

    @property
    def include_in_validation_page(self):
        return 'validation' in (self.display_locations or [])

    @property
    def include_in_summary_page(self):
        return 'summary' in (self.display_locations or [])

    @property
    def unhtmled_label(self):
        return force_str(HTMLParser().unescape(force_text(
            re.sub('<.*?>', ' ', self.label))).strip())

    def get_admin_attributes(self):
        return ['label', 'type', 'condition']

    def export_to_json(self, include_id=False, anonymise=True):
        field = {}
        if include_id:
            extra_fields = ['id']
        else:
            extra_fields = []
        for attribute in self.get_admin_attributes() + extra_fields:
            if attribute == 'display_locations':
                continue
            if hasattr(self, attribute) and getattr(self, attribute) is not None:
                val = getattr(self, attribute)
                field[attribute] = val
        if not 'type' in field:
            field['type'] = self.key
        return field

    def init_with_json(self, elem, include_id=False):
        if include_id:
            self.id = elem.get('id')
        for attribute in self.get_admin_attributes():
            if attribute in elem:
                setattr(self, attribute, elem.get(attribute))

    def export_to_xml(self, charset, include_id=False):
        field = ET.Element('field')
        if include_id:
            extra_fields = ['id']
        else:
            extra_fields = []
        for attribute in self.get_admin_attributes() + extra_fields:
            if hasattr(self, '%s_export_to_xml' % attribute):
                getattr(self, '%s_export_to_xml' % attribute)(field, charset,
                        include_id=include_id)
                continue
            if hasattr(self, attribute) and getattr(self, attribute) is not None:
                val = getattr(self, attribute)
                if type(val) is dict and not val:
                    continue
                el = ET.SubElement(field, attribute)
                if type(val) is dict:
                    for k, v in sorted(val.items()):
                        if isinstance(v, str):
                            text_value = force_text(v, charset, errors='replace')
                        else:
                            # field having non str value in dictionnary field must overload
                            # import_to_xml to handle import
                            text_value = force_text(v)
                        ET.SubElement(el, k).text = text_value
                elif type(val) is list:
                    if attribute[-1] == 's':
                        atname = attribute[:-1]
                    else:
                        atname = 'item'
                    for v in val:
                        ET.SubElement(el, atname).text = force_text(v, charset, errors='replace')
                elif isinstance(val, six.string_types):
                    el.text = force_text(val, charset, errors='replace')
                else:
                    el.text = str(val)
        return field

    def init_with_xml(self, elem, charset, include_id=False):
        for attribute in self.get_admin_attributes():
            el = elem.find(attribute)
            if hasattr(self, '%s_init_with_xml' % attribute):
                getattr(self, '%s_init_with_xml' % attribute)(el, charset,
                        include_id=include_id)
                continue
            if el is None:
                continue
            if list(el):
                if type(getattr(self, attribute)) is list:
                    v = [force_str(x.text) for x in el]
                elif type(getattr(self, attribute)) is dict:
                    v = {}
                    for e in el:
                        v[e.tag] = force_str(e.text)
                else:
                    print('currently:', self.__dict__)
                    print('  attribute:', attribute)
                    # ???
                    raise AssertionError
                setattr(self, attribute, v)
            else:
                if el.text is None:
                    setattr(self, attribute, None)
                elif el.text in ('False', 'True'): # bools
                    setattr(self, attribute, el.text == 'True')
                elif type(getattr(self, attribute)) is int:
                    setattr(self, attribute, int(el.text))
                else:
                    setattr(self, attribute, force_str(el.text))
        if include_id:
            try:
                self.id = force_str(elem.find('id').text)
            except:
                pass

    def condition_init_with_xml(self, node, charset, include_id=False):
        self.condition = None
        if node is None:
            return
        if node.findall('type'):
            self.condition = {
                'type': force_str(node.find('type').text),
                'value': force_str(node.find('value').text),
            }
        elif node.text:
            self.condition = {'type': 'python', 'value': force_str(node.text).strip()}

    def get_rst_view_value(self, value, indent=''):
        return indent + self.get_view_value(value)

    def get_csv_heading(self):
        return []

    def get_csv_value(self, element, **kwargs):
        return []

    def get_structured_value(self, data):
        if not self.store_structured_value:
            return None
        return data.get('%s_structured' % self.id)

    def get_prefill_value(self, user=None, force_string=True):
        # returns a tuple with two items,
        #  1. value[str], the value that will be used to prefill
        #  2. verified[bool], a flag to know if this is a "verified" value
        #     (that will therefore be marked as readonly etc.)
        t = self.prefill.get('type')
        if t == 'string':
            value = self.prefill.get('value')
            if not Template.is_template_string(value):
                return (value, False)

            context = get_publisher().substitutions.get_context_variables()
            try:
                return (Template(value, autoescape=False, raises=True).render(context), False)
            except TemplateError:
                return (None, False)

        elif t == 'user' and user:
            x = self.prefill.get('value')
            if x == 'email':
                return (user.email, 'email' in (user.verified_fields or []))
            elif user.form_data:
                userform = user.get_formdef()
                for userfield in userform.fields:
                    if userfield.id == x:
                        return (user.form_data.get(x),
                                str(userfield.id) in (user.verified_fields or []))

        elif t == 'formula':
            formula = self.prefill.get('value')
            try:
                ret = eval(formula,
                    get_publisher().get_global_eval_dict(),
                    get_publisher().substitutions.get_context_variables())
                if isinstance(ret, datetime.time):
                    ret = misc.site_encode(django_date_format(ret, format='TIME_FORMAT'))
                if isinstance(ret, datetime.date):
                    ret = ret.strftime(date_format())
                if ret:
                    if force_string:
                        # prefilling is done with strings for most fields so
                        # we default to forcing the value as a string.
                        # (items field are prefilled with list of strings, and
                        # will get the native python object)
                        ret = str(ret)
                    return (ret, False)
            except:
                pass

        elif t == 'geolocation':
            return (None, False)

        return (None, False)

    def get_prefill_attributes(self):
        t = self.prefill.get('type')

        if t == 'geolocation':
            return {'geolocation': self.prefill.get('value')}

        if t == 'user':
            formdef = get_publisher().user_class.get_formdef()
            for user_field in formdef.fields or []:
                if user_field.id != self.prefill.get('value'):
                    continue
                try:
                    autocomplete_attribute = re.search(r'\bautocomplete-([a-z0-9-]+)',
                            user_field.extra_css_class).groups()[0]
                except (TypeError, IndexError, AttributeError):
                    continue
                return {'autocomplete': autocomplete_attribute}

        return None

    def feed_session(self, value, display_value):
        pass

    def migrate(self):
        changed = False
        if getattr(self, 'in_listing', None) is not None:
            if self.in_listing:
                self.display_locations = self.display_locations[:]
                self.display_locations.append('listings')
                changed = True
                self.in_listing = None
        return changed

    def evaluate_condition(self, dict_vars, formdef, condition):
        return PageCondition(condition, {'dict_vars': dict_vars, 'formdef': formdef}).evaluate()

    def is_visible(self, dict, formdef):
        try:
            return self.evaluate_condition(dict, formdef, self.condition)
        except RuntimeError:
            return True

    def get_referenced_varnames(self, formdef, value):
        return re.findall(r'\b%s[_\.]var[_\.]([a-zA-Z0-9_]+?)(?:_raw|\b)' % formdef.var_prefix, value or '')

    def get_condition_varnames(self, formdef):
        return self.get_referenced_varnames(formdef, self.condition['value'])

    def has_live_conditions(self, formdef):
        varnames = self.get_condition_varnames(formdef)
        if not varnames:
            return False
        field_position = formdef.fields.index(self)
        # rewind to field page
        for field_position in range(field_position, -1, -1):
            if formdef.fields[field_position].type == 'page':
                break
        else:
            field_position = -1  # form with no page
        # start from there
        for field in formdef.fields[field_position+1:]:
            if field.type == 'page':
                # stop at next page
                break
            if field.varname in varnames:
                return True
        return False

    def __repr__(self):
        return '<%s %s %r>' % (self.__class__.__name__, self.id, self.label and self.label[:64])


class WidgetField(Field):
    hint = None
    required = True
    display_locations = ['validation', 'summary']
    extra_attributes = []
    prefill = {}

    widget_class = None

    def add_to_form(self, form, value = None):
        kwargs = {'required': self.required}
        if value:
            kwargs['value'] = value
        for k in self.extra_attributes:
            if hasattr(self, k):
                kwargs[k] = getattr(self, k)
        self.perform_more_widget_changes(form, kwargs)
        if self.hint and self.hint.startswith('<'):
            hint = htmltext(self.hint)
        else:
            hint = self.hint
        form.add(self.widget_class, 'f%s' % self.id, title=self.label, hint=hint, **kwargs)
        widget = form.get_widget('f%s' % self.id)
        widget.field = self
        if self.extra_css_class:
            if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
                widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
            else:
                widget.extra_css_class = self.extra_css_class
        if self.varname:
            widget.div_id = 'var_%s' % self.varname
        return widget

    def perform_more_widget_changes(self, form, kwargs, edit = True):
        pass


    def add_to_view_form(self, form, value = None):
        kwargs = {}

        self.field_key = 'f%s' % self.id
        self.perform_more_widget_changes(form, kwargs, False)

        for k in self.extra_attributes:
            if hasattr(self, k):
                kwargs[k] = getattr(self, k)

        if self.widget_class is StringWidget and not 'size' in kwargs and value:
            # set a size if there is not one already defined, this will be for
            # example the case with ItemField
            kwargs['size'] = len(value)

        form.add(self.widget_class, self.field_key, title = self.label,
                value = value, readonly = 'readonly', **kwargs)
        widget = form.get_widget(self.field_key)
        if self.extra_css_class:
            if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
                widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
            else:
                widget.extra_css_class = self.extra_css_class

    def get_display_locations_options(self):
        return [('validation', _('Validation Page')),
                ('summary', _('Summary Page')),
                ('listings', _('Management Listings'))]

    def fill_admin_form(self, form):
        form.add(StringWidget, 'label', title = _('Label'), value = self.label,
                required = True, size = 50)
        form.add(CheckboxWidget, 'required', title = _('Required'),
                value = self.required)
        form.add(VarnameWidget, 'varname', title=_('Identifier'),
                value=self.varname, size=30, advanced=(not self.varname),
                hint=_('This is used as suffix for variable names.'))
        form.add(TextWidget, 'hint', title = _('Hint'), value = self.hint,
                cols=60, rows=3)
        form.add(CheckboxesWidget, 'display_locations', title=_('Display Locations'),
                options=self.get_display_locations_options(),
                value=self.display_locations,
                advanced=True)
        form.add(StringWidget, 'extra_css_class', title = _('Extra classes for CSS styling'),
                value=self.extra_css_class, size=30, advanced=(not self.extra_css_class))
        prefill_in_advanced = (not self.prefill or self.prefill.get('type') == 'none')
        if prefill_in_advanced and (get_request().form.get('prefill$apply') or
                get_request().form.get('prefill$value')):
            # check if the apply button has been clicked, and get the field out
            # of the "advanced parameters" section if that's the case.
            prefill_in_advanced = False
        form.add(PrefillSelectionWidget, 'prefill', title=_('Prefill'),
                value=self.prefill, advanced=prefill_in_advanced,
                field=self)
        form.add(ConditionWidget, 'condition', title=_('Display Condition'), value=self.condition,
                required=False, size=50, django_only=True,
                advanced=not(bool(self.condition)))
        if 'anonymise' in self.get_admin_attributes():
            # override anonymise flag default value
            form.add(CheckboxWidget, 'anonymise', title=_('Anonymise'),
                    value=self.anonymise, advanced=True,
                    hint=_('Marks the field data for removal in the anonymisation processes.'))

    def check_admin_form(self, form):
        return

    def get_admin_attributes(self):
        return Field.get_admin_attributes(self) + ['required', 'hint',
                'varname', 'display_locations',
                'extra_css_class', 'prefill']

    def get_csv_heading(self):
        return [self.label]

    def get_view_value(self, value):
        return str(value) if value else ''

    def get_view_short_value(self, value, max_len = 30):
        return self.get_view_value(value)

    def get_csv_value(self, element, **kwargs):
        if self.convert_value_to_str:
            return [self.convert_value_to_str(element)]
        return [element]

field_classes = []
field_types = []

def register_field_class(klass):
    if not klass in field_classes:
        field_classes.append(klass)
        if not issubclass(klass, WidgetField):
            field_types.append((klass.key, klass.description))
        else:
            non_widgets = [x for x in field_classes if not issubclass(x, WidgetField)]
            if not non_widgets:
                field_types.append((klass.key, klass.description))
            else:
                idx = field_types.index([x for x in field_types if x[0] == non_widgets[0].key][0])
                field_types.insert(idx, (klass.key, klass.description))
        klass.init()


class TitleField(Field):
    key = 'title'
    description = N_('Title')
    html_tag = 'h3'
    display_locations = ['validation', 'summary']

    def add_to_form(self, form, value = None):
        import wcs.workflows
        extra_attributes = ' data-field-id="%s"' % self.id
        if self.extra_css_class:
            extra_attributes += ' class="%s"' % self.extra_css_class
        title_markup = '<{html_tag}{extra_attributes}>%s</{html_tag}>'.format(
            html_tag=self.html_tag,
            extra_attributes=extra_attributes,
        )
        label = wcs.workflows.template_on_formdata(None, self.label, autoescape=False)
        widget = HtmlWidget(htmltext(title_markup) % label)
        form.widgets.append(widget)
        return widget
    add_to_view_form = add_to_form

    def get_display_locations_options(self):
        return [('validation', _('Validation Page')),
                ('summary', _('Summary Page'))]

    def fill_admin_form(self, form):
        form.add(StringWidget, 'label', title = _('Label'), value = self.label,
                required = True, size = 50)
        form.add(StringWidget, 'extra_css_class', title = _('Extra classes for CSS styling'),
                value=self.extra_css_class, size=30, advanced=(not self.extra_css_class))
        form.add(ConditionWidget, 'condition', title=_('Display Condition'), value=self.condition,
                required=False, size=50, django_only=True,
                advanced=not(bool(self.condition)))
        form.add(CheckboxesWidget, 'display_locations', title=_('Display Locations'),
                options=self.get_display_locations_options(),
                value=self.display_locations,
                advanced=True)

    def get_admin_attributes(self):
        return Field.get_admin_attributes(self) + ['extra_css_class', 'display_locations']

register_field_class(TitleField)


class SubtitleField(TitleField):
    key = 'subtitle'
    description = N_('Subtitle')
    html_tag = 'h4'

register_field_class(SubtitleField)


class CommentField(Field):
    key = 'comment'
    description = N_('Comment')
    display_locations = []

    def get_text(self):
        import wcs.workflows
        label = self.get_html_content()
        return wcs.workflows.template_on_html_string(label)

    def add_to_form(self, form, value=None):
        widget = CommentWidget(
                content=self.get_text(),
                extra_css_class=self.extra_css_class)
        form.widgets.append(widget)
        return widget

    def add_to_view_form(self, *args):
        if self.include_in_validation_page:
            return self.add_to_form(*args)
        return None

    def get_html_content(self):
        if not self.label:
            return ''
        if self.label.startswith('<'):
            return self.label
        if '\n\n' in self.label:
            # blank lines to paragraphs
            label = '</p>\n<p>'.join([str(htmlescape(x)) for x in re.split('\n\n+', self.label)])
            return '<p>' + label + '</p>'
        return '<p>%s</p>' % str(htmlescape(self.label))

    def get_display_locations_options(self):
        return [('validation', _('Validation Page')),
                ('summary', _('Summary Page'))]

    def fill_admin_form(self, form):
        if self.label and (self.label[0] != '<' and '[end]' in self.label):
            form.add(TextWidget, 'label', title=_('Label'), value=self.label,
                    required=True, cols=70, rows=3, render_br=False)
        else:
            form.add(WysiwygTextWidget, 'label', title=_('Label'),
                    value=self.get_html_content(), required=True)
        form.add(StringWidget, 'extra_css_class', title = _('Extra classes for CSS styling'),
                value=self.extra_css_class, size=30, advanced=(not self.extra_css_class))
        form.add(ConditionWidget, 'condition', title=_('Display Condition'), value=self.condition,
                required=False, size=50, django_only=True,
                advanced=not(bool(self.condition)))
        form.add(CheckboxesWidget, 'display_locations', title=_('Display Locations'),
                options=self.get_display_locations_options(),
                value=self.display_locations,
                advanced=True)

    def get_admin_attributes(self):
        return Field.get_admin_attributes(self) + ['extra_css_class', 'display_locations']


register_field_class(CommentField)

def is_datasource_advanced(value):
    data_source_in_advanced = (not value)
    if data_source_in_advanced and (get_request().form.get('data_source$apply') or
            get_request().form.get('data_source$value')):
        data_source_in_advanced = False
    return data_source_in_advanced


class StringField(WidgetField):
    key = 'string'
    description = N_('Text (line)')

    widget_class = WcsExtraStringWidget
    size = None
    extra_attributes = ['size']
    validation = {}
    data_source = {}

    def perform_more_widget_changes(self, form, kwargs, edit=True):
        if self.data_source:
            real_data_source = data_sources.get_real(self.data_source)
            if real_data_source.get('type') == 'jsonp':
                kwargs['url'] = real_data_source.get('value')
                self.widget_class = AutocompleteStringWidget

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        if self.size:
            form.add(StringWidget, 'size', title=_('Line length'),
                     hint=_('Deprecated option, it is advised to use CSS classes '
                            'to size the fields in a manner compatible with all devices.'),
                     value=self.size)
        else:
            form.add(HiddenWidget, 'size', value=None)
        form.add(ValidationWidget, 'validation', title=_('Validation'),
                 value=self.validation, advanced=(not self.validation))
        form.add(data_sources.DataSourceSelectionWidget, 'data_source',
                 value=self.data_source,
                 title=_('Data Source'),
                 hint=_('This will allow autocompletion from an external source.'),
                 advanced=is_datasource_advanced(self.data_source),
                 required=False)

    def get_admin_attributes(self):
        return WidgetField.get_admin_attributes(self) + ['size', 'validation', 'data_source', 'anonymise']

    def get_view_value(self, value):
        value = value or ''
        if value.startswith('http://') or value.startswith('https://'):
            charset = get_publisher().site_charset
            value = force_text(value, charset)
            return htmltext(force_str(urlize(value, nofollow=True, autoescape=True)))
        return str(value)

    def get_rst_view_value(self, value, indent=''):
        return indent + str(value or '')

    @classmethod
    def convert_value_from_anything(cls, value):
        if value is None:
            return None
        return str(value)

    def migrate(self):
        changed = super(StringField, self).migrate()
        if isinstance(self.validation, six.string_types):
            self.validation = {'type': 'regex', 'value': self.validation}
            changed = True
        return changed

    def init_with_xml(self, element, charset, include_id=False):
        super(StringField, self).init_with_xml(element, charset, include_id=include_id)
        self.migrate()

register_field_class(StringField)


class TextField(WidgetField):
    key = 'text'
    description = N_('Long Text')

    widget_class = TextWidget
    cols = None
    rows = None
    pre = False
    maxlength = None
    extra_attributes = ['cols', 'rows', 'maxlength']

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        if self.cols:
            form.add(StringWidget, 'cols', title=_('Line length'),
                     hint=_('Deprecated option, it is advised to use CSS classes '
                            'to size the fields in a manner compatible with all devices.'),
                     value=self.cols)
        else:
            form.add(HiddenWidget, 'cols', value=None)
        form.add(StringWidget, 'rows', title = _('Number of rows'),
                value = self.rows)
        form.add(StringWidget, 'maxlength',
                 title=_('Maximum number of characters'),
                 value = self.maxlength)
        form.add(CheckboxWidget, 'pre', title = _('Preformatted Text'),
                value=self.pre, advanced=not(self.pre))

    def get_admin_attributes(self):
        return WidgetField.get_admin_attributes(self) + ['cols', 'rows', 'pre', 'maxlength']

    def get_view_value(self, value):
        if self.pre:
            return htmltext('<pre>') + value + htmltext('</pre>')
        else:
            try:
                return htmltext('<p>') + htmltext('\n').join(
                        [(x or htmltext('</p><p>')) for x in value.splitlines()]) + \
                    htmltext('</p>')
            except:
                return ''

    def get_view_short_value(self, value, max_len = 30):
        return ellipsize(str(value), max_len)

register_field_class(TextField)


class EmailField(WidgetField):
    key = 'email'
    description = N_('Email')

    widget_class = EmailWidget

    def get_view_value(self, value):
        return htmltext('<a href="mailto:%s">%s</a>') % (value, value)

    def get_rst_view_value(self, value, indent=''):
        return indent + value

register_field_class(EmailField)


class BoolField(WidgetField):
    key = 'bool'
    description = N_('Check Box (single choice)')

    widget_class = CheckboxWidget
    required = False
    anonymise = False

    def perform_more_widget_changes(self, form, kwargs, edit = True):
        if not edit:
            kwargs['disabled'] = 'disabled'
            value = get_request().get_field(self.field_key)
            form.add_hidden(self.field_key, value=str(value))
            if value and not value == 'False':
                self.field_key = 'f%sdisabled' % self.id
                get_request().form[self.field_key] = 'yes'
            self.field_key = 'f%sdisabled' % self.id

    def get_view_value(self, value):
        if value is True or value == 'True':
            return _('Yes')
        elif value is False or value == 'False':
            return _('No')
        else:
            return ''

    def convert_value_from_str(self, value):
        if value is None:
            return None
        if value in (True, 'True', _('Yes'), 'yes'):
            return True
        return False

    def convert_value_to_str(self, value):
        if value is True:
            return 'True'
        elif value is False:
            return 'False'
        return value

    def stats(self, values):
        no_records = len(values)
        if not no_records:
            return
        r = TemplateIO(html = True)
        r += htmltext('<table class="stats">')
        r += htmltext('<thead><tr><th colspan="4">')
        r += self.label
        r += htmltext('</th></tr></thead>')
        options = (True, False)
        r += htmltext('<tbody>')
        for o in options:
            r += htmltext('<tr>')
            r += htmltext('<td class="label">')
            if o is True:
                r += _('Yes')
                value = True
            else:
                r += _('No')
                value = False
            r += htmltext('</td>')
            no = len([None for x in values if self.convert_value_from_str(x.data.get(self.id)) is value])

            r += htmltext('<td class="percent">')
            r += htmltext(' %.2f&nbsp;%%') % (100.*no/no_records)
            r += htmltext('</td>')
            r += htmltext('<td class="total">')
            r += '(%d/%d)' % (no, no_records)
            r += htmltext('</td>')
            r += htmltext('</tr>')
            r += htmltext('<tr>')
            r += htmltext('<td class="bar" colspan="3">')
            r += htmltext('<span style="width: %d%%"></span>' % (100*no/no_records))
            r += htmltext('</td>')
            r += htmltext('</tr>')
        r += htmltext('</tbody>')
        r += htmltext('</table>')
        return r.getvalue()

register_field_class(BoolField)


class FileField(WidgetField):
    key = 'file'
    description = N_('File Upload')
    document_type = None
    max_file_size = None
    automatic_image_resize = False
    allow_portfolio_picking = False

    widget_class = FileWithPreviewWidget
    extra_attributes = [
            'file_type',
            'max_file_size',
            'allow_portfolio_picking',
            'automatic_image_resize',
            ]

    def __init__(self, *args, **kwargs):
        super(FileField, self).__init__(*args, **kwargs)
        self.document_type = self.document_type or {}

    @property
    def file_type(self):
        return (self.document_type or {}).get('mimetypes', [])

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        document_types = self.get_document_types()
        cur_dt = self.document_type or {}
        # SingleSelectWidget compare the value and not the keys, so if we want
        # the current value not to be hidden, we must reset it with the corresponding
        # value from settings based on the 'id'
        document_type_id = cur_dt.get('id')
        if document_type_id in document_types \
               and cur_dt != document_types[document_type_id]:
            cur_dt = document_types[document_type_id]
        options = [(None, '---', {})]
        options += [(doc_type, doc_type['label'], key) for key, doc_type in document_types.items()]
        form.add(SingleSelectWidget, 'document_type', title=_('File type suggestion'),
                value=cur_dt, options=options,
                advanced=not(cur_dt))
        form.add(FileSizeWidget, 'max_file_size', title=_('Max file size'),
                value=self.max_file_size,
                advanced=not(self.max_file_size))
        form.add(CheckboxWidget, 'automatic_image_resize',
                title=_('Automatically resize uploaded images'),
                value=self.automatic_image_resize,
                advanced=True)
        if portfolio.has_portfolio():
            form.add(CheckboxWidget, 'allow_portfolio_picking',
                    title=_('Allow user to pick a file from a portfolio'),
                    value=self.allow_portfolio_picking,
                    advanced=(self.allow_portfolio_picking is FileField.allow_portfolio_picking))

    def get_admin_attributes(self):
        return WidgetField.get_admin_attributes(self) + [
                'document_type', 'max_file_size', 'allow_portfolio_picking',
                'automatic_image_resize']

    @classmethod
    def convert_value_from_anything(cls, value):
        if value is None:
            return None
        if hasattr(value, 'base_filename'):
            upload = PicklableUpload(value.base_filename,
                                     value.content_type or 'application/octet-stream')
            upload.receive([value.get_content()])
            return upload
        from wcs.workflows import NamedAttachmentsSubstitutionProxy
        if isinstance(value, NamedAttachmentsSubstitutionProxy):
            upload = PicklableUpload(value.filename, value.content_type)
            upload.receive([value.content])
            return upload
        if isinstance(value, dict):
            # if value is a dictionary we expect it to have a content or
            # b64_content key and a filename keys and an optional
            # content_type key.
            if 'b64_content' in value:
                value['content'] = base64.decodestring(force_bytes(value['b64_content']))
            if 'filename' in value and 'content' in value:
                content_type = value.get('content_type') or 'application/octet-stream'
                if content_type.startswith('text/'):
                    charset = 'utf-8'
                else:
                    charset = None
                upload = PicklableUpload(value['filename'], content_type, charset)
                upload.receive([force_bytes(value['content'])])
                return upload
        raise ValueError('invalid data for file type (%r)' % value)

    def get_view_short_value(self, value, max_len=30):
        return self.get_view_value(value, include_image_thumbnail=False)

    def get_view_value(self, value, include_image_thumbnail=True):
        t = TemplateIO(html=True)
        t += htmltext('<div class="file-field">')
        t += htmltext('<a download="%s" href="[download]?f=%s">') % (value.base_filename, self.id)
        if include_image_thumbnail and can_thumbnail(value.content_type):
            t += htmltext('<img alt="" src="[download]?f=%s&thumbnail=1"/>') % self.id
        t += htmltext('<span>%s</span>') % value
        t += htmltext('</a></div>')
        return t.getvalue()

    def get_csv_value(self, value, **kwargs):
        return [str(value) if value else '']

    def get_json_value(self, value):
        return {
            'field_id': self.id,
            'filename': value.base_filename,
            'content_type': value.content_type or 'application/octet-stream',
            'content': force_text(base64.b64encode(value.get_content())),
        }

    def from_json_value(self, value):
        if value and 'filename' in value and 'content' in value:
            content = base64.b64decode(value['content'])
            content_type = value.get('content_type', 'application/octet-stream')
            if content_type.startswith('text/'):
                charset = 'utf-8'
            else:
                charset = None
            upload = PicklableUpload(value['filename'], content_type, charset)
            upload.receive([content])
            return upload
        return None

    def perform_more_widget_changes(self, form, kwargs, edit = True):
        if not edit:
            value = get_request().get_field(self.field_key)
            if value and hasattr(value, 'token'):
                get_request().form[self.field_key + '$token'] = value.token

    def get_document_types(self):
        document_types = {
            '_audio': {
                'label': _('Sound files'),
                'mimetypes': ['audio/*'],
            },
            '_video': {
                'label': _('Video files'),
                'mimetypes': ['video/*'],
            },
            '_image': {
                'label': _('Image files'),
                'mimetypes': ['image/*'],
            }
        }
        # Local document types
        document_types.update(get_cfg('filetypes', {}))
        for key, document_type in document_types.items():
            document_type['id'] = key
        # add current file type if it does not exist anymore in the settings
        cur_dt = self.document_type or {}
        if cur_dt and cur_dt['id'] not in document_types:
            document_types[cur_dt['id']] = cur_dt
        return document_types

    def migrate(self):
        changed = super(FileField, self).migrate()
        if 'file_type' in self.__dict__:
            self.document_type = {}
            if self.__dict__['file_type']:
                file_type = self.__dict__['file_type']
                document_types = self.get_document_types()
                parts = []
                for key, value in document_types.items():
                    if file_type == value.get('mimetypes'):
                        self.document_type = value.copy()
                        self.document_type['id'] = key
                        break
                    if not value.get('mimetypes'):
                        continue
                    if ','.join(value['mimetypes']) in file_type:
                        parts.append(value['label'])
                else:
                    # self.file_type is a combination of file type, we create a
                    # virtual one from them
                    if parts and len(parts) > 1:
                        label = ', '.join(parts)
                    else:
                        label = ','.join(file_type)
                    self.document_type = {
                        'id': '_legacy',
                        'label': label,
                        'mimetypes': file_type,
                    }
            del self.__dict__['file_type']
            changed = True
        return changed

    def export_to_xml(self, charset, include_id=False):
        # convert some sub-fields to strings as export_to_xml() only supports
        # dictionnaries with strings values
        if self.document_type and self.document_type.get('mimetypes'):
            old_value = self.document_type['mimetypes']
            self.document_type['mimetypes'] = '|'.join(self.document_type['mimetypes'])
        result = super(FileField, self).export_to_xml(charset, include_id=include_id)
        if self.document_type and self.document_type.get('mimetypes'):
            self.document_type['mimetypes'] = old_value
        return result

    def init_with_xml(self, element, charset, include_id=False):
        super(FileField, self).init_with_xml(element, charset, include_id=include_id)
        # translate fields flattened to strings
        if self.document_type and self.document_type.get('mimetypes'):
            self.document_type['mimetypes'] = self.document_type['mimetypes'].split('|')
        if self.document_type and self.document_type.get('fargo'):
            self.document_type['fargo'] = self.document_type['fargo'] == 'True'


register_field_class(FileField)


class DateField(WidgetField):
    key = 'date'
    description = N_('Date')

    widget_class = DateWidget
    minimum_date = None
    maximum_date = None
    minimum_is_future = False
    date_in_the_past = False
    date_can_be_today = False
    extra_attributes = ['minimum_date', 'minimum_is_future', 'maximum_date',
            'date_in_the_past', 'date_can_be_today']

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        form.add(DateWidget, 'minimum_date', title = _('Minimum Date'), value = self.minimum_date)
        form.add(CheckboxWidget, 'minimum_is_future',
                title = _('Date must be in the future'), value = self.minimum_is_future,
                hint = _('This option is obviously not compatible with setting a minimum date'))
        form.add(DateWidget, 'maximum_date', title = _('Maximum Date'), value = self.maximum_date)
        form.add(CheckboxWidget, 'date_in_the_past',
                title = _('Date must be in the past'), value = self.date_in_the_past,
                hint = _('This option is obviously not compatible with setting a maximum date'))
        form.add(CheckboxWidget, 'date_can_be_today',
                title = _('Date can be present day'), value = self.date_can_be_today,
                hint = _('This option is only useful combined with one of the previous checkboxes.'))

    def get_admin_attributes(self):
        return WidgetField.get_admin_attributes(self) + [
                'minimum_date', 'minimum_is_future', 'maximum_date',
                'date_in_the_past', 'date_can_be_today']

    @classmethod
    def convert_value_from_anything(cls, value):
        if value is None or value == '':
            return None
        date_value = evalutils.make_date(value).timetuple()  # could raise ValueError
        return date_value

    def convert_value_from_str(self, value):
        return time.strptime(value, date_format())

    def convert_value_to_str(self, value):
        if value is None:
            return ''
        try:
            return strftime(date_format(), value)
        except TypeError:
            return ''

    def add_to_form(self, form, value=None):
        if value and type(value) is not str:
            value = self.convert_value_to_str(value)
        return WidgetField.add_to_form(self, form, value=value)

    def add_to_view_form(self, form, value = None):
        value = localstrftime(value)
        return WidgetField.add_to_view_form(self, form, value = value)

    def get_view_value(self, value):
        try:
            return strftime(misc.date_format(), value)
        except TypeError:
            return value

    def get_json_value(self, value):
        try:
            return strftime('%Y-%m-%d', value)
        except TypeError:
            return ''

    def from_json_value(self, value):
        try:
            return time.strptime(value, '%Y-%m-%d')
        except (TypeError, ValueError):
            return None


register_field_class(DateField)


def item_items_stats(field, values):
    if field.data_source:
        options = data_sources.get_items(field.data_source)
    else:
        options = field.items or []
    if len(options) == 0:
        return None
    no_records = len(values)
    if no_records == 0:
        return None
    r = TemplateIO(html = True)
    r += htmltext('<table class="stats">')
    r += htmltext('<thead><tr><th colspan="4">')
    r += field.label
    r += htmltext('</th></tr></thead>')
    r += htmltext('<tbody>')
    for option in options:
        if type(option) in (tuple, list):
            option_label = option[1]
            option_value = str(option[0])
        else:
            option_label = option
            option_value = option
        if field.type == 'item':
            no = len([None for x in values if x.data.get(field.id) == option_value])
        else:
            no = len([None for x in values if option_value in (x.data.get(field.id) or [])])
        r += htmltext('<tr>')
        r += htmltext('<td class="label">')
        r += option_label
        r += htmltext('</td>')

        r += htmltext('<td class="percent">')
        r += htmltext(' %.2f&nbsp;%%') % (100.*no/no_records)
        r += htmltext('</td>')
        r += htmltext('<td class="total">')
        r += '(%d/%d)' % (no, no_records)
        r += htmltext('</td>')
        r += htmltext('</tr>')
        r += htmltext('<tr>')
        r += htmltext('<td class="bar" colspan="3">')
        r += htmltext('<span style="width: %d%%"></span>' % (100*no/no_records))
        r += htmltext('</td>')
        r += htmltext('</tr>')
    r += htmltext('</tbody>')
    r += htmltext('</table>')
    return r.getvalue()


class ItemField(WidgetField):
    key = 'item'
    description = N_('List')

    items = []
    show_as_radio = None
    anonymise = False
    widget_class = SingleSelectHintWidget
    data_source = {}
    in_filters = False
    display_disabled_items = False
    display_mode = 'list'

    def __init__(self, **kwargs):
        self.items = []
        WidgetField.__init__(self, **kwargs)

    def migrate(self):
        changed = super(ItemField, self).migrate()
        if isinstance(getattr(self, 'show_as_radio', None), bool):
            if self.show_as_radio:
                self.display_mode = 'radio'
            else:
                self.display_mode = 'list'
            self.show_as_radio = None
            changed = True
        return changed

    def init_with_xml(self, element, charset, include_id=False):
        super(ItemField, self).init_with_xml(element, charset, include_id=include_id)
        if getattr(element.find('show_as_radio'), 'text', None) == 'True':
            self.display_mode = 'radio'

    def get_options(self, mode=None):
        if self.data_source:
            return [x[:3] for x in data_sources.get_items(self.data_source, mode=mode)]
        if self.items:
            return [(x, x) for x in self.items]
        return []

    def perform_more_widget_changes(self, form, kwargs, edit=True):
        data_source = data_sources.get_object(self.data_source)

        if data_source and data_source.type == 'jsonp':
            # a source defined as JSONP can only be used in autocomplete mode
            self.display_mode = 'autocomplete'

        if self.display_mode == 'autocomplete' and data_source and data_source.can_jsonp():
            self.url = kwargs['url'] = data_source.get_jsonp_url()
            self.widget_class = JsonpSingleSelectWidget
            return

        if self.items:
            kwargs['options'] = self.get_options()
        elif self.data_source:
            items = data_sources.get_items(self.data_source,
                    include_disabled=self.display_disabled_items)
            kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
            kwargs['options_with_attributes'] = items[:]
        else:
            kwargs['options'] = self.get_options()
        if not kwargs.get('options'):
            kwargs['options'] = [(None, '---')]
        if self.display_mode == 'radio':
            self.widget_class = RadiobuttonsWidget
            if type(kwargs['options'][0]) is str:
                first_items = [x for x in kwargs['options'][:3]]
            else:
                first_items = [x[1] for x in kwargs['options'][:3]]
            length_first_items = sum([len(x) for x in first_items])
            if len(kwargs['options']) > 3 or length_first_items > 40:
                # TODO: absence/presence of delimitor should be an option
                kwargs['delim'] = htmltext('<br />')
        elif self.display_mode == 'autocomplete':
            kwargs['select2'] = True

    def get_display_value(self, value):
        data_source = data_sources.get_object(self.data_source)
        if data_source is None:
            return value or ''

        if data_source.type == 'jsonp':
            if not get_session().jsonp_display_values:
                get_session().jsonp_display_values = {}
            return get_session().jsonp_display_values.get(
                            '%s_%s' % (data_source.get_jsonp_url(), value))

        return data_source.get_display_value(value)

    def add_to_view_form(self, form, value = None):
        real_value = value
        label_value = ''
        if value is not None:
            label_value = self.get_display_value(value)
        self.field_key = 'f%s' % self.id

        get_request().form[self.field_key + '_label'] = label_value # :/
        form.add(StringWidget, self.field_key + '_label', title = self.label,
                 value=label_value, readonly='readonly', size=len(label_value or '')+2)
        form.add(HiddenWidget, self.field_key, value=real_value)
        widget = form.get_widget(self.field_key + '_label')
        if self.extra_css_class:
            if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
                widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
            else:
                widget.extra_css_class = self.extra_css_class

    def store_display_value(self, data, field_id):
        value = data.get(field_id)
        data_source = data_sources.get_object(self.data_source)
        if data_source and data_source.type == 'jsonp':
            if get_request():
                display_value = get_request().form.get('f%s_display' % field_id)
                real_data_source = data_source.data_source
                if display_value is None:
                    if not get_session().jsonp_display_values:
                        get_session().jsonp_display_values = {}
                    display_value = get_session().jsonp_display_values.get(
                                    '%s_%s' % (real_data_source.get('value'), value))
                else:
                    if not get_session().jsonp_display_values:
                        get_session().jsonp_display_values = {}
                    get_session().jsonp_display_values[
                            '%s_%s' % (real_data_source.get('value'), value)] = display_value
                return display_value
        return self.get_display_value(value)

    def store_structured_value(self, data, field_id):
        data_source = data_sources.get_object(self.data_source)
        if data_source is None:
            return

        if data_source.type == 'jsonp':
            return

        value = data_source.get_structured_value(data.get(field_id))
        if value is None or set(value.keys()) == set(['id', 'text']):
            return
        return value

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        form.add(CheckboxWidget, 'in_filters', title=_('Display in default filters'),
                value=self.in_filters, advanced=True)
        options = [('list', _('List')),
                   ('radio', _('Radio buttons')),
                   ('autocomplete', _('Autocomplete')),
                  ]
        form.add(RadiobuttonsWidget, 'display_mode',
                title=_('Display Mode'),
                options=options,
                value=self.display_mode)
        form.add(WidgetList, 'items', title = _('Items'), element_type = StringWidget,
                value = self.items, required = False,
                element_kwargs = {'render_br': False, 'size': 50},
                add_element_label = _('Add item'))
        form.add(data_sources.DataSourceSelectionWidget, 'data_source',
                 value=self.data_source,
                 title=_('Data Source'),
                 hint=_('This will get the available items from an external source.'),
                 required=False,
                 advanced=is_datasource_advanced(self.data_source))
        form.add(CheckboxWidget, 'display_disabled_items',
                title=_('Display disabled items'),
                value=self.display_disabled_items,
                advanced=not(self.display_disabled_items))

    def get_admin_attributes(self):
        return WidgetField.get_admin_attributes(self) + ['items',
                'display_mode', 'data_source', 'in_filters', 'anonymise',
                'display_disabled_items']

    def check_admin_form(self, form):
        items = form.get_widget('items').parse()
        d = {}
        for v in (items or []):
            if v in d:
                form.set_error('items', _('Duplicated Items'))
                return
            d[v] = None

    def stats(self, values):
        return item_items_stats(self, values)

    def feed_session(self, value, display_value):
        real_data_source = data_sources.get_real(self.data_source)
        if real_data_source and real_data_source.get('type') == 'jsonp':
            if not get_session().jsonp_display_values:
                get_session().jsonp_display_values = {}
            get_session().jsonp_display_values[
                    '%s_%s' % (real_data_source.get('value'), value)] = display_value

    def get_csv_heading(self):
        headings = [self.label]
        if self.data_source:
            headings.append('')
        return headings

    def get_csv_value(self, value, display_value=None, **kwargs):
        values = [value]
        if self.data_source:
            values.append(display_value)
        return values

    def export_to_json(self, include_id=False, anonymise=True):
        field = super(ItemField, self).export_to_json(include_id=include_id, anonymise=anonymise)
        if self.data_source and not anonymise:
            structured_items = data_sources.get_structured_items(self.data_source)
            if structured_items:
                field['structured_items'] = structured_items
                if not field.get('items'):
                    field['items'] = [item['text'] for item in structured_items]
        return field


register_field_class(ItemField)


class ItemsField(WidgetField):
    key = 'items'
    description = N_('Multiple choice list')

    items = []
    max_choices = 0
    data_source = {}
    in_filters = False
    display_disabled_items = False

    widget_class = CheckboxesWidget

    _cached_data_source = None

    def __init__(self, **kwargs):
        self.items = []
        WidgetField.__init__(self, **kwargs)

    def get_options(self):
        if self.data_source:
            if self._cached_data_source:
                return self._cached_data_source
            self._cached_data_source = [x[:3] for x in data_sources.get_items(self.data_source)]
            return self._cached_data_source[:]
        elif self.items:
            return [(x, x) for x in self.items]
        else:
            return []

    def perform_more_widget_changes(self, form, kwargs, edit = True):
        kwargs['options'] = self.get_options()
        kwargs['max_choices'] = self.max_choices
        if self.data_source:
            items = data_sources.get_items(self.data_source,
                    include_disabled=self.display_disabled_items)
            kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
            kwargs['options_with_attributes'] = items[:]

        if len(kwargs['options']) > 3:
            kwargs['inline'] = False

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        form.add(CheckboxWidget, 'in_filters', title=_('Display in default filters'),
                value=self.in_filters, advanced=True)
        form.add(WidgetList, 'items', title = _('Items'), element_type = StringWidget,
                value = self.items, required = False,
                element_kwargs = {'render_br': False, 'size': 50},
                add_element_label = _('Add item'))
        form.add(IntWidget, 'max_choices', title = _('Maximum number of choices'),
                value = self.max_choices, required = False, size = 4)
        form.add(data_sources.DataSourceSelectionWidget, 'data_source',
                 value=self.data_source,
                 allow_jsonp=False,
                 title=_('Data Source'),
                 hint=_('This will get the available items from an external source.'),
                 required=False,
                 advanced=is_datasource_advanced(self.data_source))
        form.add(CheckboxWidget, 'display_disabled_items',
                title=_('Display disabled items'),
                value=self.display_disabled_items,
                advanced=not(self.display_disabled_items))

    def get_admin_attributes(self):
        return WidgetField.get_admin_attributes(self) + ['items',
                'max_choices', 'data_source', 'in_filters', 'anonymise',
                'display_disabled_items']

    def check_admin_form(self, form):
        items = form.get_widget('items').parse()
        d = {}
        for v in (items or []):
            if v in d:
                form.set_error('items', _('Duplicated Items'))
                return
            d[v] = None

    def get_prefill_value(self, user=None, force_string=True):
        return super(ItemsField, self).get_prefill_value(user=user, force_string=False)

    def convert_value_to_str(self, value):
        return value

    def get_view_value(self, value):
        if type(value) is str: # == display_value
            return value
        if value:
            try:
                return ', '.join([(x) for x in value])
            except TypeError:
                pass
        return ''

    def stats(self, values):
        return item_items_stats(self, values)

    def get_csv_heading(self):
        labels = [self.label]
        if self.max_choices:
            labels.extend(['']*(self.max_choices-1))
        elif len(self.get_options()):
            labels.extend(['']*(len(self.get_options())-1))
        return labels

    def get_csv_value(self, value, **kwargs):
        values = []
        for one_value in value:
            values.append(one_value)
        if self.max_choices:
            nb_columns = self.max_choices
        elif len(self.get_options()):
            nb_columns = len(self.get_options())
        else:
            nb_columns = 1

        if len(values) > nb_columns:
            # this would happen if max_choices is set after forms were already
            # filled with more values
            values = values[:nb_columns]
        elif len(values) < nb_columns:
            values.extend(['']*(nb_columns-len(values)))
        return values

    def store_display_value(self, data, field_id):
        kwargs = {}
        options = self.get_options()
        choices = []
        for choice in data.get(field_id) or []:
            if type(options[0]) is str:
                choices.append(choice)
            elif type(options[0]) in (tuple, list):
                if len(options[0]) == 2:
                    for key, option_value in options:
                        if str(key) == str(choice):
                            choices.append(option_value)
                            break
                elif len(options[0]) == 3:
                    for key, option_value, key_repeat in options:
                        if str(key) == str(choice):
                            choices.append(option_value)
                            break
        return ', '.join(choices)

    def store_structured_value(self, data, field_id):
        value = data.get(field_id)
        if not self.data_source:
            return
        structured_options = data_sources.get_structured_items(self.data_source)
        if not structured_options:
            return
        if not set(structured_options[0].keys()) != set(['id', 'text']):
            return
        structured_value = []
        for structured_option in structured_options:
            for choice in data.get(field_id) or []:
                if str(structured_option.get('id')) == str(choice):
                    structured_value.append(structured_option)
        return structured_value

    def export_to_json(self, include_id=False, anonymise=True):
        field = super(ItemsField, self).export_to_json(include_id=include_id, anonymise=True)
        if self.data_source and not anonymise:
            structured_items = data_sources.get_structured_items(self.data_source)
            if structured_items:
                field['structured_items'] = structured_items
                if not field.get('items'):
                    field['items'] = [item['text'] for item in structured_items]
        return field

register_field_class(ItemsField)


class PostConditionsRowWidget(CompositeWidget):
    def __init__(self, name, value=None, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)
        if not value:
            value = {}
        self.add(ConditionWidget, name='condition', title=_('Condition'),
                value=value.get('condition'), size=50)
        self.add(StringWidget, name='error_message', title=_('Error message if condition is not met'),
                value=value.get('error_message'), size=50)

    def _parse(self, request):
        if self.get('condition') or self.get('error_message'):
            self.value = {
                'condition': self.get('condition'),
                'error_message': self.get('error_message')
            }
        else:
            self.value = None


class PostConditionsTableWidget(WidgetListAsTable):
    readonly = False

    def __init__(self, name, **kwargs):
        super(PostConditionsTableWidget, self).__init__(name,
                element_type=PostConditionsRowWidget, **kwargs)

    def parse(self, request=None):
        super(PostConditionsTableWidget, self).parse(request=request)
        for post_condition in self.value or []:
            if not (post_condition.get('error_message') and post_condition.get('condition')):
                self.set_error(_('Both condition and error message are required.'))
                break
        return self.value


class PageCondition(Condition):
    log_errors = True
    record_errors = False

    def get_data(self):
        dict_vars = self.context['dict_vars']
        formdef = self.context['formdef']

        # create variables with values currently being evaluated, not yet
        # available in the formdata.
        from .formdata import get_dict_with_varnames
        live_data = {}
        form_live_data = {}
        if dict_vars is not None:
            live_data = get_dict_with_varnames(formdef.fields, dict_vars)
            form_live_data = dict(('form_' + x, y) for x, y in live_data.items())

        # 1) feed the form_var_* variables in the global substitution system,
        # they will shadow formdata context variables with their new "live"
        # value, this may be useful when evaluating data sources.
        class ConditionVars(object):
            def __init__(self, id_dict_var):
                # keep track of reference dictionary
                self.id_dict_var = id_dict_var

            def get_substitution_variables(self):
                return form_live_data

            def __eq__(self, other):
                # Assume all ConditionVars are equal when initialized with
                # the same live data dictionary; this avoids filling
                # the substitution sources with duplicates and invalidating its
                # cache.
                return self.id_dict_var == getattr(other, 'id_dict_var', None)

        if dict_vars is not None:
            # Add them only if there is a real dict_vars in context,
            # ie do nothing on first page condition
            get_publisher().substitutions.feed(ConditionVars(id(dict_vars)))

        data = super(PageCondition, self).get_data()
        # 2) add live data as var_ variables for local evaluation only, for
        # backward compatibility. They are not added globally as they would
        # interfere with the var_ prefixed variables used in dynamic jsonp
        # fields. (#9786)
        data = copy.copy(data)
        data.update(live_data)
        if dict_vars is None:
            # ConditionsVars is not set when evaluating first page condition,
            # but we need to have form_var_* variables already; add them from
            # form_live_data (where all variables will have been set to None).
            data.update(form_live_data)
        return data


class PageField(Field):
    key = 'page'
    description = N_('Page')

    post_conditions = None

    def post_conditions_init_with_xml(self, node, charset, include_id=False):
        if node is None:
            return
        self.post_conditions = []
        for post_condition_node in node.findall('post_condition'):
            if post_condition_node.findall('condition/type'):
                condition = {
                    'type': force_str(post_condition_node.find('condition/type').text),
                    'value': force_str(post_condition_node.find('condition/value').text),
                }
            elif post_condition_node.find('condition').text:
                condition = {
                    'type': 'python',
                    'value': force_str(post_condition_node.find('condition').text),
                }
            else:
                continue
            self.post_conditions.append({
                'condition': condition,
                'error_message': force_str(post_condition_node.find('error_message').text),
            })

    def post_conditions_export_to_xml(self, node, charset, include_id=False):
        if not self.post_conditions:
            return

        conditions_node = ET.SubElement(node, 'post_conditions')
        for post_condition in self.post_conditions:
            post_condition_node = ET.SubElement(conditions_node, 'post_condition')
            condition_node = ET.SubElement(post_condition_node, 'condition')
            ET.SubElement(condition_node, 'type').text = force_text(
                    post_condition['condition'].get('type') or '', charset, errors='replace')
            ET.SubElement(condition_node, 'value').text = force_text(
                    post_condition['condition'].get('value') or '', charset, errors='replace')
            ET.SubElement(post_condition_node, 'error_message').text = force_text(
                    post_condition['error_message'] or '', charset, errors='replace')

    def fill_admin_form(self, form):
        form.add(StringWidget, 'label', title = _('Label'), value = self.label,
                required = True, size = 50)
        form.add(ConditionWidget, 'condition', title=_('Display Condition'), value=self.condition,
                required=False, size=50)
        form.add(PostConditionsTableWidget, 'post_conditions',
                title=_('Post Conditions'),
                value=self.post_conditions,
                advanced=not(self.post_conditions))

    def get_admin_attributes(self):
        return Field.get_admin_attributes(self) + ['post_conditions']

    def migrate(self):
        changed = super(PageField, self).migrate()
        if isinstance(self.condition, six.string_types):
            if self.condition:
                self.condition = {'type': 'python', 'value': self.condition}
            else:
                self.condition = {}
            changed = True
        for post_condition in self.post_conditions or []:
            condition = post_condition.get('condition')
            if isinstance(condition, six.string_types):
                if condition:
                    post_condition['condition'] = {'type': 'python', 'value': condition}
                else:
                    post_condition['condition'] = {}
                changed = True
        return changed

    def add_to_view_form(self, *args):
        pass

register_field_class(PageField)


class TableField(WidgetField):
    key = 'table'
    description = N_('Table')

    rows = None
    columns = None

    widget_class = TableWidget

    def __init__(self, **kwargs):
        self.rows = []
        self.columns = []
        WidgetField.__init__(self, **kwargs)

    def perform_more_widget_changes(self, form, kwargs, edit = True):
        kwargs['rows'] = self.rows
        kwargs['columns'] = self.columns

    def get_display_locations_options(self):
        return [('validation', _('Validation Page')),
                ('summary', _('Summary Page'))]

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        try:
            form.remove('prefill')
        except KeyError: # perhaps it was already removed
            pass
        form.add(WidgetList, 'rows', title = _('Rows'), element_type = StringWidget,
                value = self.rows, required = True,
                element_kwargs = {'render_br': False, 'size': 50},
                add_element_label = _('Add row'))
        form.add(WidgetList, 'columns', title = _('Columns'), element_type = StringWidget,
                value = self.columns, required = True,
                element_kwargs = {'render_br': False, 'size': 50},
                add_element_label = _('Add column'))

    def get_admin_attributes(self):
        t = WidgetField.get_admin_attributes(self) + ['rows', 'columns']
        try:
            t.remove('prefill')
        except ValueError:
            pass
        return t

    def get_view_value(self, value):
        r = TemplateIO(html=True)
        r += htmltext('<table><thead><tr><td></td>')
        for column in self.columns:
            r += htmltext('<th>%s</th>') % column
        r += htmltext('</tr></thead><tbody>')
        for i, row in enumerate(self.rows):
            r += htmltext('<tr><th>%s</th>') % row
            for j, column in enumerate(self.columns):
                r += htmltext('<td>')
                if value:
                    try:
                        r += value[i][j]
                    except IndexError:
                        pass
                r += htmltext('</td>')
            r += htmltext('</tr>')
        r += htmltext('</tbody></table>')

        return r.getvalue()

    def get_rst_view_value(self, value, indent=''):
        if not value:
            return indent
        r = []
        max_width = 0
        for column in self.columns:
            max_width = max(max_width, len(smart_text(column)))

        for i, row in enumerate(value):
            value[i] = [x or '' for x in row]

        def get_value(i, j):
            try:
                return smart_text(value[i][j])
            except IndexError:
                return '-'

        for i, row in enumerate(self.rows):
            max_width = max(max_width, len(row))
            for j, column in enumerate(self.columns):
                max_width = max(max_width, len(get_value(i, j)))

        r.append(' '.join(['='*max_width]*(len(self.columns)+1)))
        r.append(' '.join([smart_text(column).center(max_width) for column in ['/']+self.columns]))
        r.append(' '.join(['='*max_width]*(len(self.columns)+1)))
        for i, row in enumerate(self.rows):
            r.append(' '.join([cell.center(max_width) for cell in [smart_text(row)] +
                    [get_value(i, x) for x in range(len(self.columns))]]))
        r.append(' '.join(['='*max_width]*(len(self.columns)+1)))
        return misc.site_encode('\n'.join([indent + x for x in r]))

    def get_csv_heading(self):
        if not self.columns:
            return [self.label]
        labels = []
        for col in self.columns:
            for row in self.rows:
                t = '%s / %s' % (col, row)
                if len(labels) == 0:
                    labels.append('%s - %s' % (self.label, t))
                else:
                    labels.append(t)
        return labels

    def get_csv_value(self, value, **kwargs):
        if not self.columns:
            return ['']
        values = []
        for i in range(len(self.columns)):
            for j in range(len(self.rows)):
                try:
                    values.append(value[j][i])
                except IndexError:
                    values.append('')
        return values

register_field_class(TableField)


class TableSelectField(TableField):
    key = 'table-select'
    description = N_('Table of Lists')

    items = None

    widget_class = SingleSelectTableWidget

    def __init__(self, **kwargs):
        self.items = []
        TableField.__init__(self, **kwargs)

    def fill_admin_form(self, form):
        TableField.fill_admin_form(self, form)
        form.add(WidgetList, 'items', title=_('Items'), element_type=StringWidget,
                value=self.items, required=True,
                element_kwargs = {'render_br': False, 'size': 50},
                add_element_label = _('Add item'))

    def perform_more_widget_changes(self, form, kwargs, edit=True):
        TableField.perform_more_widget_changes(self, form, kwargs, edit=edit)
        if edit:
            kwargs['options'] = self.items or [(None, '---')]
        else:
            self.widget_class = TableWidget

    def get_admin_attributes(self):
        return TableField.get_admin_attributes(self) + ['items']

    def check_admin_form(self, form):
        items = form.get_widget('items').parse()
        d = {}
        for v in (items or []):
            if v in d:
                form.set_error('items', _('Duplicated Items'))
                return
            d[v] = None

register_field_class(TableSelectField)


class TableRowsField(WidgetField):
    key = 'tablerows'
    description = N_('Table with rows')

    total_row = True
    columns = None

    widget_class = TableListRowsWidget

    def __init__(self, **kwargs):
        self.columns = []
        WidgetField.__init__(self, **kwargs)

    def perform_more_widget_changes(self, form, kwargs, edit = True):
        kwargs['columns'] = self.columns
        kwargs['add_element_label'] = _('Add row')

    def get_display_locations_options(self):
        return [('validation', _('Validation Page')),
                ('summary', _('Summary Page'))]

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        try:
            form.remove('prefill')
        except KeyError: # perhaps it was already removed
            pass
        form.add(WidgetList, 'columns', title = _('Columns'), element_type = StringWidget,
                value = self.columns, required = True,
                element_kwargs = {'render_br': False, 'size': 50},
                add_element_label = _('Add column'))
        form.add(CheckboxWidget, 'total_row', title = _('Total Row'),
                value = self.total_row)

    def get_admin_attributes(self):
        t = WidgetField.get_admin_attributes(self) + ['columns', 'total_row']
        try:
            t.remove('prefill')
        except ValueError:
            pass
        return t

    def get_view_value(self, value):
        r = TemplateIO(html=True)
        r += htmltext('<table><thead><tr>')
        for column in self.columns:
            r += htmltext('<th>%s</th>') % column
        r += htmltext('</tr></thead><tbody>')
        for i, row in enumerate(value):
            r += htmltext('<tr>')
            for j, column in enumerate(self.columns):
                r += htmltext('<td>')
                if value:
                    try:
                        r += row[j]
                    except IndexError:
                        pass
                r += htmltext('</td>')
            r += htmltext('</tr>')
        r += htmltext('</tbody>')

        if self.total_row:
            sums_row = []
            for j, column in enumerate(self.columns):
                sum_column = 0
                for row_value in value:
                    try:
                        cell_value = row_value[j]
                    except IndexError:
                        continue
                    if cell_value in (None, ''):
                        continue
                    try:
                        sum_column += float(cell_value)
                    except ValueError:
                        sums_row.append(None)
                        break
                else:
                    sums_row.append(sum_column)
            if [x for x in sums_row if x is not None]:
                r += htmltext('<tfoot><tr>')
                for sum_column in sums_row:
                    if sum_column is None:
                        r += htmltext('<td></td>')
                    else:
                        r += htmltext('<td>%.2f</td>' % sum_column)
                r += htmltext('</tr></tfoot>')

        r += htmltext('</table>')

        return r.getvalue()

    def get_rst_view_value(self, value, indent=''):
        if not value:
            return indent
        r = []
        max_width = 0
        for column in self.columns:
            max_width = max(max_width, len(smart_text(column)))

        for i, row in enumerate(value):
            value[i] = [x or '' for x in row]

        def get_value(i, j):
            try:
                return smart_text(value[i][j])
            except IndexError:
                return '-'

        for i, row_value in enumerate(value):
            for j, column in enumerate(self.columns):
                try:
                    max_width = max(max_width, len(smart_text(row_value[j])))
                except IndexError:
                    # ignore errors for shorter than expected rows, this is
                    # typical of the field gaining new columns after some forms
                    # were already saved.
                    pass

        r.append(' '.join(['='*max_width]*(len(self.columns))))
        r.append(' '.join([smart_text(column).center(max_width) for column in self.columns]))
        r.append(' '.join(['='*max_width]*(len(self.columns))))
        for i, row_value in enumerate(value):
            r.append(' '.join([cell.center(max_width) for cell in
                    [get_value(i, x) for x in range(len(self.columns))]]))
        r.append(' '.join(['='*max_width]*(len(self.columns))))
        return misc.site_encode('\n'.join([indent + x for x in r]))

    def get_csv_value(self, element, **kwargs):
        return [_('unimplemented')] # XXX


register_field_class(TableRowsField)


class MapField(WidgetField):
    key = 'map'
    description = N_('Map')

    initial_zoom = None
    min_zoom = None
    max_zoom = None
    default_position = None
    init_with_geoloc = False

    widget_class = MapWidget
    extra_attributes = ['initial_zoom', 'min_zoom', 'max_zoom',
            'default_position', 'init_with_geoloc']

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        # 0: whole world, 9: wide area, 11: area, 13: town, 16: small road
        zoom_levels = [(None, '---'),
                ('0', _('Whole world')),
                ('9', _('Wide area')),
                ('11', _('Area')),
                ('13', _('Town')),
                ('16', _('Small road')),
                ('19', _('Ant')),]
        form.add(SingleSelectWidget, 'initial_zoom', title=_('Initial zoom level'),
                value=self.initial_zoom or '13', options=zoom_levels)
        form.add(SingleSelectWidget, 'min_zoom', title=_('Minimal zoom level'),
                value=self.min_zoom, options=zoom_levels, required=False)
        form.add(SingleSelectWidget, 'max_zoom', title=_('Maximal zoom level'),
                value=self.max_zoom, options=zoom_levels, required=False)
        form.add(MapWidget, 'default_position', title=_('Initial Position'),
                value=self.default_position, default_zoom='9', required=False)
        form.add(CheckboxWidget, 'init_with_geoloc',
                title=_('Initialize position using device geolocation'),
                value=self.init_with_geoloc, required=False)

    def check_admin_form(self, form):
        initial_zoom = form.get_widget('initial_zoom').parse()
        min_zoom = form.get_widget('min_zoom').parse()
        max_zoom = form.get_widget('max_zoom').parse()
        if min_zoom and max_zoom:
            if int(min_zoom) > int(max_zoom):
                form.get_widget('min_zoom').set_error(
                        _('Minimal zoom level cannot be greater than maximal zoom level.'))
        if (initial_zoom and min_zoom and int(initial_zoom) < int(min_zoom)) or (
                (initial_zoom and max_zoom and int(initial_zoom) > int(max_zoom))):
            form.get_widget('initial_zoom').set_error(
                        _('Initial zoom level must be between minimal and maximal zoom levels.'))

    def get_admin_attributes(self):
        return WidgetField.get_admin_attributes(self) + ['initial_zoom',
                'min_zoom', 'max_zoom', 'default_position',
                'init_with_geoloc']

    def get_prefill_value(self, user=None, force_string=True):
        if self.prefill.get('type') != 'string' or not self.prefill.get('value'):
            return (None, False)
        # string is actually interpreted as a template of a string that will be
        # geocoded
        from wcs.wf.geolocate import GeolocateWorkflowStatusItem
        geolocate = GeolocateWorkflowStatusItem()
        geolocate.method = 'address_string'
        geolocate.address_string = self.prefill.get('value')
        coords = geolocate.geolocate_address_string(None)
        if not coords:
            return (None, False)
        return ('%(lat)s;%(lon)s' % coords, False)

    def get_view_value(self, value):
        widget = self.widget_class('x%s' % random.random(), value, readonly=True)
        return widget.render_widget_content()

    def get_rst_view_value(self, value, indent=''):
        return indent + value

    def get_json_value(self, value):
        if not value or ';' not in value:
            return None
        lat, lon = value.split(';')
        try:
            lat = float(lat)
            lon = float(lon)
        except ValueError:
            return None
        return {'lat': lat, 'lon': lon}

    def from_json_value(self, value):
        if 'lat' in value and 'lon' in value:
            return '%s;%s' % (float(value['lat']), float(value['lon']))
        else:
            return None

    def get_structured_value(self, data):
        return self.get_json_value(data.get(self.id))


register_field_class(MapField)


class RankedItemsField(WidgetField):
    key = 'ranked-items'
    description = N_('Ranked Items')

    items = []
    randomize_items = False
    widget_class = RankedItemsWidget
    anonymise = False

    def perform_more_widget_changes(self, form, kwargs, edit = True):
        kwargs['elements'] = self.items or []
        kwargs['randomize_items'] = self.randomize_items

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        try:
            form.remove('prefill')
        except KeyError:  # perhaps it was already removed
            pass
        form.add(WidgetList, 'items', title = _('Items'), element_type = StringWidget,
                value = self.items, required = True,
                element_kwargs = {'render_br': False, 'size': 50},
                add_element_label = _('Add item'))
        form.add(CheckboxWidget, 'randomize_items', title = _('Randomize Items'),
                value = self.randomize_items)

    def get_admin_attributes(self):
        attrs = WidgetField.get_admin_attributes(self) + ['items', 'randomize_items']
        if 'prefill' in attrs:
            attrs.remove('prefill')
        return attrs

    def get_view_value(self, value):
        r = TemplateIO(html=True)
        r += htmltext('<ul>')
        items = list(value.items())
        items.sort(key=lambda x: x[1] or sys.maxsize)
        counter = 0
        last_it = None
        for it in items:
            if it[1] is not None:
                if last_it != it[1]:
                    counter += 1
                    last_it = it[1]
                r += htmltext('<li>%s: %s</li>') % (counter, it[0])
        r += htmltext('</ul>')
        return r.getvalue()

    def get_rst_view_value(self, value, indent=''):
        items = list(value.items())
        items.sort(key=lambda x: x[1] or sys.maxsize)
        counter = 0
        last_it = None
        values = []
        for it in items:
            if it[1] is not None:
                if last_it != it[1]:
                    counter += 1
                    last_it = it[1]
                values.append('%s: %s' % (counter, it[0]))
        return indent + ' / '.join(values)

    def get_csv_heading(self):
        if not self.items:
            return [self.label]
        return [self.label] + ['']*(len(self.items)-1)

    def get_csv_value(self, value, **kwargs):
        if not self.items:
            return ['']
        if type(value) is not dict:
            value = {}
        items = [x for x in value.items() if x[1] is not None]
        items.sort(key=lambda x: x[1])
        ranked = [x[0] for x in items]
        return ranked + ['' for x in range(len(self.items)-len(ranked))]


register_field_class(RankedItemsField)


class PasswordField(WidgetField):
    key = 'password'
    description = N_('Password')

    min_length = 0
    max_length = 0
    count_uppercase = 0
    count_lowercase = 0
    count_digit = 0
    count_special = 0
    confirmation = True
    confirmation_title = None
    strength_indicator = True
    formats = ['sha1']
    extra_attributes = ['formats', 'min_length', 'max_length',
            'count_uppercase', 'count_lowercase', 'count_digit',
            'count_special', 'confirmation', 'confirmation_title',
            'strength_indicator']

    widget_class = PasswordEntryWidget

    def get_admin_attributes(self):
        return WidgetField.get_admin_attributes(self) + self.extra_attributes

    def fill_admin_form(self, form):
        WidgetField.fill_admin_form(self, form)
        formats = [('cleartext', _('Clear text')),
            ('md5', _('MD5')),
            ('sha1', _('SHA1')),
            ]
        form.add(CheckboxesWidget, 'formats', title=_('Storage formats'),
                value=self.formats, options=formats, inline=True)
        form.add(IntWidget, 'min_length', title=_('Minimum length'),
                value=self.min_length)
        form.add(IntWidget, 'max_length', title=_('Maximum password length'),
                value=self.max_length,
                hint=_('0 for unlimited length'))
        form.add(IntWidget, 'count_uppercase',
                title=_('Minimum number of uppercase characters'),
                value=self.count_uppercase)
        form.add(IntWidget, 'count_lowercase',
                title=_('Minimum number of lowercase characters'),
                value=self.count_lowercase)
        form.add(IntWidget, 'count_digit',
                title=_('Minimum number of digits'),
                value=self.count_digit)
        form.add(IntWidget, 'count_special',
                title=_('Minimum number of special characters'),
                value=self.count_special)
        form.add(CheckboxWidget, 'strength_indicator',
                title=_('Add a password strength indicator'),
                value=self.strength_indicator)
        form.add(CheckboxWidget, 'confirmation',
                title=_('Add a confirmation input'),
                value=self.confirmation)
        form.add(StringWidget, 'confirmation_title', size=50,
                title=_('Label for confirmation input'),
                value=self.confirmation_title)

    def get_view_value(self, value):
        return '●'*8

    def get_csv_value(self, value, **kwargs):
        return [self.get_view_value(value)]

    def get_rst_view_value(self, value, indent=''):
        return indent + self.get_view_value(value)

register_field_class(PasswordField)


def get_field_class_by_type(type):
    for k in field_classes:
        if k.key == type:
            return k
    raise KeyError()

def get_field_types():
    return field_types

